Merge pull request #6311 from matrix-org/t3chguy/a11y/focus-lock-ctx-menu
This commit is contained in:
commit
1b5aef4447
3 changed files with 27 additions and 17 deletions
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react";
|
import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import FocusLock from "react-focus-lock";
|
||||||
|
|
||||||
import { Key } from "../../Keyboard";
|
import { Key } from "../../Keyboard";
|
||||||
import { Writeable } from "../../@types/common";
|
import { Writeable } from "../../@types/common";
|
||||||
|
@ -43,8 +44,6 @@ function getOrCreateContainer(): HTMLDivElement {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
|
|
||||||
|
|
||||||
export interface IPosition {
|
export interface IPosition {
|
||||||
top?: number;
|
top?: number;
|
||||||
bottom?: number;
|
bottom?: number;
|
||||||
|
@ -84,6 +83,10 @@ export interface IProps extends IPosition {
|
||||||
// it will be mounted to a container at the root of the DOM.
|
// it will be mounted to a container at the root of the DOM.
|
||||||
mountAsChild?: boolean;
|
mountAsChild?: boolean;
|
||||||
|
|
||||||
|
// If specified, contents will be wrapped in a FocusLock, this is only needed if the context menu is being rendered
|
||||||
|
// within an existing FocusLock e.g inside a modal.
|
||||||
|
focusLock?: boolean;
|
||||||
|
|
||||||
// Function to be called on menu close
|
// Function to be called on menu close
|
||||||
onFinished();
|
onFinished();
|
||||||
// on resize callback
|
// on resize callback
|
||||||
|
@ -99,7 +102,7 @@ interface IState {
|
||||||
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
||||||
@replaceableComponent("structures.ContextMenu")
|
@replaceableComponent("structures.ContextMenu")
|
||||||
export class ContextMenu extends React.PureComponent<IProps, IState> {
|
export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
private initialFocus: HTMLElement;
|
private readonly initialFocus: HTMLElement;
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
hasBackground: true,
|
hasBackground: true,
|
||||||
|
@ -108,6 +111,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
contextMenuElem: null,
|
contextMenuElem: null,
|
||||||
};
|
};
|
||||||
|
@ -121,14 +125,13 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
this.initialFocus.focus();
|
this.initialFocus.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private collectContextMenuRect = (element) => {
|
private collectContextMenuRect = (element: HTMLDivElement) => {
|
||||||
// We don't need to clean up when unmounting, so ignore
|
// We don't need to clean up when unmounting, so ignore
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
let first = element.querySelector('[role^="menuitem"]');
|
const first = element.querySelector<HTMLElement>('[role^="menuitem"]')
|
||||||
if (!first) {
|
|| element.querySelector<HTMLElement>('[tab-index]');
|
||||||
first = element.querySelector('[tab-index]');
|
|
||||||
}
|
|
||||||
if (first) {
|
if (first) {
|
||||||
first.focus();
|
first.focus();
|
||||||
}
|
}
|
||||||
|
@ -205,7 +208,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
descending = true;
|
descending = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role")));
|
} while (element && !element.getAttribute("role")?.startsWith("menuitem"));
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
(element as HTMLElement).focus();
|
(element as HTMLElement).focus();
|
||||||
|
@ -383,6 +386,17 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let body = <>
|
||||||
|
{ chevron }
|
||||||
|
{ props.children }
|
||||||
|
</>;
|
||||||
|
|
||||||
|
if (props.focusLock) {
|
||||||
|
body = <FocusLock>
|
||||||
|
{ body }
|
||||||
|
</FocusLock>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
|
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
|
||||||
|
@ -397,8 +411,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
ref={this.collectContextMenuRect}
|
ref={this.collectContextMenuRect}
|
||||||
role={this.props.managed ? "menu" : undefined}
|
role={this.props.managed ? "menu" : undefined}
|
||||||
>
|
>
|
||||||
{ chevron }
|
{ body }
|
||||||
{ props.children }
|
|
||||||
</div>
|
</div>
|
||||||
{ background }
|
{ background }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -268,7 +268,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonRect = handle.current.getBoundingClientRect();
|
const buttonRect = handle.current.getBoundingClientRect();
|
||||||
content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu}>
|
content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu} focusLock>
|
||||||
<div className="mx_NetworkDropdown_menu">
|
<div className="mx_NetworkDropdown_menu">
|
||||||
{ options }
|
{ options }
|
||||||
<MenuItem className="mx_NetworkDropdown_server_add" label={undefined} onClick={onClick}>
|
<MenuItem className="mx_NetworkDropdown_server_add" label={undefined} onClick={onClick}>
|
||||||
|
|
|
@ -17,9 +17,8 @@ limitations under the License.
|
||||||
import React, { ComponentProps, RefObject, SyntheticEvent, KeyboardEvent, useContext, useRef, useState } from "react";
|
import React, { ComponentProps, RefObject, SyntheticEvent, KeyboardEvent, useContext, useRef, useState } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
import FocusLock from "react-focus-lock";
|
|
||||||
import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
|
||||||
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
||||||
|
import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
@ -361,9 +360,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
wrapperClassName="mx_SpaceCreateMenu_wrapper"
|
wrapperClassName="mx_SpaceCreateMenu_wrapper"
|
||||||
managed={false}
|
managed={false}
|
||||||
>
|
>
|
||||||
<FocusLock returnFocus={true}>
|
|
||||||
{ body }
|
{ body }
|
||||||
</FocusLock>
|
|
||||||
</ContextMenu>;
|
</ContextMenu>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue