Merge pull request #3497 from matrix-org/t3chguy/autocomplete_a11y
Make Autocomplete more accessible to screen reader users
This commit is contained in:
commit
2d6461d376
9 changed files with 67 additions and 29 deletions
|
@ -105,8 +105,14 @@ export default class CommunityProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
return (
|
||||||
{ completions }
|
<div
|
||||||
</div>;
|
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
||||||
|
role="listbox"
|
||||||
|
aria-label={_t("Community Autocomplete")}
|
||||||
|
>
|
||||||
|
{ completions }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ export class PillCompletion extends React.Component {
|
||||||
...restProps
|
...restProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={classNames('mx_Autocomplete_Completion_pill', className)} {...restProps}>
|
<div className={classNames('mx_Autocomplete_Completion_pill', className)} role="option" {...restProps}>
|
||||||
{ initialComponent }
|
{ initialComponent }
|
||||||
<span className="mx_Autocomplete_Completion_title">{ title }</span>
|
<span className="mx_Autocomplete_Completion_title">{ title }</span>
|
||||||
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
|
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
|
||||||
|
|
|
@ -116,7 +116,9 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
return {
|
return {
|
||||||
completion: unicode,
|
completion: unicode,
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion title={shortname} initialComponent={<span style={{maxWidth: '1em'}}>{ unicode }</span>} />
|
<PillCompletion title={shortname} aria-label={unicode} initialComponent={
|
||||||
|
<span style={{maxWidth: '1em'}}>{ unicode }</span>
|
||||||
|
} />
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
|
@ -130,8 +132,10 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_pill">
|
return (
|
||||||
{ completions }
|
<div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("Emoji Autocomplete")}>
|
||||||
</div>;
|
{ completions }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,8 +58,14 @@ export default class NotifProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
return (
|
||||||
{ completions }
|
<div
|
||||||
</div>;
|
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
||||||
|
role="listbox"
|
||||||
|
aria-label={_t("Notification Autocomplete")}
|
||||||
|
>
|
||||||
|
{ completions }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,8 +109,14 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
return (
|
||||||
{ completions }
|
<div
|
||||||
</div>;
|
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
||||||
|
role="listbox"
|
||||||
|
aria-label={_t("Room Autocomplete")}
|
||||||
|
>
|
||||||
|
{ completions }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,9 +164,11 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_pill">
|
return (
|
||||||
{ completions }
|
<div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("User Autocomplete")}>
|
||||||
</div>;
|
{ completions }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldForceComplete(): boolean {
|
shouldForceComplete(): boolean {
|
||||||
|
|
|
@ -20,18 +20,17 @@ import ReactDOM from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import flatMap from 'lodash/flatMap';
|
import flatMap from 'lodash/flatMap';
|
||||||
import isEqual from 'lodash/isEqual';
|
|
||||||
import sdk from '../../../index';
|
|
||||||
import type {Completion} from '../../../autocomplete/Autocompleter';
|
import type {Completion} from '../../../autocomplete/Autocompleter';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import { Room } from 'matrix-js-sdk';
|
import { Room } from 'matrix-js-sdk';
|
||||||
|
|
||||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import Autocompleter from '../../../autocomplete/Autocompleter';
|
import Autocompleter from '../../../autocomplete/Autocompleter';
|
||||||
|
|
||||||
const COMPOSER_SELECTED = 0;
|
const COMPOSER_SELECTED = 0;
|
||||||
|
|
||||||
|
export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`;
|
||||||
|
|
||||||
export default class Autocomplete extends React.Component {
|
export default class Autocomplete extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -224,7 +223,7 @@ export default class Autocomplete extends React.Component {
|
||||||
setSelection(selectionOffset: number) {
|
setSelection(selectionOffset: number) {
|
||||||
this.setState({selectionOffset, hide: false});
|
this.setState({selectionOffset, hide: false});
|
||||||
if (this.props.onSelectionChange) {
|
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;
|
let position = 1;
|
||||||
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||||
const completions = completionResult.completions.map((completion, i) => {
|
const completions = completionResult.completions.map((completion, i) => {
|
||||||
const className = classNames('mx_Autocomplete_Completion', {
|
const selected = position === this.state.selectionOffset;
|
||||||
'selected': position === this.state.selectionOffset,
|
const className = classNames('mx_Autocomplete_Completion', {selected});
|
||||||
});
|
|
||||||
const componentPosition = position;
|
const componentPosition = position;
|
||||||
position++;
|
position++;
|
||||||
|
|
||||||
|
@ -261,10 +259,12 @@ export default class Autocomplete extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
return React.cloneElement(completion.component, {
|
return React.cloneElement(completion.component, {
|
||||||
key: i,
|
"key": i,
|
||||||
ref: `completion${position - 1}`,
|
"ref": `completion${componentPosition}`,
|
||||||
|
"id": generateCompletionDomId(componentPosition - 1), // 0 index the completion IDs
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
|
"aria-selected": selected,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import {
|
||||||
replaceRangeAndMoveCaret,
|
replaceRangeAndMoveCaret,
|
||||||
} from '../../../editor/operations';
|
} from '../../../editor/operations';
|
||||||
import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom';
|
import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom';
|
||||||
import Autocomplete from '../rooms/Autocomplete';
|
import Autocomplete, {generateCompletionDomId} from '../rooms/Autocomplete';
|
||||||
import {autoCompleteCreator} from '../../../editor/parts';
|
import {autoCompleteCreator} from '../../../editor/parts';
|
||||||
import {parsePlainTextMessage} from '../../../editor/deserialize';
|
import {parsePlainTextMessage} from '../../../editor/deserialize';
|
||||||
import {renderModel} from '../../../editor/render';
|
import {renderModel} from '../../../editor/render';
|
||||||
|
@ -432,8 +432,9 @@ export default class BasicMessageEditor extends React.Component {
|
||||||
this.props.model.autoComplete.onComponentConfirm(completion);
|
this.props.model.autoComplete.onComponentConfirm(completion);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAutoCompleteSelectionChange = (completion) => {
|
_onAutoCompleteSelectionChange = (completion, completionIndex) => {
|
||||||
this.props.model.autoComplete.onComponentSelectionChange(completion);
|
this.props.model.autoComplete.onComponentSelectionChange(completion);
|
||||||
|
this.setState({completionIndex});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -535,6 +536,8 @@ export default class BasicMessageEditor extends React.Component {
|
||||||
quote: ctrlShortcutLabel(">"),
|
quote: ctrlShortcutLabel(">"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const {completionIndex} = this.state;
|
||||||
|
|
||||||
return (<div className={classes}>
|
return (<div className={classes}>
|
||||||
{ autoComplete }
|
{ autoComplete }
|
||||||
<MessageComposerFormatBar ref={ref => this._formatBarRef = ref} onAction={this._onFormatAction} shortcuts={shortcuts} />
|
<MessageComposerFormatBar ref={ref => this._formatBarRef = ref} onAction={this._onFormatAction} shortcuts={shortcuts} />
|
||||||
|
@ -548,7 +551,13 @@ export default class BasicMessageEditor extends React.Component {
|
||||||
onKeyDown={this._onKeyDown}
|
onKeyDown={this._onKeyDown}
|
||||||
ref={ref => this._editorRef = ref}
|
ref={ref => this._editorRef = ref}
|
||||||
aria-label={this.props.label}
|
aria-label={this.props.label}
|
||||||
></div>
|
role="textbox"
|
||||||
|
aria-multiline="true"
|
||||||
|
aria-autocomplete="both"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={Boolean(this.state.autoComplete)}
|
||||||
|
aria-activedescendant={completionIndex >= 0 ? generateCompletionDomId(completionIndex) : undefined}
|
||||||
|
/>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1726,11 +1726,16 @@
|
||||||
"Clear personal data": "Clear personal data",
|
"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.",
|
"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",
|
"Commands": "Commands",
|
||||||
|
"Community Autocomplete": "Community Autocomplete",
|
||||||
"Results from DuckDuckGo": "Results from DuckDuckGo",
|
"Results from DuckDuckGo": "Results from DuckDuckGo",
|
||||||
"Emoji": "Emoji",
|
"Emoji": "Emoji",
|
||||||
|
"Emoji Autocomplete": "Emoji Autocomplete",
|
||||||
"Notify the whole room": "Notify the whole room",
|
"Notify the whole room": "Notify the whole room",
|
||||||
"Room Notification": "Room Notification",
|
"Room Notification": "Room Notification",
|
||||||
|
"Notification Autocomplete": "Notification Autocomplete",
|
||||||
|
"Room Autocomplete": "Room Autocomplete",
|
||||||
"Users": "Users",
|
"Users": "Users",
|
||||||
|
"User Autocomplete": "User Autocomplete",
|
||||||
"unknown device": "unknown device",
|
"unknown device": "unknown device",
|
||||||
"NOT verified": "NOT verified",
|
"NOT verified": "NOT verified",
|
||||||
"Blacklisted": "Blacklisted",
|
"Blacklisted": "Blacklisted",
|
||||||
|
|
Loading…
Reference in a new issue