fix: make autosuggestion accessible (#1183)
* fix: make autosuggestion accessible fixes #129 * remove tabindexes, fix aria-hidden
This commit is contained in:
parent
78715bc098
commit
8d0db2c97c
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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},})$`)
|
||||||
|
|
||||||
|
|
20
src/routes/_utils/createAutosuggestAccessibleLabel.js
Normal file
20
src/routes/_utils/createAutosuggestAccessibleLabel.js
Normal 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.`
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue