diff options
Diffstat (limited to 'src/components')
44 files changed, 608 insertions, 373 deletions
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue index 7139d4fc..42f89be9 100644 --- a/src/components/checkbox/checkbox.vue +++ b/src/components/checkbox/checkbox.vue @@ -5,12 +5,16 @@ > <input type="checkbox" + class="visible-for-screenreader-only" :disabled="disabled" :checked="modelValue" :indeterminate="indeterminate" @change="$emit('update:modelValue', $event.target.checked)" > - <i class="checkbox-indicator" /> + <i + class="checkbox-indicator" + :aria-hidden="true" + /> <span v-if="!!$slots.default" class="label" @@ -33,6 +37,7 @@ export default { <style lang="scss"> @import "../../variables"; +@import "../../mixins"; .checkbox { position: relative; @@ -81,8 +86,6 @@ export default { } input[type="checkbox"] { - display: none; - &:checked + .checkbox-indicator::before { color: $fallback--text; color: var(--inputText, $fallback--text); diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue index 92622cf9..dc8bbfd3 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -20,6 +20,7 @@ class="logo" :to="{ name: 'root' }" :style="logoBgStyle" + :title="sitename" > <div class="mask" @@ -38,13 +39,13 @@ /> <button class="button-unstyled nav-icon" + :title="$t('nav.preferences')" @click.stop="openSettingsModal" > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" icon="cog" - :title="$t('nav.preferences')" /> </button> <a @@ -52,26 +53,26 @@ href="/pleroma/admin/#/login-pleroma" class="nav-icon" target="_blank" + :title="$t('nav.administration')" @click.stop > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" icon="tachometer-alt" - :title="$t('nav.administration')" /> </a> <span class="spacer" /> <button v-if="currentUser" class="button-unstyled nav-icon" + :title="$t('login.logout')" @click.stop.prevent="logout" > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" icon="sign-out-alt" - :title="$t('login.logout')" /> </button> </div> diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index ba5f7552..68654f69 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -1,6 +1,7 @@ import Completion from '../../services/completion/completion.js' import EmojiPicker from '../emoji_picker/emoji_picker.vue' import Popover from 'src/components/popover/popover.vue' +import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue' import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue' import { take } from 'lodash' import { findOffset } from '../../services/offset_finder/offset_finder.service.js' @@ -109,9 +110,10 @@ const EmojiInput = { }, data () { return { + randomSeed: `${Math.random()}`.replace('.', '-'), input: undefined, caretEl: undefined, - highlighted: 0, + highlighted: -1, caret: 0, focused: false, blurTimeout: null, @@ -125,12 +127,16 @@ const EmojiInput = { components: { Popover, EmojiPicker, - UnicodeDomainIndicator + UnicodeDomainIndicator, + ScreenReaderNotice }, computed: { padEmoji () { return this.$store.getters.mergedConfig.padEmoji }, + defaultCandidateIndex () { + return this.$store.getters.mergedConfig.autocompleteSelect ? 0 : -1 + }, preText () { return this.modelValue.slice(0, this.caret) }, @@ -203,6 +209,12 @@ const EmojiInput = { top: this.input.scrollTop, left: this.input.scrollLeft }) + }, + suggestionListId () { + return `suggestions-${this.randomSeed}` + }, + suggestionItemId () { + return (index) => `suggestion-item-${index}-${this.randomSeed}` } }, mounted () { @@ -278,6 +290,11 @@ const EmojiInput = { ...rest, img: imageUrl || '' })) + this.highlighted = this.defaultCandidateIndex + this.$refs.screenReaderNotice.announce( + this.$tc('tool_tip.autocomplete_available', + this.suggestions.length, + { number: this.suggestions.length })) } }, methods: { @@ -374,26 +391,27 @@ const EmojiInput = { }, cycleBackward (e) { const len = this.suggestions.length || 0 - if (len > 1) { - this.highlighted -= 1 - if (this.highlighted < 0) { - this.highlighted = this.suggestions.length - 1 - } + + this.highlighted -= 1 + if (this.highlighted === -1) { + this.input.focus() + } else if (this.highlighted < -1) { + this.highlighted = len - 1 + } + if (len > 0) { e.preventDefault() - } else { - this.highlighted = 0 } }, cycleForward (e) { const len = this.suggestions.length || 0 - if (len > 1) { - this.highlighted += 1 - if (this.highlighted >= len) { - this.highlighted = 0 - } + + this.highlighted += 1 + if (this.highlighted >= len) { + this.highlighted = -1 + this.input.focus() + } + if (len > 0) { e.preventDefault() - } else { - this.highlighted = 0 } }, scrollIntoView () { @@ -540,6 +558,13 @@ const EmojiInput = { }) }, resize () { + }, + autoCompleteItemLabel (suggestion) { + if (suggestion.user) { + return suggestion.displayText + ' ' + suggestion.detailText + } else { + return this.maybeLocalizedEmojiName(suggestion) + } } } } diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue index ccba0393..7f9ecc99 100644 --- a/src/components/emoji_input/emoji_input.vue +++ b/src/components/emoji_input/emoji_input.vue @@ -4,12 +4,19 @@ class="emoji-input" :class="{ 'with-picker': !hideEmojiButton }" > - <slot /> + <slot + :id="'textbox-' + randomSeed" + :aria-owns="suggestionListId" + aria-autocomplete="both" + :aria-expanded="showSuggestions" + :aria-activedescendant="(!showSuggestions || highlighted === -1) ? '' : suggestionItemId(highlighted)" + /> <!-- TODO: make the 'x' disappear if at the end maybe? --> <div ref="hiddenOverlay" class="hidden-overlay" :style="overlayStyle" + :aria-hidden="true" > <span>{{ preText }}</span> <span @@ -18,11 +25,16 @@ >x</span> <span>{{ postText }}</span> </div> + <screen-reader-notice + ref="screenReaderNotice" + aria-live="assertive" + /> <template v-if="enableEmojiPicker"> <button v-if="!hideEmojiButton" class="button-unstyled emoji-picker-icon" type="button" + :title="$t('emoji.add_emoji')" @click.prevent="togglePicker" > <FAIcon :icon="['far', 'smile-beam']" /> @@ -43,17 +55,24 @@ ref="suggestorPopover" class="autocomplete-panel" placement="bottom" + :trigger-attrs="{ 'aria-hidden': true }" > <template #content> <div + :id="suggestionListId" ref="panel-body" class="autocomplete-panel-body" + role="listbox" > <div v-for="(suggestion, index) in suggestions" + :id="suggestionItemId(index)" :key="index" class="autocomplete-item" + role="option" :class="{ highlighted: index === highlighted }" + :aria-label="autoCompleteItemLabel(suggestion)" + :aria-selected="index === highlighted" @click.stop.prevent="onClick($event, suggestion)" > <span class="image"> diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js index adaa879e..e746dcd7 100644 --- a/src/components/emoji_input/suggestor.js +++ b/src/components/emoji_input/suggestor.js @@ -94,8 +94,9 @@ export const suggestUsers = ({ dispatch, state }) => { const newSuggestions = state.users.users.filter( user => - user.screen_name.toLowerCase().startsWith(noPrefix) || - user.name.toLowerCase().startsWith(noPrefix) + user.screen_name && user.name && ( + user.screen_name.toLowerCase().startsWith(noPrefix) || + user.name.toLowerCase().startsWith(noPrefix)) ).slice(0, 20).sort((a, b) => { let aScore = 0 let bScore = 0 diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js index 0d7ca812..349b043d 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -98,6 +98,11 @@ const EmojiPicker = { required: false, type: Boolean, default: false + }, + hideCustomEmoji: { + required: false, + type: Boolean, + default: false } }, data () { @@ -280,6 +285,9 @@ const EmojiPicker = { return 0 }, allCustomGroups () { + if (this.hideCustomEmoji) { + return {} + } const emojis = this.$store.getters.groupedCustomEmojis if (emojis.unpacked) { emojis.unpacked.text = this.$t('emoji.unpacked') diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue index ca90bf26..6972164b 100644 --- a/src/components/emoji_picker/emoji_picker.vue +++ b/src/components/emoji_picker/emoji_picker.vue @@ -3,6 +3,7 @@ ref="popover" trigger="click" popover-class="emoji-picker popover-default" + :trigger-attrs="{ 'aria-hidden': true }" @show="onPopoverShown" @close="onPopoverClosed" > diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue index a63daa97..eb46018e 100644 --- a/src/components/emoji_reactions/emoji_reactions.vue +++ b/src/components/emoji_reactions/emoji_reactions.vue @@ -2,7 +2,7 @@ <div class="EmojiReactions"> <UserListPopover v-for="(reaction) in emojiReactions" - :key="reaction.name" + :key="reaction.url || reaction.name" :users="accountsForEmoji[reaction.name]" > <button @@ -11,7 +11,21 @@ @click="emojiOnClick(reaction.name, $event)" @mouseenter="fetchEmojiReactionsByIfMissing()" > - <span class="reaction-emoji">{{ reaction.name }}</span> + <span + class="reaction-emoji" + > + <img + v-if="reaction.url" + :src="reaction.url" + :title="reaction.name" + class="reaction-emoji-content" + width="1em" + > + <span + v-else + class="reaction-emoji reaction-emoji-content" + >{{ reaction.name }}</span> + </span> <span>{{ reaction.count }}</span> </button> </UserListPopover> @@ -35,6 +49,8 @@ margin-top: 0.25em; flex-wrap: wrap; + --emoji-size: calc(1.25em * var(--emojiReactionsScale, 1)); + .emoji-reaction { padding: 0 0.5em; margin-right: 0.5em; @@ -45,8 +61,24 @@ box-sizing: border-box; .reaction-emoji { - width: 1.25em; + width: var(--emoji-size); + height: var(--emoji-size); margin-right: 0.25em; + line-height: var(--emoji-size); + display: flex; + justify-content: center; + align-items: center; + } + + .reaction-emoji-content { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + line-height: inherit; + overflow: hidden; + font-size: calc(var(--emoji-size) * 0.8); + margin: 0; } &:focus { diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue index 58d14945..8c883c13 100644 --- a/src/components/favorite_button/favorite_button.vue +++ b/src/components/favorite_button/favorite_button.vue @@ -38,13 +38,20 @@ class="button-unstyled interactive" target="_blank" role="button" + :title="$t('tool_tip.favorite')" :href="remoteInteractionLink" > - <FAIcon - class="fa-scale-110 fa-old-padding" - :title="$t('tool_tip.favorite')" - :icon="['far', 'star']" - /> + <FALayers class="fa-scale-110 fa-old-padding-layer"> + <FAIcon + class="fa-scale-110" + :icon="['far', 'star']" + /> + <FAIcon + class="focus-marker" + transform="shrink-6 up-9 right-12" + icon="plus" + /> + </FALayers> </a> <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0" diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue index bb7e64bc..e2ba74d1 100644 --- a/src/components/font_control/font_control.vue +++ b/src/components/font_control/font_control.vue @@ -4,6 +4,7 @@ :class="{ custom: isCustom }" > <label + :id="name + '-label'" :for="preset === 'custom' ? name : name + '-font-switcher'" class="label" > @@ -12,7 +13,8 @@ <input v-if="typeof fallback !== 'undefined'" :id="name + '-o'" - class="opt exlcude-disabled" + :aria-labelledby="name + '-label'" + class="opt exlcude-disabled visible-for-screenreader-only" type="checkbox" :checked="present" @change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" @@ -21,6 +23,7 @@ v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'" + :aria-hidden="true" /> {{ ' ' }} <Select diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js index 7f096316..face430e 100644 --- a/src/components/navigation/navigation.js +++ b/src/components/navigation/navigation.js @@ -80,3 +80,21 @@ export const ROOT_ITEMS = { criteria: ['announcements'] } } + +export function routeTo (item, currentUser) { + if (!item.route && !item.routeObject) return null + + let route + + if (item.routeObject) { + route = item.routeObject + } else { + route = { name: (item.anon || currentUser) ? item.route : item.anonRoute } + } + + if (USERNAME_ROUTES.has(route.name)) { + route.params = { username: currentUser.screen_name, name: currentUser.screen_name } + } + + return route +} diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js index 81cc936a..22ed77d9 100644 --- a/src/components/navigation/navigation_entry.js +++ b/src/components/navigation/navigation_entry.js @@ -1,5 +1,5 @@ import { mapState } from 'vuex' -import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js' +import { routeTo } from 'src/components/navigation/navigation.js' import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faThumbtack } from '@fortawesome/free-solid-svg-icons' @@ -26,17 +26,7 @@ const NavigationEntry = { }, computed: { routeTo () { - if (!this.item.route && !this.item.routeObject) return null - let route - if (this.item.routeObject) { - route = this.item.routeObject - } else { - route = { name: (this.item.anon || this.currentUser) ? this.item.route : this.item.anonRoute } - } - if (USERNAME_ROUTES.has(route.name)) { - route.params = { username: this.currentUser.screen_name, name: this.currentUser.screen_name } - } - return route + return routeTo(this.item, this.currentUser) }, getters () { return this.$store.getters diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js index 9dd795aa..ef78e44c 100644 --- a/src/components/navigation/navigation_pins.js +++ b/src/components/navigation/navigation_pins.js @@ -1,5 +1,5 @@ import { mapState } from 'vuex' -import { TIMELINES, ROOT_ITEMS, USERNAME_ROUTES } from 'src/components/navigation/navigation.js' +import { TIMELINES, ROOT_ITEMS, routeTo } from 'src/components/navigation/navigation.js' import { getListEntries, filterNavigation } from 'src/components/navigation/filter.js' import { library } from '@fortawesome/fontawesome-svg-core' @@ -31,14 +31,7 @@ const NavPanel = { props: ['limit'], methods: { getRouteTo (item) { - if (item.routeObject) { - return item.routeObject - } - const route = { name: (item.anon || this.currentUser) ? item.route : item.anonRoute } - if (USERNAME_ROUTES.has(route.name)) { - route.params = { username: this.currentUser.screen_name } - } - return route + return routeTo(item, this.currentUser) } }, computed: { diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index e1ea42ad..4d801c5e 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -121,7 +121,16 @@ scope="global" keypath="notifications.reacted_with" > - <span class="emoji-reaction-emoji">{{ notification.emoji }}</span> + <img + v-if="notification.emoji_url" + class="emoji-reaction-emoji emoji-reaction-emoji-image" + :src="notification.emoji_url" + :name="notification.emoji" + > + <span + v-else + class="emoji-reaction-emoji" + >{{ notification.emoji }}</span> </i18n-t> </small> </span> @@ -153,9 +162,9 @@ </router-link> <button class="button-unstyled expand-icon" - @click.prevent="toggleStatusExpanded" - :title="$t('tool_tip.toggle_expand')" :aria-expanded="statusExpanded" + :title="$t('tool_tip.toggle_expand')" + @click.prevent="toggleStatusExpanded" > <FAIcon class="fa-scale-110" diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index 41cfcef0..61f7317e 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -129,6 +129,13 @@ .emoji-reaction-emoji { font-size: 1.3em; + max-width: 1.25em; + height: 1.25em; + width: auto; + } + + .emoji-reaction-emoji-image { + vertical-align: middle; } .notification-details { diff --git a/src/components/poll/poll.js b/src/components/poll/poll.js index eda1733a..e4d6869a 100644 --- a/src/components/poll/poll.js +++ b/src/components/poll/poll.js @@ -12,7 +12,8 @@ export default { data () { return { loading: false, - choices: [] + choices: [], + randomSeed: `${Math.random()}`.replace('.', '-') } }, created () { diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue index cacc3298..b3a74c49 100644 --- a/src/components/poll/poll.vue +++ b/src/components/poll/poll.vue @@ -4,53 +4,63 @@ :class="containerClass" > <div - v-for="(option, index) in options" - :key="index" - class="poll-option" + :role="showResults ? 'section' : (poll.multiple ? 'group' : 'radiogroup')" > <div - v-if="showResults" - :title="resultTitle(option)" - class="option-result" + v-for="(option, index) in options" + :key="index" + class="poll-option" > - <div class="option-result-label"> - <span class="result-percentage"> - {{ percentageForOption(option.votes_count) }}% - </span> - <RichContent - :html="option.title_html" - :handle-links="false" - :emoji="emoji" + <div + v-if="showResults" + :title="resultTitle(option)" + class="option-result" + > + <div class="option-result-label"> + <span class="result-percentage"> + {{ percentageForOption(option.votes_count) }}% + </span> + <RichContent + :html="option.title_html" + :handle-links="false" + :emoji="emoji" + /> + </div> + <div + class="result-fill" + :style="{ 'width': `${percentageForOption(option.votes_count)}%` }" /> </div> <div - class="result-fill" - :style="{ 'width': `${percentageForOption(option.votes_count)}%` }" - /> - </div> - <div - v-else - @click="activateOption(index)" - > - <input - v-if="poll.multiple" - type="checkbox" - :disabled="loading" - :value="index" - > - <input v-else - type="radio" - :disabled="loading" - :value="index" + tabindex="0" + :role="poll.multiple ? 'checkbox' : 'radio'" + :aria-labelledby="`option-vote-${randomSeed}-${index}`" + :aria-checked="choices[index]" + @click="activateOption(index)" > - <label class="option-vote"> - <RichContent - :html="option.title_html" - :handle-links="false" - :emoji="emoji" - /> - </label> + <input + v-if="poll.multiple" + type="checkbox" + class="poll-checkbox" + :disabled="loading" + :value="index" + > + <input + v-else + type="radio" + :disabled="loading" + :value="index" + > + <label class="option-vote"> + <RichContent + :id="`option-vote-${randomSeed}-${index}`" + :html="option.title_html" + :handle-links="false" + :emoji="emoji" + /> + </label> + </div> </div> </div> <div class="footer faint"> @@ -161,5 +171,9 @@ padding: 0 0.5em; margin-right: 0.5em; } + + .poll-checkbox { + display: none; + } } </style> diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index eb55cfcc..b75fee69 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -8,6 +8,7 @@ import Gallery from 'src/components/gallery/gallery.vue' import StatusContent from '../status_content/status_content.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js' +import { propsToNative } from '../../services/attributes_helper/attributes_helper.service.js' import { reject, map, uniqBy, debounce } from 'lodash' import suggestor from '../emoji_input/suggestor.js' import { mapGetters, mapState } from 'vuex' @@ -629,6 +630,9 @@ const PostStatusForm = { }, openProfileTab () { this.$store.dispatch('openSettingsModalTab', 'profile') + }, + propsToNative (props) { + return propsToNative(props) } } } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index c51639db..86c1f907 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -30,6 +30,9 @@ <span>{{ $t('post_status.scope_notice.public') }}</span> <a class="fa-scale-110 fa-old-padding dismiss" + :title="$t('post_status.scope_notice_dismiss')" + role="button" + tabindex="0" @click.prevent="dismissScopeNotice()" > <FAIcon icon="times" /> @@ -42,6 +45,9 @@ <span>{{ $t('post_status.scope_notice.unlisted') }}</span> <a class="fa-scale-110 fa-old-padding dismiss" + :title="$t('post_status.scope_notice_dismiss')" + role="button" + tabindex="0" @click.prevent="dismissScopeNotice()" > <FAIcon icon="times" /> @@ -54,6 +60,9 @@ <span>{{ $t('post_status.scope_notice.private') }}</span> <a class="fa-scale-110 fa-old-padding dismiss" + :title="$t('post_status.scope_notice_dismiss')" + role="button" + tabindex="0" @click.prevent="dismissScopeNotice()" > <FAIcon icon="times" /> @@ -124,14 +133,17 @@ :suggest="emojiSuggestor" class="form-control" > - <input - v-model="newStatus.spoilerText" - type="text" - :placeholder="$t('post_status.content_warning')" - :disabled="posting && !optimisticPosting" - size="1" - class="form-post-subject" - > + <template #default="inputProps"> + <input + v-model="newStatus.spoilerText" + type="text" + :placeholder="$t('post_status.content_warning')" + :disabled="posting && !optimisticPosting" + v-bind="propsToNative(inputProps)" + size="1" + class="form-post-subject" + > + </template> </EmojiInput> <EmojiInput ref="emoji-input" @@ -148,29 +160,32 @@ @sticker-upload-failed="uploadFailed" @shown="handleEmojiInputShow" > - <textarea - ref="textarea" - v-model="newStatus.status" - :placeholder="placeholder || $t('post_status.default')" - rows="1" - cols="1" - :disabled="posting && !optimisticPosting" - class="form-post-body" - :class="{ 'scrollable-form': !!maxHeight }" - @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)" - @keydown.meta.enter="postStatus($event, newStatus)" - @keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)" - @input="resize" - @compositionupdate="resize" - @paste="paste" - /> - <p - v-if="hasStatusLengthLimit" - class="character-counter faint" - :class="{ error: isOverLengthLimit }" - > - {{ charactersLeft }} - </p> + <template #default="inputProps"> + <textarea + ref="textarea" + v-model="newStatus.status" + :placeholder="placeholder || $t('post_status.default')" + rows="1" + cols="1" + :disabled="posting && !optimisticPosting" + class="form-post-body" + :class="{ 'scrollable-form': !!maxHeight }" + v-bind="propsToNative(inputProps)" + @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)" + @keydown.meta.enter="postStatus($event, newStatus)" + @keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)" + @input="resize" + @compositionupdate="resize" + @paste="paste" + /> + <p + v-if="hasStatusLengthLimit" + class="character-counter faint" + :class="{ error: isOverLengthLimit }" + > + {{ charactersLeft }} + </p> + </template> </EmojiInput> <div v-if="!disableScopeSelector" @@ -193,6 +208,7 @@ id="post-content-type" v-model="newStatus.contentType" class="form-control" + :attrs="{ 'aria-label': $t('post_status.content_type_selection') }" > <option v-for="postFormat in postFormats" @@ -265,12 +281,10 @@ > {{ $t('post_status.post') }} </button> - <!-- touchstart is used to keep the OSK at the same position after a message send --> <button v-else :disabled="uploadingFiles || disableSubmit" class="btn button-default" - @touchstart.stop.prevent="postStatus($event, newStatus)" @click.stop.prevent="postStatus($event, newStatus)" > {{ $t('post_status.post') }} diff --git a/src/components/quick_filter_settings/quick_filter_settings.vue b/src/components/quick_filter_settings/quick_filter_settings.vue index f2aa61ee..b81215a1 100644 --- a/src/components/quick_filter_settings/quick_filter_settings.vue +++ b/src/components/quick_filter_settings/quick_filter_settings.vue @@ -6,36 +6,51 @@ :trigger-attrs="{ title: $t('timeline.quick_filter_settings') }" > <template #content> - <div class="dropdown-menu"> - <div v-if="loggedIn"> + <div + class="dropdown-menu" + role="menu" + > + <div + v-if="loggedIn" + role="group" + > <button v-if="!conversation" class="button-default dropdown-item" + :aria-checked="replyVisibilityAll" + role="menuitemradio" @click="replyVisibilityAll = true" > <span class="menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilityAll }" + :aria-hidden="true" />{{ $t('settings.reply_visibility_all') }} </button> <button v-if="!conversation" class="button-default dropdown-item" + :aria-checked="replyVisibilityFollowing" + role="menuitemradio" @click="replyVisibilityFollowing = true" > <span class="menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilityFollowing }" + :aria-hidden="true" />{{ $t('settings.reply_visibility_following_short') }} </button> <button v-if="!conversation" class="button-default dropdown-item" + :aria-checked="replyVisibilitySelf" + role="menuitemradio" @click="replyVisibilitySelf = true" > <span class="menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilitySelf }" + :aria-hidden="true" />{{ $t('settings.reply_visibility_self_short') }} </button> <div @@ -46,33 +61,43 @@ </div> <button class="button-default dropdown-item" + role="menuitemcheckbox" + :aria-checked="muteBotStatuses" @click="muteBotStatuses = !muteBotStatuses" > <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': muteBotStatuses }" + :aria-hidden="true" />{{ $t('settings.mute_bot_posts') }} </button> <button class="button-default dropdown-item" + role="menuitemcheckbox" + :aria-checked="hideMedia" @click="hideMedia = !hideMedia" > <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hideMedia }" + :aria-hidden="true" />{{ $t('settings.hide_media_previews') }} </button> <button class="button-default dropdown-item" + role="menuitemcheckbox" + :aria-checked="hideMutedPosts" @click="hideMutedPosts = !hideMutedPosts" > <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hideMutedPosts }" + :aria-hidden="true" />{{ $t('settings.hide_all_muted_posts') }} </button> <button class="button-default dropdown-item dropdown-item-icon" + role="menuitem" @click="openTab('filtering')" > <FAIcon icon="font" />{{ $t('settings.word_filter_and_more') }} diff --git a/src/components/quick_view_settings/quick_view_settings.vue b/src/components/quick_view_settings/quick_view_settings.vue index 4bd81c5b..9f5cdabc 100644 --- a/src/components/quick_view_settings/quick_view_settings.vue +++ b/src/components/quick_view_settings/quick_view_settings.vue @@ -6,60 +6,87 @@ :trigger-attrs="{ title: $t('timeline.quick_view_settings') }" > <template #content> - <div class="dropdown-menu"> - <button - class="button-default dropdown-item" - @click="conversationDisplay = 'tree'" - > - <span - class="menu-checkbox -radio" - :class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }" - /><FAIcon icon="folder-tree" /> {{ $t('settings.conversation_display_tree_quick') }} - </button> - <button - class="button-default dropdown-item" - @click="conversationDisplay = 'linear'" - > - <span - class="menu-checkbox -radio" - :class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }" - /><FAIcon icon="list" /> {{ $t('settings.conversation_display_linear_quick') }} - </button> + <div + class="dropdown-menu" + role="menu" + > + <div role="group"> + <button + class="button-default dropdown-item" + :aria-checked="conversationDisplay === 'tree'" + role="menuitemradio" + @click="conversationDisplay = 'tree'" + > + <span + class="menu-checkbox -radio" + :aria-hidden="true" + :class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }" + /><FAIcon + icon="folder-tree" + :aria-hidden="true" + /> {{ $t('settings.conversation_display_tree_quick') }} + </button> + <button + class="button-default dropdown-item" + :aria-checked="conversationDisplay === 'linear'" + role="menuitemradio" + @click="conversationDisplay = 'linear'" + > + <span + class="menu-checkbox -radio" + :class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }" + :aria-hidden="true" + /><FAIcon + icon="list" + :aria-hidden="true" + /> {{ $t('settings.conversation_display_linear_quick') }} + </button> + </div> <div role="separator" class="dropdown-divider" /> <button class="button-default dropdown-item" + role="menuitemcheckbox" + :aria-checked="showUserAvatars" @click="showUserAvatars = !showUserAvatars" > <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': showUserAvatars }" + :aria-hidden="true" />{{ $t('settings.mention_link_show_avatar_quick') }} </button> <button v-if="!conversation" class="button-default dropdown-item" + role="menuitemcheckbox" + :aria-checked="autoUpdate" @click="autoUpdate = !autoUpdate" > <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': autoUpdate }" + :aria-hidden="true" />{{ $t('settings.auto_update') }} </button> <button v-if="!conversation" class="button-default dropdown-item" + role="menuitemcheckbox" + :aria-checked="collapseWithSubjects" @click="collapseWithSubjects = !collapseWithSubjects" > <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': collapseWithSubjects }" + :aria-hidden="true" />{{ $t('settings.collapse_subject') }} </button> <button class="button-default dropdown-item dropdown-item-icon" + role="menuitem" @click="openTab('general')" > <FAIcon icon="wrench" />{{ $t('settings.more_settings') }} diff --git a/src/components/range_input/range_input.vue b/src/components/range_input/range_input.vue index 1e7e42d5..1e720105 100644 --- a/src/components/range_input/range_input.vue +++ b/src/components/range_input/range_input.vue @@ -4,6 +4,7 @@ :class="{ disabled: !present || disabled }" > <label + :id="name + '-label'" :for="name" class="label" > @@ -12,7 +13,8 @@ <input v-if="typeof fallback !== 'undefined'" :id="name + '-o'" - class="opt" + :aria-labelledby="name + '-label'" + class="opt visible-for-screenreader-only" type="checkbox" :checked="present" @change="$emit('update:modelValue', !present ? fallback : undefined)" @@ -21,6 +23,7 @@ v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'" + :aria-hidden="true" /> <input :id="name" @@ -34,9 +37,10 @@ @input="$emit('update:modelValue', $event.target.value)" > <input - :id="name" + :id="name + '-numeric'" class="input-number" type="number" + :aria-labelledby="name + '-label'" :value="modelValue || fallback" :disabled="!present || disabled" :max="hardMax" diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js index 47a48623..8eed4b60 100644 --- a/src/components/react_button/react_button.js +++ b/src/components/react_button/react_button.js @@ -1,9 +1,8 @@ import Popover from '../popover/popover.vue' -import { ensureFinalFallback } from '../../i18n/languages.js' +import EmojiPicker from '../emoji_picker/emoji_picker.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons' import { faSmileBeam } from '@fortawesome/free-regular-svg-icons' -import { trim } from 'lodash' library.add( faPlus, @@ -20,105 +19,34 @@ const ReactButton = { } }, components: { - Popover + Popover, + EmojiPicker }, methods: { - addReaction (event, emoji, close) { + addReaction (event) { + const emoji = event.insertion const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji) if (existingReaction && existingReaction.me) { this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) } else { this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) } - close() + }, + show () { + if (!this.expanded) { + this.$refs.picker.showPicker() + } }, onShow () { this.expanded = true - this.focusInput() }, onClose () { this.expanded = false - }, - focusInput () { - this.$nextTick(() => { - const input = document.querySelector('.reaction-picker-filter > input') - if (input) input.focus() - }) - }, - // Vaguely adjusted copypaste from emoji_input and emoji_picker! - maybeLocalizedEmojiNamesAndKeywords (emoji) { - const names = [emoji.displayText] - const keywords = [] - - if (emoji.displayTextI18n) { - names.push(this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)) - } - - if (emoji.annotations) { - this.languages.forEach(lang => { - names.push(emoji.annotations[lang]?.name) - - keywords.push(...(emoji.annotations[lang]?.keywords || [])) - }) - } - - return { - names: names.filter(k => k), - keywords: keywords.filter(k => k) - } - }, - maybeLocalizedEmojiName (emoji) { - if (!emoji.annotations) { - return emoji.displayText - } - - if (emoji.displayTextI18n) { - return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args) - } - - for (const lang of this.languages) { - if (emoji.annotations[lang]?.name) { - return emoji.annotations[lang].name - } - } - - return emoji.displayText } }, computed: { - commonEmojis () { - const hardcodedSet = new Set(['👍', '😠', '👀', '😂', '🔥']) - return this.$store.getters.standardEmojiList.filter(emoji => hardcodedSet.has(emoji.replacement)) - }, - languages () { - return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage) - }, - emojis () { - if (this.filterWord !== '') { - const keywordLowercase = trim(this.filterWord.toLowerCase()) - - const orderedEmojiList = [] - for (const emoji of this.$store.getters.standardEmojiList) { - const indices = this.maybeLocalizedEmojiNamesAndKeywords(emoji) - .keywords - .map(k => k.toLowerCase().indexOf(keywordLowercase)) - .filter(k => k > -1) - - const indexOfKeyword = indices.length ? Math.min(...indices) : -1 - - if (indexOfKeyword > -1) { - if (!Array.isArray(orderedEmojiList[indexOfKeyword])) { - orderedEmojiList[indexOfKeyword] = [] - } - orderedEmojiList[indexOfKeyword].push(emoji) - } - } - return orderedEmojiList.flat() - } - return this.$store.getters.standardEmojiList || [] - }, - mergedConfig () { - return this.$store.getters.mergedConfig + hideCustomEmoji () { + return !this.$store.state.instance.pleromaChatMessagesAvailable } } } diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue index a813b6fd..947536a1 100644 --- a/src/components/react_button/react_button.vue +++ b/src/components/react_button/react_button.vue @@ -1,73 +1,39 @@ <template> - <Popover - trigger="click" - class="ReactButton" - placement="top" - :offset="{ y: 5 }" - :bound-to="{ x: 'container' }" - remove-padding - popover-class="ReactButton popover-default" - @show="onShow" - @close="onClose" - > - <template #content="{close}"> - <div class="reaction-picker-filter"> - <input - v-model="filterWord" - size="1" - :placeholder="$t('emoji.search_emoji')" - @input="$event.target.composing = false" - > - </div> - <div class="reaction-picker"> - <span - v-for="emoji in commonEmojis" - :key="emoji.replacement" - class="emoji-button" - :title="maybeLocalizedEmojiName(emoji)" - @click="addReaction($event, emoji.replacement, close)" - > - {{ emoji.replacement }} - </span> - <div class="reaction-picker-divider" /> - <span - v-for="(emoji, key) in emojis" - :key="key" - class="emoji-button" - :title="maybeLocalizedEmojiName(emoji)" - @click="addReaction($event, emoji.replacement, close)" - > - {{ emoji.replacement }} - </span> - <div class="reaction-bottom-fader" /> - </div> - </template> - <template #trigger> - <span - class="button-unstyled popover-trigger" - :title="$t('tool_tip.add_reaction')" - > - <FALayers> - <FAIcon - class="fa-scale-110 fa-old-padding" - :icon="['far', 'smile-beam']" - /> - <FAIcon - v-show="!expanded" - class="focus-marker" - transform="shrink-6 up-9 right-17" - icon="plus" - /> - <FAIcon - v-show="expanded" - class="focus-marker" - transform="shrink-6 up-9 right-17" - icon="times" - /> - </FALayers> - </span> - </template> - </Popover> + <span class="ReactButton"> + <EmojiPicker + ref="picker" + :enable-sticker-picker="enableStickerPicker" + :hide-custom-emoji="hideCustomEmoji" + class="emoji-picker-panel" + @emoji="addReaction" + @show="onShow" + @close="onClose" + /> + <span + class="button-unstyled popover-trigger" + :title="$t('tool_tip.add_reaction')" + @click.stop.prevent="show" + > + <FALayers> + <FAIcon + class="fa-scale-110 fa-old-padding" + :icon="['far', 'smile-beam']" + /> + <FAIcon + v-show="!expanded" + class="focus-marker" + transform="shrink-6 up-9 right-17" + icon="plus" + /> + <FAIcon + v-show="expanded" + class="focus-marker" + transform="shrink-6 up-9 right-17" + icon="times" + /> + </FALayers> + </span> + </span> </template> <script src="./react_button.js"></script> @@ -135,11 +101,6 @@ color: $fallback--text; color: var(--text, $fallback--text); } - } - - .popover-trigger-button { - /* override of popover internal stuff */ - width: auto; @include unfocused-style { .focus-marker { diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js index 22ca6ad6..b88bdeec 100644 --- a/src/components/registration/registration.js +++ b/src/components/registration/registration.js @@ -16,7 +16,7 @@ const registration = { confirm: '', birthday: '', reason: '', - language: '' + language: [''] }, captcha: {} }), @@ -100,7 +100,7 @@ const registration = { this.user.captcha_token = this.captcha.token this.user.captcha_answer_data = this.captcha.answer_data if (this.user.language) { - this.user.language = localeService.internalToBackendLocale(this.user.language) + this.user.language = localeService.internalToBackendLocaleMulti(this.user.language.filter(k => k)) } this.v$.$touch() diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue index 5701b05e..7438a5f4 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -210,6 +210,7 @@ :prompt-text="$t('registration.email_language')" :language="v$.user.language.$model" :set-language="val => v$.user.language.$model = val" + @click.stop.prevent /> </div> diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue index 6e3964b7..60a40a08 100644 --- a/src/components/reply_button/reply_button.vue +++ b/src/components/reply_button/reply_button.vue @@ -32,12 +32,20 @@ target="_blank" role="button" :href="remoteInteractionLink" + :title="$t('tool_tip.reply')" > - <FAIcon - icon="reply" - class="fa-scale-110 fa-old-padding" - :title="$t('tool_tip.reply')" - /> + <FALayers class="fa-old-padding-layer"> + <FAIcon + class="fa-scale-110" + icon="reply" + /> + <FAIcon + v-if="!replying" + class="focus-marker" + transform="shrink-6 up-8 right-16" + icon="plus" + /> + </FALayers> </a> <span v-if="status.replies_count > 0" diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue index 134fcb36..e1b6b153 100644 --- a/src/components/retweet_button/retweet_button.vue +++ b/src/components/retweet_button/retweet_button.vue @@ -45,13 +45,20 @@ class="button-unstyled interactive" target="_blank" role="button" + :title="$t('tool_tip.repeat')" :href="remoteInteractionLink" > - <FAIcon - class="fa-scale-110 fa-old-padding" - icon="retweet" - :title="$t('tool_tip.repeat')" - /> + <FALayers class="fa-old-padding-layer"> + <FAIcon + class="fa-scale-110" + icon="retweet" + /> + <FAIcon + class="focus-marker" + transform="shrink-6 up-9 right-12" + icon="plus" + /> + </FALayers> </a> <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0" diff --git a/src/components/screen_reader_notice/screen_reader_notice.js b/src/components/screen_reader_notice/screen_reader_notice.js new file mode 100644 index 00000000..3b8eaf37 --- /dev/null +++ b/src/components/screen_reader_notice/screen_reader_notice.js @@ -0,0 +1,21 @@ +const ScreenReaderNotice = { + props: { + ariaLive: { + type: String, + defualt: 'assertive' + } + }, + data () { + return { + currentText: '' + } + }, + methods: { + announce (text) { + this.currentText = text + setTimeout(() => { this.currentText = '' }, 1000) + } + } +} + +export default ScreenReaderNotice diff --git a/src/components/screen_reader_notice/screen_reader_notice.vue b/src/components/screen_reader_notice/screen_reader_notice.vue new file mode 100644 index 00000000..8384ae6b --- /dev/null +++ b/src/components/screen_reader_notice/screen_reader_notice.vue @@ -0,0 +1,10 @@ +<template> + <div + class="visible-for-screenreader-only" + :aria-live="ariaLive" + > + {{ currentText }} + </div> +</template> + +<script src="./screen_reader_notice.js"></script> diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue index 3969d8de..9da2b272 100644 --- a/src/components/search_bar/search_bar.vue +++ b/src/components/search_bar/search_bar.vue @@ -8,6 +8,7 @@ class="button-unstyled nav-icon" :title="$t('nav.search')" type="button" + :aria-expanded="!hidden" @click.prevent.stop="toggleHidden" > <FAIcon @@ -29,6 +30,7 @@ <button class="button-default search-button" type="submit" + :title="$t('nav.search')" @click="find(searchTerm)" > <FAIcon @@ -39,6 +41,8 @@ <button class="button-unstyled cancel-search" type="button" + :title="$t('nav.search_close')" + :aria-expanded="!hidden" @click.prevent.stop="toggleHidden" > <FAIcon diff --git a/src/components/select/select.js b/src/components/select/select.js index ec571a14..34d64fd2 100644 --- a/src/components/select/select.js +++ b/src/components/select/select.js @@ -13,6 +13,7 @@ export default { 'modelValue', 'disabled', 'unstyled', - 'kind' + 'kind', + 'attrs' ] } diff --git a/src/components/select/select.vue b/src/components/select/select.vue index 0ce4ea82..1797afc8 100644 --- a/src/components/select/select.vue +++ b/src/components/select/select.vue @@ -6,6 +6,7 @@ <select :disabled="disabled" :value="modelValue" + v-bind="attrs" @change="$emit('update:modelValue', $event.target.value)" > <slot /> diff --git a/src/components/settings_modal/helpers/float_setting.vue b/src/components/settings_modal/helpers/float_setting.vue new file mode 100644 index 00000000..15edb3c3 --- /dev/null +++ b/src/components/settings_modal/helpers/float_setting.vue @@ -0,0 +1,16 @@ +<template> + <NumberSetting + v-bind="$attrs" + > + <slot /> + </NumberSetting> +</template> + +<script> +import NumberSetting from './number_setting.vue' +export default { + components: { + NumberSetting + } +} +</script> diff --git a/src/components/settings_modal/helpers/integer_setting.vue b/src/components/settings_modal/helpers/integer_setting.vue index 695e2673..43fa7e1a 100644 --- a/src/components/settings_modal/helpers/integer_setting.vue +++ b/src/components/settings_modal/helpers/integer_setting.vue @@ -1,27 +1,17 @@ <template> - <span - v-if="matchesExpertLevel" - class="IntegerSetting" + <NumberSetting + v-bind="$attrs" + truncate="1" > - <label :for="path"> - <slot /> - </label> - <input - :id="path" - class="number-input" - type="number" - step="1" - :disabled="disabled" - :min="min || 0" - :value="state" - @change="update" - > - {{ ' ' }} - <ModifiedIndicator - :changed="isChanged" - :onclick="reset" - /> - </span> + <slot /> + </NumberSetting> </template> -<script src="./integer_setting.js"></script> +<script> +import NumberSetting from './number_setting.vue' +export default { + components: { + NumberSetting + } +} +</script> diff --git a/src/components/settings_modal/helpers/modified_indicator.vue b/src/components/settings_modal/helpers/modified_indicator.vue index 8311533a..45db3fc2 100644 --- a/src/components/settings_modal/helpers/modified_indicator.vue +++ b/src/components/settings_modal/helpers/modified_indicator.vue @@ -5,12 +5,12 @@ > <Popover trigger="hover" + :trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }" > <template #trigger> <FAIcon icon="wrench" - :aria-label="$t('settings.setting_changed')" /> </template> <template #content> diff --git a/src/components/settings_modal/helpers/integer_setting.js b/src/components/settings_modal/helpers/number_setting.js index e64d0cee..73c39948 100644 --- a/src/components/settings_modal/helpers/integer_setting.js +++ b/src/components/settings_modal/helpers/number_setting.js @@ -8,6 +8,8 @@ export default { path: String, disabled: Boolean, min: Number, + step: Number, + truncate: Number, expert: [Number, String] }, computed: { @@ -15,8 +17,11 @@ export default { const [firstSegment, ...rest] = this.path.split('.') return [firstSegment + 'DefaultValue', ...rest].join('.') }, + parent () { + return this.$parent.$parent + }, state () { - const value = get(this.$parent, this.path) + const value = get(this.parent, this.path) if (value === undefined) { return this.defaultState } else { @@ -24,21 +29,28 @@ export default { } }, defaultState () { - return get(this.$parent, this.pathDefault) + return get(this.parent, this.pathDefault) }, isChanged () { return this.state !== this.defaultState }, matchesExpertLevel () { - return (this.expert || 0) <= this.$parent.expertLevel + return (this.expert || 0) <= this.parent.expertLevel } }, methods: { + truncateValue (value) { + if (!this.truncate) { + return value + } + + return Math.trunc(value / this.truncate) * this.truncate + }, update (e) { - set(this.$parent, this.path, parseInt(e.target.value)) + set(this.parent, this.path, this.truncateValue(parseFloat(e.target.value))) }, reset () { - set(this.$parent, this.path, this.defaultState) + set(this.parent, this.path, this.defaultState) } } } diff --git a/src/components/settings_modal/helpers/number_setting.vue b/src/components/settings_modal/helpers/number_setting.vue new file mode 100644 index 00000000..3eab5178 --- /dev/null +++ b/src/components/settings_modal/helpers/number_setting.vue @@ -0,0 +1,27 @@ +<template> + <span + v-if="matchesExpertLevel" + class="NumberSetting" + > + <label :for="path"> + <slot /> + </label> + <input + :id="path" + class="number-input" + type="number" + :step="step || 1" + :disabled="disabled" + :min="min || 0" + :value="state" + @change="update" + > + {{ ' ' }} + <ModifiedIndicator + :changed="isChanged" + :onclick="reset" + /> + </span> +</template> + +<script src="./number_setting.js"></script> diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index ea24d6ad..be97710f 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -2,6 +2,7 @@ import BooleanSetting from '../helpers/boolean_setting.vue' import ChoiceSetting from '../helpers/choice_setting.vue' import ScopeSelector from 'src/components/scope_selector/scope_selector.vue' import IntegerSetting from '../helpers/integer_setting.vue' +import FloatSetting from '../helpers/float_setting.vue' import SizeSetting, { defaultHorizontalUnits } from '../helpers/size_setting.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' @@ -62,6 +63,7 @@ const GeneralTab = { BooleanSetting, ChoiceSetting, IntegerSetting, + FloatSetting, SizeSetting, InterfaceLanguageSwitcher, ScopeSelector, diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 703e94a0..21e2d855 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -271,6 +271,15 @@ {{ $t('settings.no_rich_text_description') }} </BooleanSetting> </li> + <li> + <FloatSetting + v-if="user" + path="emojiReactionsScale" + expert="1" + > + {{ $t('settings.emoji_reactions_scale') }} + </FloatSetting> + </li> <h3>{{ $t('settings.attachments') }}</h3> <li> <BooleanSetting @@ -501,6 +510,14 @@ {{ $t('settings.pad_emoji') }} </BooleanSetting> </li> + <li> + <BooleanSetting + path="autocompleteSelect" + expert="1" + > + {{ $t('settings.autocomplete_select_first') }} + </BooleanSetting> + </li> </ul> </div> </div> diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index 72d27083..eeacad48 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -12,6 +12,7 @@ import InterfaceLanguageSwitcher from 'src/components/interface_language_switche import BooleanSetting from '../helpers/boolean_setting.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' import localeService from 'src/services/locale/locale.service.js' +import { propsToNative } from 'src/services/attributes_helper/attributes_helper.service.js' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -261,6 +262,9 @@ const ProfileTab = { messageArgs: [error.message], level: 'error' }) + }, + propsToNative (props) { + return propsToNative(props) } } } diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue index faa05bcc..6a5b478a 100644 --- a/src/components/settings_modal/tabs/profile_tab.vue +++ b/src/components/settings_modal/tabs/profile_tab.vue @@ -8,11 +8,14 @@ enable-emoji-picker :suggest="emojiSuggestor" > - <input - id="username" - v-model="newName" - class="name-changer" - > + <template #default="inputProps"> + <input + id="username" + v-model="newName" + class="name-changer" + v-bind="propsToNative(inputProps)" + > + </template> </EmojiInput> <p>{{ $t('settings.bio') }}</p> <EmojiInput @@ -20,10 +23,13 @@ enable-emoji-picker :suggest="emojiUserSuggestor" > - <textarea - v-model="newBio" - class="bio resize-height" - /> + <template #default="inputProps"> + <textarea + v-model="newBio" + class="bio resize-height" + v-bind="propsToNative(inputProps)" + /> + </template> </EmojiInput> <p v-if="role === 'admin' || role === 'moderator'"> <Checkbox v-model="showRole"> @@ -60,10 +66,13 @@ hide-emoji-button :suggest="userSuggestor" > - <input - v-model="newFields[i].name" - :placeholder="$t('settings.profile_fields.name')" - > + <template #default="inputProps"> + <input + v-model="newFields[i].name" + :placeholder="$t('settings.profile_fields.name')" + v-bind="propsToNative(inputProps)" + > + </template> </EmojiInput> <EmojiInput v-model="newFields[i].value" @@ -71,10 +80,13 @@ hide-emoji-button :suggest="userSuggestor" > - <input - v-model="newFields[i].value" - :placeholder="$t('settings.profile_fields.value')" - > + <template #default="inputProps"> + <input + v-model="newFields[i].value" + :placeholder="$t('settings.profile_fields.value')" + v-bind="propsToNative(inputProps)" + > + </template> </EmojiInput> <button class="delete-field button-unstyled -hover-highlight" diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue index 7546535d..1f3c26aa 100644 --- a/src/components/shadow_control/shadow_control.vue +++ b/src/components/shadow_control/shadow_control.vue @@ -129,12 +129,13 @@ v-model="selected.inset" :disabled="!present" name="inset" - class="input-inset" + class="input-inset visible-for-screenreader-only" type="checkbox" > <label class="checkbox-label" for="inset" + :aria-hidden="true" /> </div> <div diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx index c8d390bc..a7ef8560 100644 --- a/src/components/tab_switcher/tab_switcher.jsx +++ b/src/components/tab_switcher/tab_switcher.jsx @@ -117,6 +117,7 @@ export default { onClick={this.clickTab(index)} class={classesTab.join(' ')} type="button" + role="tab" > <img src={props.image} title={props['image-tooltip']}/> {props.label ? '' : props.label} @@ -131,6 +132,7 @@ export default { onClick={this.clickTab(index)} class={classesTab.join(' ')} type="button" + role="tab" > {!props.icon ? '' : (<FAIcon class="tab-icon" size="2x" fixed-width icon={props.icon}/>)} <span class="text"> @@ -167,11 +169,15 @@ export default { return ( <div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}> - <div class="tabs"> + <div + class="tabs" + role="tablist" + > {tabs} </div> <div ref="contents" + role="tabpanel" class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')} v-body-scroll-lock={this.bodyScrollLock} > |
