logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: 6235af4592c52a657415ffae772bd83ec106bc13
parent 4db7f07421a6dc49f07dee9140a024ef5195a934
Author: tusooa <tusooa@kazv.moe>
Date:   Sat, 21 Jan 2023 01:07:07 -0500

Make screenreaders read out autocomplete results

Diffstat:

Msrc/components/emoji_input/emoji_input.js53+++++++++++++++++++++++++++++++++++------------------
Msrc/components/emoji_input/emoji_input.vue19+++++++++++++++++--
Msrc/components/post_status_form/post_status_form.vue53++++++++++++++++++++++++++++++-----------------------
Asrc/components/screen_reader_notice/screen_reader_notice.js21+++++++++++++++++++++
Asrc/components/screen_reader_notice/screen_reader_notice.vue21+++++++++++++++++++++
Msrc/i18n/en.json3++-
6 files changed, 126 insertions(+), 44 deletions(-)

diff --git 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,7 +127,8 @@ const EmojiInput = { components: { Popover, EmojiPicker, - UnicodeDomainIndicator + UnicodeDomainIndicator, + ScreenReaderNotice }, computed: { padEmoji () { @@ -203,6 +206,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 +287,10 @@ const EmojiInput = { ...rest, img: imageUrl || '' })) + this.$refs.screenReaderNotice.announce( + this.$tc('tool_tip.autocomplete_available', + this.suggestions.length, + { number: this.suggestions.length })) } }, methods: { @@ -374,27 +387,24 @@ 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 - } - e.preventDefault() - } else { - this.highlighted = 0 + + this.highlighted -= 1 + if (this.highlighted === -1) { + this.input.focus() + } else if (this.highlighted < -1) { + this.highlighted = len - 1 } + e.preventDefault() }, cycleForward (e) { const len = this.suggestions.length || 0 - if (len > 1) { - this.highlighted += 1 - if (this.highlighted >= len) { - this.highlighted = 0 - } - e.preventDefault() - } else { - this.highlighted = 0 + + this.highlighted += 1 + if (this.highlighted >= len) { + this.highlighted = -1 + this.input.focus() } + e.preventDefault() }, scrollIntoView () { const rootRef = this.$refs.picker.$el @@ -540,6 +550,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 @@ -4,7 +4,13 @@ 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" @@ -18,6 +24,10 @@ >x</span> <span>{{ postText }}</span> </div> + <screen-reader-notice + ref="screenReaderNotice" + aria-live="assertive" + /> <template v-if="enableEmojiPicker"> <button v-if="!hideEmojiButton" @@ -46,15 +56,20 @@ > <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="button" + 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/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue @@ -148,29 +148,36 @@ @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="inputProps" + :aria-owns="inputProps.ariaOwns" + :aria-autocomplete="inputProps.ariaAutocomplete" + :aria-activedescendant="inputProps.ariaActiveDescendant" + :aria-expanded="inputProps.ariaExpanded" + @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" diff --git a/src/components/screen_reader_notice/screen_reader_notice.js 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 @@ -0,0 +1,21 @@ +<template> + <div + class="screen-reader-text" + :aria-live="ariaLive" + > + {{ currentText }} + </div> +</template> + +<script src="./screen_reader_notice.js"></script> + +<style lang="scss"> +.screen-reader-text { + display: block; + width: 1px; + height: 1px; + margin: -1px; + overflow: hidden; + visibility: visible; +} +</style> diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -996,7 +996,8 @@ "reject_follow_request": "Reject follow request", "bookmark": "Bookmark", "toggle_expand": "Expand or collapse notification to show post in full", - "toggle_mute": "Expand or collapse notification to reveal muted content" + "toggle_mute": "Expand or collapse notification to reveal muted content", + "autocomplete_available": "{number} result is available | {number} results are available" }, "upload": { "error": {