Make screenreaders read out autocomplete results
This commit is contained in:
parent
4db7f07421
commit
6235af4592
6 changed files with 126 additions and 44 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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"
|
||||
|
|
21
src/components/screen_reader_notice/screen_reader_notice.js
Normal file
21
src/components/screen_reader_notice/screen_reader_notice.js
Normal 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
|
21
src/components/screen_reader_notice/screen_reader_notice.vue
Normal file
21
src/components/screen_reader_notice/screen_reader_notice.vue
Normal 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>
|
|
@ -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": {
|
||||
|
|
Loading…
Reference in a new issue