Make screenreaders read out autocomplete results

This commit is contained in:
tusooa 2023-01-21 01:07:07 -05:00
parent 4db7f07421
commit 6235af4592
No known key found for this signature in database
GPG Key ID: 7B467EDE43A08224
6 changed files with 126 additions and 44 deletions

View File

@ -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
if (this.highlighted === -1) {
this.input.focus()
} else if (this.highlighted < -1) {
this.highlighted = len - 1
}
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
this.input.focus()
}
e.preventDefault()
} else {
this.highlighted = 0
}
},
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)
}
}
}
}

View File

@ -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">

View File

@ -148,6 +148,7 @@
@sticker-upload-failed="uploadFailed"
@shown="handleEmojiInputShow"
>
<template #default="inputProps">
<textarea
ref="textarea"
v-model="newStatus.status"
@ -157,6 +158,11 @@
: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)"
@ -171,6 +177,7 @@
>
{{ charactersLeft }}
</p>
</template>
</EmojiInput>
<div
v-if="!disableScopeSelector"

View File

@ -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

View File

@ -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>

View File

@ -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": {