Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Autocomplete more accessible to screen reader users #3497

Merged
merged 2 commits into from
Oct 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/autocomplete/CommunityProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,14 @@ export default class CommunityProvider extends AutocompleteProvider {
}

renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
{ completions }
</div>;
return (
<div
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
role="listbox"
aria-label={_t("Community Autocomplete")}
>
{ completions }
</div>
);
}
}
2 changes: 1 addition & 1 deletion src/autocomplete/Components.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class PillCompletion extends React.Component {
...restProps
} = this.props;
return (
<div className={classNames('mx_Autocomplete_Completion_pill', className)} {...restProps}>
<div className={classNames('mx_Autocomplete_Completion_pill', className)} role="option" {...restProps}>
{ initialComponent }
<span className="mx_Autocomplete_Completion_title">{ title }</span>
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
Expand Down
12 changes: 8 additions & 4 deletions src/autocomplete/EmojiProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ export default class EmojiProvider extends AutocompleteProvider {
return {
completion: unicode,
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,
};
Expand All @@ -130,8 +132,10 @@ export default class EmojiProvider extends AutocompleteProvider {
}

renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill">
{ completions }
</div>;
return (
<div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("Emoji Autocomplete")}>
{ completions }
</div>
);
}
}
12 changes: 9 additions & 3 deletions src/autocomplete/NotifProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,14 @@ export default class NotifProvider extends AutocompleteProvider {
}

renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
{ completions }
</div>;
return (
<div
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
role="listbox"
aria-label={_t("Notification Autocomplete")}
>
{ completions }
</div>
);
}
}
12 changes: 9 additions & 3 deletions src/autocomplete/RoomProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,14 @@ export default class RoomProvider extends AutocompleteProvider {
}

renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
{ completions }
</div>;
return (
<div
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
role="listbox"
aria-label={_t("Room Autocomplete")}
>
{ completions }
</div>
);
}
}
8 changes: 5 additions & 3 deletions src/autocomplete/UserProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,11 @@ export default class UserProvider extends AutocompleteProvider {
}

renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill">
{ completions }
</div>;
return (
<div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("User Autocomplete")}>
{ completions }
</div>
);
}

shouldForceComplete(): boolean {
Expand Down
18 changes: 9 additions & 9 deletions src/components/views/rooms/Autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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++;

Expand All @@ -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,
});
});

Expand Down
15 changes: 12 additions & 3 deletions src/components/views/rooms/BasicMessageComposer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -535,6 +536,8 @@ export default class BasicMessageEditor extends React.Component {
quote: ctrlShortcutLabel(">"),
};

const {completionIndex} = this.state;

return (<div className={classes}>
{ autoComplete }
<MessageComposerFormatBar ref={ref => this._formatBarRef = ref} onAction={this._onFormatAction} shortcuts={shortcuts} />
Expand All @@ -548,7 +551,13 @@ export default class BasicMessageEditor extends React.Component {
onKeyDown={this._onKeyDown}
ref={ref => this._editorRef = ref}
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>);
}

Expand Down
5 changes: 5 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down