Fix accessibility around the room list treeview and new search beta (#7856)

This commit is contained in:
Michael Telatynski 2022-02-21 15:46:13 +00:00 committed by GitHub
parent c6b8574dcb
commit e2827b4082
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 60 additions and 45 deletions

View file

@ -20,9 +20,9 @@ limitations under the License.
position: relative; position: relative;
height: 60%; height: 60%;
padding: 0; padding: 0;
contain: unset; // needed for .mx_SpotlightDialog_keyboardPrompt to not be culled contain: unset; // needed for #mx_SpotlightDialog_keyboardPrompt to not be culled
.mx_SpotlightDialog_keyboardPrompt { #mx_SpotlightDialog_keyboardPrompt {
position: absolute; position: absolute;
padding: 8px; padding: 8px;
border-radius: 8px; border-radius: 8px;
@ -146,8 +146,9 @@ limitations under the License.
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
.mx_SpotlightDialog_metaspaceResult, > .mx_SpotlightDialog_metaspaceResult,
.mx_DecoratedRoomAvatar { > .mx_DecoratedRoomAvatar,
> .mx_BaseAvatar {
margin-right: 8px; margin-right: 8px;
width: 20px; width: 20px;
height: 20px; height: 20px;

View file

@ -404,13 +404,13 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = {
default: { default: {
key: Key.ARROW_DOWN, key: Key.ARROW_DOWN,
}, },
displayName: _td("Navigate up in the room list"), displayName: _td("Navigate down in the room list"),
}, },
[KeyBindingAction.PrevRoom]: { [KeyBindingAction.PrevRoom]: {
default: { default: {
key: Key.ARROW_UP, key: Key.ARROW_UP,
}, },
displayName: _td("Navigate down in the room list"), displayName: _td("Navigate up in the room list"),
}, },
[KeyBindingAction.ToggleUserMenu]: { [KeyBindingAction.ToggleUserMenu]: {
default: { default: {

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import * as React from "react"; import * as React from "react";
import { createRef } from "react"; import { createRef, RefObject } from "react";
import classNames from "classnames"; import classNames from "classnames";
import defaultDispatcher from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher";
@ -54,7 +54,7 @@ interface IState {
export default class RoomSearch extends React.PureComponent<IProps, IState> { export default class RoomSearch extends React.PureComponent<IProps, IState> {
private readonly dispatcherRef: string; private readonly dispatcherRef: string;
private readonly betaRef: string; private readonly betaRef: string;
private inputRef: React.RefObject<HTMLInputElement> = createRef(); private elementRef: React.RefObject<HTMLInputElement | HTMLDivElement> = createRef();
private searchFilter: NameFilterCondition = new NameFilterCondition(); private searchFilter: NameFilterCondition = new NameFilterCondition();
constructor(props: IProps) { constructor(props: IProps) {
@ -113,13 +113,17 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
if (payload.action === Action.ViewRoom && payload.clear_search) { if (payload.action === Action.ViewRoom && payload.clear_search) {
this.clearInput(); this.clearInput();
} else if (payload.action === 'focus_room_filter') { } else if (payload.action === 'focus_room_filter') {
this.focus(); if (this.state.spotlightBetaEnabled) {
this.openSpotlight();
} else {
this.focus();
}
} }
}; };
private clearInput = () => { private clearInput = () => {
if (!this.inputRef.current) return; if (this.elementRef.current?.tagName !== "INPUT") return;
this.inputRef.current.value = ""; (this.elementRef.current as HTMLInputElement).value = "";
this.onChange(); this.onChange();
}; };
@ -133,8 +137,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
}; };
private onChange = () => { private onChange = () => {
if (!this.inputRef.current) return; if (this.elementRef.current?.tagName !== "INPUT") return;
this.setState({ query: this.inputRef.current.value }); this.setState({ query: (this.elementRef.current as HTMLInputElement).value });
}; };
private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => { private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
@ -167,11 +171,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
}; };
public focus = (): void => { public focus = (): void => {
if (this.state.spotlightBetaEnabled) { this.elementRef.current?.focus();
this.openSpotlight();
} else {
this.inputRef.current?.focus();
}
}; };
public render(): React.ReactNode { public render(): React.ReactNode {
@ -195,7 +195,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
let input = ( let input = (
<input <input
type="text" type="text"
ref={this.inputRef} ref={this.elementRef as RefObject<HTMLInputElement>}
className={inputClasses} className={inputClasses}
value={this.state.query} value={this.state.query}
onFocus={this.onFocus} onFocus={this.onFocus}
@ -217,7 +217,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
} }
if (this.state.spotlightBetaEnabled) { if (this.state.spotlightBetaEnabled) {
return <AccessibleButton onClick={this.openSpotlight} className={classes}> return <AccessibleButton onClick={this.openSpotlight} className={classes} inputRef={this.elementRef}>
{ icon } { icon }
{ input && <div className="mx_RoomSearch_spotlightTriggerText"> { input && <div className="mx_RoomSearch_spotlightTriggerText">
{ _t("Search") } { _t("Search") }
@ -229,6 +229,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
onClick={this.openSearch} onClick={this.openSearch}
className="mx_RoomSearch mx_RoomSearch_minimized" className="mx_RoomSearch mx_RoomSearch_minimized"
title={_t("Filter rooms and people")} title={_t("Filter rooms and people")}
inputRef={this.elementRef}
> >
{ icon } { icon }
</AccessibleButton>; </AccessibleButton>;

View file

@ -52,6 +52,8 @@ interface IProps extends IDialogProps {
// Title for the dialog. // Title for the dialog.
title?: JSX.Element | string; title?: JSX.Element | string;
// Specific aria label to use, if not provided will set aria-labelledBy to mx_Dialog_title
"aria-label"?: string;
// Path to an icon to put in the header // Path to an icon to put in the header
headerImage?: string; headerImage?: string;
@ -121,23 +123,30 @@ export default class BaseDialog extends React.Component<IProps> {
headerImage = <img className="mx_Dialog_titleImage" src={this.props.headerImage} alt="" />; headerImage = <img className="mx_Dialog_titleImage" src={this.props.headerImage} alt="" />;
} }
const lockProps = {
"onKeyDown": this.onKeyDown,
"role": "dialog",
// This should point to a node describing the dialog.
// If we were about to completely follow this recommendation we'd need to
// make all the components relying on BaseDialog to be aware of it.
// So instead we will use the whole content as the description.
// Description comes first and if the content contains more text,
// AT users can skip its presentation.
"aria-describedby": this.props.contentId,
};
if (this.props["aria-label"]) {
lockProps["aria-label"] = this.props["aria-label"];
} else {
lockProps["aria-labelledby"] = "mx_BaseDialog_title";
}
return ( return (
<MatrixClientContext.Provider value={this.matrixClient}> <MatrixClientContext.Provider value={this.matrixClient}>
<PosthogScreenTracker screenName={this.props.screenName} /> <PosthogScreenTracker screenName={this.props.screenName} />
<FocusLock <FocusLock
returnFocus={true} returnFocus={true}
lockProps={{ lockProps={lockProps}
onKeyDown: this.onKeyDown,
role: "dialog",
["aria-labelledby"]: "mx_BaseDialog_title",
// This should point to a node describing the dialog.
// If we were about to completely follow this recommendation we'd need to
// make all the components relying on BaseDialog to be aware of it.
// So instead we will use the whole content as the description.
// Description comes first and if the content contains more text,
// AT users can skip its presentation.
["aria-describedby"]: this.props.contentId,
}}
className={classNames({ className={classNames({
[this.props.className]: true, [this.props.className]: true,
'mx_Dialog_fixedWidth': this.props.fixedWidth, 'mx_Dialog_fixedWidth': this.props.fixedWidth,

View file

@ -79,7 +79,7 @@ import { getCachedRoomIDForAlias } from "../../../RoomAliasCache";
const MAX_RECENT_SEARCHES = 10; const MAX_RECENT_SEARCHES = 10;
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
const Option: React.FC<ComponentProps<typeof RovingAccessibleButton>> = ({ inputRef, ...props }) => { const Option: React.FC<ComponentProps<typeof RovingAccessibleButton>> = ({ inputRef, children, ...props }) => {
const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); const [onFocus, isActive, ref] = useRovingTabIndex(inputRef);
return <AccessibleButton return <AccessibleButton
{...props} {...props}
@ -88,7 +88,10 @@ const Option: React.FC<ComponentProps<typeof RovingAccessibleButton>> = ({ input
tabIndex={-1} tabIndex={-1}
aria-selected={isActive} aria-selected={isActive}
role="option" role="option"
/>; >
{ children }
<div className="mx_SpotlightDialog_enterPrompt" aria-hidden></div>
</AccessibleButton>;
}; };
const TooltipOption: React.FC<ComponentProps<typeof RovingAccessibleTooltipButton>> = ({ inputRef, ...props }) => { const TooltipOption: React.FC<ComponentProps<typeof RovingAccessibleTooltipButton>> = ({ inputRef, ...props }) => {
@ -357,7 +360,6 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
{ result.room.name } { result.room.name }
<NotificationBadge notification={RoomNotificationStateStore.instance.getRoomState(result.room)} /> <NotificationBadge notification={RoomNotificationStateStore.instance.getRoomState(result.room)} />
<ResultDetails room={result.room} /> <ResultDetails room={result.room} />
<div className="mx_SpotlightDialog_enterPrompt"></div>
</Option> </Option>
); );
} }
@ -372,7 +374,6 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
{ otherResult.avatar } { otherResult.avatar }
{ otherResult.name } { otherResult.name }
{ otherResult.description } { otherResult.description }
<div className="mx_SpotlightDialog_enterPrompt"></div>
</Option> </Option>
); );
}; };
@ -431,7 +432,6 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
{ room.name && room.canonical_alias && <div className="mx_SpotlightDialog_result_details"> { room.name && room.canonical_alias && <div className="mx_SpotlightDialog_result_details">
{ room.canonical_alias } { room.canonical_alias }
</div> } </div> }
<div className="mx_SpotlightDialog_enterPrompt"></div>
</Option> </Option>
)) } )) }
{ spaceResultsLoading && <Spinner /> } { spaceResultsLoading && <Spinner /> }
@ -463,7 +463,6 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
{ _t("Join %(roomAddress)s", { { _t("Join %(roomAddress)s", {
roomAddress: trimmedQuery, roomAddress: trimmedQuery,
}) } }) }
<div className="mx_SpotlightDialog_enterPrompt"></div>
</Option> </Option>
</div> </div>
</div>; </div>;
@ -490,7 +489,6 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
}} }}
> >
{ _t("Public rooms") } { _t("Public rooms") }
<div className="mx_SpotlightDialog_enterPrompt"></div>
</Option> </Option>
<Option <Option
id="mx_SpotlightDialog_button_startChat" id="mx_SpotlightDialog_button_startChat"
@ -501,7 +499,6 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
}} }}
> >
{ _t("People") } { _t("People") }
<div className="mx_SpotlightDialog_enterPrompt"></div>
</Option> </Option>
</div> </div>
</div> </div>
@ -544,7 +541,6 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
{ room.name } { room.name }
<NotificationBadge notification={RoomNotificationStateStore.instance.getRoomState(room)} /> <NotificationBadge notification={RoomNotificationStateStore.instance.getRoomState(room)} />
<ResultDetails room={room} /> <ResultDetails room={room} />
<div className="mx_SpotlightDialog_enterPrompt"></div>
</Option> </Option>
)) } )) }
</div> </div>
@ -590,7 +586,6 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
}} }}
> >
{ _t("Explore public rooms") } { _t("Explore public rooms") }
<div className="mx_SpotlightDialog_enterPrompt"></div>
</Option> </Option>
</div> </div>
</div> </div>
@ -679,7 +674,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
const activeDescendant = rovingContext.state.activeRef?.current?.id; const activeDescendant = rovingContext.state.activeRef?.current?.id;
return <> return <>
<div className="mx_SpotlightDialog_keyboardPrompt"> <div id="mx_SpotlightDialog_keyboardPrompt">
{ _t("Use <arrows/> to scroll", {}, { { _t("Use <arrows/> to scroll", {}, {
arrows: () => <> arrows: () => <>
<div></div> <div></div>
@ -696,6 +691,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
hasCancel={false} hasCancel={false}
onKeyDown={onDialogKeyDown} onKeyDown={onDialogKeyDown}
screenName="UnifiedSearch" screenName="UnifiedSearch"
aria-label={_t("Search Dialog")}
> >
<div className="mx_SpotlightDialog_searchBox mx_textinput"> <div className="mx_SpotlightDialog_searchBox mx_textinput">
<input <input
@ -708,10 +704,17 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
aria-owns="mx_SpotlightDialog_content" aria-owns="mx_SpotlightDialog_content"
aria-activedescendant={activeDescendant} aria-activedescendant={activeDescendant}
aria-label={_t("Search")}
aria-describedby="mx_SpotlightDialog_keyboardPrompt"
/> />
</div> </div>
<div id="mx_SpotlightDialog_content" role="listbox" aria-activedescendant={activeDescendant}> <div
id="mx_SpotlightDialog_content"
role="listbox"
aria-activedescendant={activeDescendant}
aria-describedby="mx_SpotlightDialog_keyboardPrompt"
>
{ content } { content }
</div> </div>

View file

@ -2809,6 +2809,7 @@
"Recent searches": "Recent searches", "Recent searches": "Recent searches",
"Clear": "Clear", "Clear": "Clear",
"Use <arrows/> to scroll": "Use <arrows/> to scroll", "Use <arrows/> to scroll": "Use <arrows/> to scroll",
"Search Dialog": "Search Dialog",
"Results not as expected? Please <a>give feedback</a>.": "Results not as expected? Please <a>give feedback</a>.", "Results not as expected? Please <a>give feedback</a>.": "Results not as expected? Please <a>give feedback</a>.",
"To help us prevent this in future, please <a>send us logs</a>.": "To help us prevent this in future, please <a>send us logs</a>.", "To help us prevent this in future, please <a>send us logs</a>.": "To help us prevent this in future, please <a>send us logs</a>.",
"Missing session data": "Missing session data", "Missing session data": "Missing session data",
@ -3423,8 +3424,8 @@
"Collapse room list section": "Collapse room list section", "Collapse room list section": "Collapse room list section",
"Expand room list section": "Expand room list section", "Expand room list section": "Expand room list section",
"Clear room list filter field": "Clear room list filter field", "Clear room list filter field": "Clear room list filter field",
"Navigate up in the room list": "Navigate up in the room list",
"Navigate down in the room list": "Navigate down in the room list", "Navigate down in the room list": "Navigate down in the room list",
"Navigate up in the room list": "Navigate up in the room list",
"Toggle the top left menu": "Toggle the top left menu", "Toggle the top left menu": "Toggle the top left menu",
"Toggle right panel": "Toggle right panel", "Toggle right panel": "Toggle right panel",
"Open this settings tab": "Open this settings tab", "Open this settings tab": "Open this settings tab",