From f160a308b4b408862c6b919f3b6500b6c5d114ef Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 30 Sep 2019 14:04:39 +0100 Subject: [PATCH 1/2] Make Autocomplete more accessible to screen reader users Use ARIA to: + notate that the composer has an autocomplete + notate the open/closed state of the autocomplete + notate the name of the open autocomplete options + notate the ID of the highlighted autocomplete option + improve naming of emoji autocomplete options for screen readers Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/CommunityProvider.js | 12 +++++++++--- src/autocomplete/Components.js | 2 +- src/autocomplete/EmojiProvider.js | 12 ++++++++---- src/autocomplete/NotifProvider.js | 12 +++++++++--- src/autocomplete/RoomProvider.js | 12 +++++++++--- src/autocomplete/UserProvider.js | 2 +- src/components/views/rooms/Autocomplete.js | 18 +++++++++--------- .../views/rooms/BasicMessageComposer.js | 15 ++++++++++++--- src/i18n/strings/en_EN.json | 5 +++++ 9 files changed, 63 insertions(+), 27 deletions(-) diff --git a/src/autocomplete/CommunityProvider.js b/src/autocomplete/CommunityProvider.js index ffce1e71cfb..992df0f7731 100644 --- a/src/autocomplete/CommunityProvider.js +++ b/src/autocomplete/CommunityProvider.js @@ -105,8 +105,14 @@ export default class CommunityProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return
- { completions } -
; + return ( +
+ { completions } +
+ ); } } diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index b09f4e963e8..ca105bb211e 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -60,7 +60,7 @@ export class PillCompletion extends React.Component { ...restProps } = this.props; return ( -
+
{ initialComponent } { title } { subtitle } diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 8afcba6ab04..1e395930221 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -116,7 +116,9 @@ export default class EmojiProvider extends AutocompleteProvider { return { completion: unicode, component: ( - { unicode }} /> + { unicode } + } /> ), range, }; @@ -130,8 +132,10 @@ export default class EmojiProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return
- { completions } -
; + return ( +
+ { completions } +
+ ); } } diff --git a/src/autocomplete/NotifProvider.js b/src/autocomplete/NotifProvider.js index 60a3352f9b7..95cfb34616b 100644 --- a/src/autocomplete/NotifProvider.js +++ b/src/autocomplete/NotifProvider.js @@ -58,8 +58,14 @@ export default class NotifProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return
- { completions } -
; + return ( +
+ { completions } +
+ ); } } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index b94edf590ca..79986657b8b 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -109,8 +109,14 @@ export default class RoomProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return
- { completions } -
; + return ( +
+ { completions } +
+ ); } } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 62ae5d49707..edba6d4b035 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -164,7 +164,7 @@ export default class UserProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return
+ return
{ completions }
; } diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 243cfe2f753..ad5fa198a3c 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -20,18 +20,17 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import flatMap from 'lodash/flatMap'; -import isEqual from 'lodash/isEqual'; -import sdk from '../../../index'; import type {Completion} from '../../../autocomplete/Autocompleter'; import Promise from 'bluebird'; import { Room } from 'matrix-js-sdk'; -import {getCompletions} from '../../../autocomplete/Autocompleter'; import SettingsStore from "../../../settings/SettingsStore"; import Autocompleter from '../../../autocomplete/Autocompleter'; const COMPOSER_SELECTED = 0; +export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`; + export default class Autocomplete extends React.Component { constructor(props) { super(props); @@ -224,7 +223,7 @@ export default class Autocomplete extends React.Component { setSelection(selectionOffset: number) { this.setState({selectionOffset, hide: false}); if (this.props.onSelectionChange) { - this.props.onSelectionChange(this.state.completionList[selectionOffset - 1]); + this.props.onSelectionChange(this.state.completionList[selectionOffset - 1], selectionOffset - 1); } } @@ -250,9 +249,8 @@ export default class Autocomplete extends React.Component { let position = 1; const renderedCompletions = this.state.completions.map((completionResult, i) => { const completions = completionResult.completions.map((completion, i) => { - const className = classNames('mx_Autocomplete_Completion', { - 'selected': position === this.state.selectionOffset, - }); + const selected = position === this.state.selectionOffset; + const className = classNames('mx_Autocomplete_Completion', {selected}); const componentPosition = position; position++; @@ -261,10 +259,12 @@ export default class Autocomplete extends React.Component { }; return React.cloneElement(completion.component, { - key: i, - ref: `completion${position - 1}`, + "key": i, + "ref": `completion${componentPosition}`, + "id": generateCompletionDomId(componentPosition - 1), // 0 index the completion IDs className, onClick, + "aria-selected": selected, }); }); diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 895696e1186..4ec43d8af20 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -28,7 +28,7 @@ import { replaceRangeAndMoveCaret, } from '../../../editor/operations'; import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom'; -import Autocomplete from '../rooms/Autocomplete'; +import Autocomplete, {generateCompletionDomId} from '../rooms/Autocomplete'; import {autoCompleteCreator} from '../../../editor/parts'; import {parsePlainTextMessage} from '../../../editor/deserialize'; import {renderModel} from '../../../editor/render'; @@ -432,8 +432,9 @@ export default class BasicMessageEditor extends React.Component { this.props.model.autoComplete.onComponentConfirm(completion); } - _onAutoCompleteSelectionChange = (completion) => { + _onAutoCompleteSelectionChange = (completion, completionIndex) => { this.props.model.autoComplete.onComponentSelectionChange(completion); + this.setState({completionIndex}); } componentWillUnmount() { @@ -535,6 +536,8 @@ export default class BasicMessageEditor extends React.Component { quote: ctrlShortcutLabel(">"), }; + const {completionIndex} = this.state; + return (
{ autoComplete } this._formatBarRef = ref} onAction={this._onFormatAction} shortcuts={shortcuts} /> @@ -548,7 +551,13 @@ export default class BasicMessageEditor extends React.Component { onKeyDown={this._onKeyDown} ref={ref => this._editorRef = ref} aria-label={this.props.label} - >
+ role="textbox" + aria-multiline="true" + aria-autocomplete="both" + aria-haspopup="listbox" + aria-expanded={Boolean(this.state.autoComplete)} + aria-activedescendant={completionIndex >= 0 ? generateCompletionDomId(completionIndex) : undefined} + />
); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c23cd6d324d..9e8a0fec5c4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1726,11 +1726,16 @@ "Clear personal data": "Clear personal data", "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.", "Commands": "Commands", + "Community Autocomplete": "Community Autocomplete", "Results from DuckDuckGo": "Results from DuckDuckGo", "Emoji": "Emoji", + "Emoji Autocomplete": "Emoji Autocomplete", "Notify the whole room": "Notify the whole room", "Room Notification": "Room Notification", + "Notification Autocomplete": "Notification Autocomplete", + "Room Autocomplete": "Room Autocomplete", "Users": "Users", + "User Autocomplete": "User Autocomplete", "unknown device": "unknown device", "NOT verified": "NOT verified", "Blacklisted": "Blacklisted", From f299f7e092170dc054cb94cc6f0124ca29d928ec Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 30 Sep 2019 14:32:42 +0100 Subject: [PATCH 2/2] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/UserProvider.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index edba6d4b035..451ae0bb839 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -164,9 +164,11 @@ export default class UserProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return
- { completions } -
; + return ( +
+ { completions } +
+ ); } shouldForceComplete(): boolean {