Fix accessibility around the room list treeview and new search beta (#7856)
This commit is contained in:
parent
c6b8574dcb
commit
e2827b4082
6 changed files with 60 additions and 45 deletions
|
@ -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;
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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') {
|
||||||
|
if (this.state.spotlightBetaEnabled) {
|
||||||
|
this.openSpotlight();
|
||||||
|
} else {
|
||||||
this.focus();
|
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>;
|
||||||
|
|
|
@ -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="" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const lockProps = {
|
||||||
<MatrixClientContext.Provider value={this.matrixClient}>
|
"onKeyDown": this.onKeyDown,
|
||||||
<PosthogScreenTracker screenName={this.props.screenName} />
|
"role": "dialog",
|
||||||
<FocusLock
|
|
||||||
returnFocus={true}
|
|
||||||
lockProps={{
|
|
||||||
onKeyDown: this.onKeyDown,
|
|
||||||
role: "dialog",
|
|
||||||
["aria-labelledby"]: "mx_BaseDialog_title",
|
|
||||||
// This should point to a node describing the dialog.
|
// This should point to a node describing the dialog.
|
||||||
// If we were about to completely follow this recommendation we'd need to
|
// 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.
|
// make all the components relying on BaseDialog to be aware of it.
|
||||||
// So instead we will use the whole content as the description.
|
// So instead we will use the whole content as the description.
|
||||||
// Description comes first and if the content contains more text,
|
// Description comes first and if the content contains more text,
|
||||||
// AT users can skip its presentation.
|
// AT users can skip its presentation.
|
||||||
["aria-describedby"]: this.props.contentId,
|
"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 (
|
||||||
|
<MatrixClientContext.Provider value={this.matrixClient}>
|
||||||
|
<PosthogScreenTracker screenName={this.props.screenName} />
|
||||||
|
<FocusLock
|
||||||
|
returnFocus={true}
|
||||||
|
lockProps={lockProps}
|
||||||
className={classNames({
|
className={classNames({
|
||||||
[this.props.className]: true,
|
[this.props.className]: true,
|
||||||
'mx_Dialog_fixedWidth': this.props.fixedWidth,
|
'mx_Dialog_fixedWidth': this.props.fixedWidth,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue