mirror of https://github.com/vector-im/riot-web
				
				
				
			Merge pull request #3497 from matrix-org/t3chguy/autocomplete_a11y
Make Autocomplete more accessible to screen reader userspull/21833/head
						commit
						2d6461d376
					
				| 
						 | 
					@ -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 (
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					                className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
 | 
				
			||||||
 | 
					                role="listbox"
 | 
				
			||||||
 | 
					                aria-label={_t("Community Autocomplete")}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
                { completions }
 | 
					                { completions }
 | 
				
			||||||
        </div>;
 | 
					            </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 (
 | 
				
			||||||
 | 
					            <div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("Emoji Autocomplete")}>
 | 
				
			||||||
                { completions }
 | 
					                { completions }
 | 
				
			||||||
        </div>;
 | 
					            </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 (
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					                className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
 | 
				
			||||||
 | 
					                role="listbox"
 | 
				
			||||||
 | 
					                aria-label={_t("Notification Autocomplete")}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
                { completions }
 | 
					                { completions }
 | 
				
			||||||
        </div>;
 | 
					            </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 (
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					                className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
 | 
				
			||||||
 | 
					                role="listbox"
 | 
				
			||||||
 | 
					                aria-label={_t("Room Autocomplete")}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
                { completions }
 | 
					                { completions }
 | 
				
			||||||
        </div>;
 | 
					            </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 (
 | 
				
			||||||
 | 
					            <div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("User Autocomplete")}>
 | 
				
			||||||
                { completions }
 | 
					                { completions }
 | 
				
			||||||
        </div>;
 | 
					            </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 New Issue