fix: make autosuggestion accessible (#1183)

* fix: make autosuggestion accessible

fixes #129

* remove tabindexes, fix aria-hidden
This commit is contained in:
Nolan Lawson 2019-05-05 22:08:54 -07:00 committed by GitHub
parent 78715bc098
commit 8d0db2c97c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 89 additions and 20 deletions

View file

@ -1,11 +1,16 @@
<div class="compose-autosuggest {shown ? 'shown' : ''} {realm === 'dialog' ? 'is-dialog' : ''}" <div class="compose-autosuggest {shown ? 'shown' : ''} {realm === 'dialog' ? 'is-dialog' : ''}"
aria-hidden="true" > aria-hidden={!shown}
>
<ComposeAutosuggestionList <ComposeAutosuggestionList
items={autosuggestSearchResults} items={autosuggestSearchResults}
on:click="onClick(event)" on:click="onClick(event)"
type={autosuggestType} type={autosuggestType}
selected={autosuggestSelected} selected={autosuggestSelected}
{realm}
/> />
<div class="sr-only" aria-live="assertive">
{assertiveAriaText}
</div>
</div> </div>
<style> <style>
.compose-autosuggest { .compose-autosuggest {
@ -44,6 +49,7 @@
import { selectAutosuggestItem } from '../../_actions/autosuggest' import { selectAutosuggestItem } from '../../_actions/autosuggest'
import { observe } from 'svelte-extras' import { observe } from 'svelte-extras'
import { once } from '../../_utils/once' import { once } from '../../_utils/once'
import { createAutosuggestAccessibleLabel } from '../../_utils/createAutosuggestAccessibleLabel'
export default { export default {
oncreate () { oncreate () {
@ -94,7 +100,19 @@
/* eslint-enable camelcase */ /* eslint-enable camelcase */
shouldBeShown: ({ realm, $autosuggestShown, composeFocused }) => ( shouldBeShown: ({ realm, $autosuggestShown, composeFocused }) => (
!!($autosuggestShown && composeFocused) !!($autosuggestShown && composeFocused)
) ),
// text that is read to screen readers. based on https://haltersweb.github.io/Accessibility/autocomplete.html
assertiveAriaText: ({ shouldBeShown,
autosuggestSearchResults,
autosuggestSelected,
autosuggestType,
$omitEmojiInDisplayNames }) => {
if (!shouldBeShown || !autosuggestSearchResults || !autosuggestSearchResults.length) {
return ''
}
return createAutosuggestAccessibleLabel(autosuggestType, $omitEmojiInDisplayNames,
autosuggestSelected, autosuggestSearchResults)
}
}, },
data: () => ({ data: () => ({
shown: false shown: false

View file

@ -1,10 +1,17 @@
<ul class="compose-autosuggest-list"> <!-- accessible autocomplete, based on https://haltersweb.github.io/Accessibility/autocomplete.html -->
<ul id="compose-autosuggest-list-{realm}"
class="compose-autosuggest-list"
role="listbox"
>
{#each items as item, i (item.shortcode ? `emoji-${item.shortcode}` : `account-${item.id}`)} {#each items as item, i (item.shortcode ? `emoji-${item.shortcode}` : `account-${item.id}`)}
<li class="compose-autosuggest-list-item"> <li id="{i === selected ? `compose-autosuggest-active-item-${realm}` : ''}"
<button class="compose-autosuggest-list-button {i === selected ? 'selected' : ''}" class="compose-autosuggest-list-item {i === selected ? 'selected' : ''}"
tabindex="0" role="option"
on:click="onClick(event, item)"> aria-selected="{i === selected}"
<div class="compose-autosuggest-list-grid"> aria-label="{ariaLabels[i]}"
on:click="onClick(event, item)"
>
<div class="compose-autosuggest-list-grid" aria-hidden="true">
{#if type === 'account'} {#if type === 'account'}
<div class="compose-autosuggest-list-item-avatar"> <div class="compose-autosuggest-list-item-avatar">
<Avatar <Avatar
@ -28,7 +35,6 @@
</span> </span>
{/if} {/if}
</div> </div>
</button>
</li> </li>
{/each} {/each}
</ul> </ul>
@ -43,17 +49,15 @@
.compose-autosuggest-list-item { .compose-autosuggest-list-item {
border-bottom: 1px solid var(--compose-autosuggest-outline); border-bottom: 1px solid var(--compose-autosuggest-outline);
display: flex; display: flex;
padding: 10px;
background: var(--settings-list-item-bg);
margin: 0;
flex: 1;
cursor: pointer;
} }
.compose-autosuggest-list-item:last-child { .compose-autosuggest-list-item:last-child {
border-bottom: none; border-bottom: none;
} }
.compose-autosuggest-list-button {
padding: 10px;
background: var(--settings-list-item-bg);
border: none;
margin: 0;
flex: 1;
}
.compose-autosuggest-list-grid { .compose-autosuggest-list-grid {
display: grid; display: grid;
width: 100%; width: 100%;
@ -90,10 +94,10 @@
text-overflow: ellipsis; text-overflow: ellipsis;
text-align: left; text-align: left;
} }
.compose-autosuggest-list-button:hover, .compose-autosuggest-list-button.selected { .compose-autosuggest-list-item:hover, .compose-autosuggest-list-item.selected {
background: var(--compose-autosuggest-item-hover); background: var(--compose-autosuggest-item-hover);
} }
.compose-autosuggest-list-button:active { .compose-autosuggest-list-item:active {
background: var(--compose-autosuggest-item-active); background: var(--compose-autosuggest-item-active);
} }
</style> </style>
@ -101,9 +105,17 @@
import Avatar from '../Avatar.html' import Avatar from '../Avatar.html'
import { store } from '../../_store/store' import { store } from '../../_store/store'
import AccountDisplayName from '../profile/AccountDisplayName.html' import AccountDisplayName from '../profile/AccountDisplayName.html'
import { createAutosuggestAccessibleLabel } from '../../_utils/createAutosuggestAccessibleLabel'
export default { export default {
store: () => store, store: () => store,
computed: {
ariaLabels: ({ items, type, $omitEmojiInDisplayNames }) => {
return items.map((item, i) => {
return createAutosuggestAccessibleLabel(type, $omitEmojiInDisplayNames, i, items)
})
}
},
methods: { methods: {
onClick (event, item) { onClick (event, item) {
event.preventDefault() event.preventDefault()

View file

@ -2,6 +2,11 @@
id="the-compose-box-input-{realm}" id="the-compose-box-input-{realm}"
class="compose-box-input compose-box-input-realm-{realm}" class="compose-box-input compose-box-input-realm-{realm}"
placeholder="What's on your mind?" placeholder="What's on your mind?"
aria-describedby="compose-box-input-description-{realm}"
aria-owns="compose-autosuggest-list-{realm}"
aria-expanded={autosuggestShownForThisInput}
aria-autocomplete="both"
aria-activedescendant="{autosuggestShownForThisInput ? `compose-autosuggest-active-item-${realm}` : ''}"
ref:textarea ref:textarea
bind:value=rawText bind:value=rawText
on:blur="onBlur()" on:blur="onBlur()"
@ -12,6 +17,9 @@
<label for="the-compose-box-input-{realm}" class="sr-only"> <label for="the-compose-box-input-{realm}" class="sr-only">
What's on your mind? What's on your mind?
</label> </label>
<span id="compose-box-input-description-{realm}" class="sr-only">
When autocomplete results are available, press up or down arrows and enter to select.
</span>
<style> <style>
.compose-box-input { .compose-box-input {
grid-area: input; grid-area: input;
@ -59,6 +67,7 @@
clickSelectedAutosuggestionEmoji clickSelectedAutosuggestionEmoji
} from '../../_actions/autosuggest' } from '../../_actions/autosuggest'
import { observe } from 'svelte-extras' import { observe } from 'svelte-extras'
import { get } from '../../_utils/lodash-lite'
export default { export default {
oncreate () { oncreate () {
@ -214,6 +223,16 @@
data: () => ({ data: () => ({
rawText: '' rawText: ''
}), }),
computed: {
/* eslint-disable camelcase */
composeFocused: ({ $autosuggestData_composeFocused, $currentInstance, realm }) => (
get($autosuggestData_composeFocused, [$currentInstance, realm], false)
),
/* eslint-enable camelcase */
autosuggestShownForThisInput: ({ realm, $autosuggestShown, composeFocused }) => (
!!($autosuggestShown && composeFocused)
)
},
events: { events: {
selectionChange selectionChange
} }

View file

@ -1,6 +1,6 @@
import { get } from '../../_utils/lodash-lite' import { get } from '../../_utils/lodash-lite'
const MIN_PREFIX_LENGTH = 1 const MIN_PREFIX_LENGTH = 2
const ACCOUNT_SEARCH_REGEX = new RegExp(`(?:\\s|^)(@\\S{${MIN_PREFIX_LENGTH},})$`) const ACCOUNT_SEARCH_REGEX = new RegExp(`(?:\\s|^)(@\\S{${MIN_PREFIX_LENGTH},})$`)
const EMOJI_SEARCH_REGEX = new RegExp(`(?:\\s|^)(:[^:]{${MIN_PREFIX_LENGTH},})$`) const EMOJI_SEARCH_REGEX = new RegExp(`(?:\\s|^)(:[^:]{${MIN_PREFIX_LENGTH},})$`)

View file

@ -0,0 +1,20 @@
import { removeEmoji } from './removeEmoji'
export function createAutosuggestAccessibleLabel (
autosuggestType, $omitEmojiInDisplayNames,
selectedIndex, searchResults) {
let selected = searchResults[selectedIndex]
let label
if (autosuggestType === 'emoji') {
label = `${selected.shortcode}`
} else { // account
let displayName = selected.display_name || selected.username
let emojis = selected.emojis || []
displayName = $omitEmojiInDisplayNames
? removeEmoji(displayName, emojis) || displayName
: displayName
label = `${displayName} @${selected.acct}`
}
return `${label} (${selectedIndex + 1} of ${searchResults.length}). ` +
`Press up and down arrows to review and enter to select.`
}

View file

@ -213,7 +213,7 @@ export function getNthPostPrivacyButton (n) {
} }
export function getNthAutosuggestionResult (n) { export function getNthAutosuggestionResult (n) {
return $(`.compose-autosuggest-list-item:nth-child(${n}) button`) return $(`.compose-autosuggest-list-item:nth-child(${n})`)
} }
export function getSearchResultByHref (href) { export function getSearchResultByHref (href) {