Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 1b5aef4

Browse files
authored
Merge pull request #6311 from matrix-org/t3chguy/a11y/focus-lock-ctx-menu
2 parents ad1eb54 + 1c8bcce commit 1b5aef4

File tree

3 files changed

+27
-17
lines changed

3 files changed

+27
-17
lines changed

src/components/structures/ContextMenu.tsx

+24-11
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ limitations under the License.
1919
import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react";
2020
import ReactDOM from "react-dom";
2121
import classNames from "classnames";
22+
import FocusLock from "react-focus-lock";
2223

2324
import { Key } from "../../Keyboard";
2425
import { Writeable } from "../../@types/common";
@@ -43,8 +44,6 @@ function getOrCreateContainer(): HTMLDivElement {
4344
return container;
4445
}
4546

46-
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
47-
4847
export interface IPosition {
4948
top?: number;
5049
bottom?: number;
@@ -84,6 +83,10 @@ export interface IProps extends IPosition {
8483
// it will be mounted to a container at the root of the DOM.
8584
mountAsChild?: boolean;
8685

86+
// If specified, contents will be wrapped in a FocusLock, this is only needed if the context menu is being rendered
87+
// within an existing FocusLock e.g inside a modal.
88+
focusLock?: boolean;
89+
8790
// Function to be called on menu close
8891
onFinished();
8992
// on resize callback
@@ -99,7 +102,7 @@ interface IState {
99102
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
100103
@replaceableComponent("structures.ContextMenu")
101104
export class ContextMenu extends React.PureComponent<IProps, IState> {
102-
private initialFocus: HTMLElement;
105+
private readonly initialFocus: HTMLElement;
103106

104107
static defaultProps = {
105108
hasBackground: true,
@@ -108,6 +111,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
108111

109112
constructor(props, context) {
110113
super(props, context);
114+
111115
this.state = {
112116
contextMenuElem: null,
113117
};
@@ -121,14 +125,13 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
121125
this.initialFocus.focus();
122126
}
123127

124-
private collectContextMenuRect = (element) => {
128+
private collectContextMenuRect = (element: HTMLDivElement) => {
125129
// We don't need to clean up when unmounting, so ignore
126130
if (!element) return;
127131

128-
let first = element.querySelector('[role^="menuitem"]');
129-
if (!first) {
130-
first = element.querySelector('[tab-index]');
131-
}
132+
const first = element.querySelector<HTMLElement>('[role^="menuitem"]')
133+
|| element.querySelector<HTMLElement>('[tab-index]');
134+
132135
if (first) {
133136
first.focus();
134137
}
@@ -205,7 +208,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
205208
descending = true;
206209
}
207210
}
208-
} while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role")));
211+
} while (element && !element.getAttribute("role")?.startsWith("menuitem"));
209212

210213
if (element) {
211214
(element as HTMLElement).focus();
@@ -383,6 +386,17 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
383386
);
384387
}
385388

389+
let body = <>
390+
{ chevron }
391+
{ props.children }
392+
</>;
393+
394+
if (props.focusLock) {
395+
body = <FocusLock>
396+
{ body }
397+
</FocusLock>;
398+
}
399+
386400
return (
387401
<div
388402
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
@@ -397,8 +411,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
397411
ref={this.collectContextMenuRect}
398412
role={this.props.managed ? "menu" : undefined}
399413
>
400-
{ chevron }
401-
{ props.children }
414+
{ body }
402415
</div>
403416
{ background }
404417
</div>

src/components/views/directory/NetworkDropdown.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
268268
};
269269

270270
const buttonRect = handle.current.getBoundingClientRect();
271-
content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu}>
271+
content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu} focusLock>
272272
<div className="mx_NetworkDropdown_menu">
273273
{ options }
274274
<MenuItem className="mx_NetworkDropdown_server_add" label={undefined} onClick={onClick}>

src/components/views/spaces/SpaceCreateMenu.tsx

+2-5
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@ limitations under the License.
1717
import React, { ComponentProps, RefObject, SyntheticEvent, KeyboardEvent, useContext, useRef, useState } from "react";
1818
import classNames from "classnames";
1919
import { RoomType } from "matrix-js-sdk/src/@types/event";
20-
import FocusLock from "react-focus-lock";
21-
import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
2220
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
21+
import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
2322

2423
import { _t } from "../../../languageHandler";
2524
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
@@ -361,9 +360,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
361360
wrapperClassName="mx_SpaceCreateMenu_wrapper"
362361
managed={false}
363362
>
364-
<FocusLock returnFocus={true}>
365-
{ body }
366-
</FocusLock>
363+
{ body }
367364
</ContextMenu>;
368365
};
369366

0 commit comments

Comments
 (0)