Use FocusLock around ContextMenus to simplify focus management

This commit is contained in:
Michael Telatynski 2021-07-02 16:31:37 +01:00
parent 780c413b5d
commit 171874ae30
2 changed files with 20 additions and 30 deletions

View file

@ -19,6 +19,7 @@ limitations under the License.
import React, { CSSProperties, RefObject, useRef, useState } from "react"; import React, { CSSProperties, RefObject, 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";
@ -44,6 +45,7 @@ function getOrCreateContainer(): HTMLDivElement {
} }
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]); const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
const ARIA_MENU_ITEM_SELECTOR = '[role^="menuitem"], [role^="menuitemcheckbox"], [role^="menuitemradio"]';
interface IPosition { interface IPosition {
top?: number; top?: number;
@ -95,8 +97,6 @@ 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;
static defaultProps = { static defaultProps = {
hasBackground: true, hasBackground: true,
managed: true, managed: true,
@ -107,24 +107,15 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
this.state = { this.state = {
contextMenuElem: null, contextMenuElem: null,
}; };
// persist what had focus when we got initialized so we can return it after
this.initialFocus = document.activeElement as HTMLElement;
} }
componentWillUnmount() { private collectContextMenuRect = (element: HTMLDivElement) => {
// return focus to the thing which had it before us
this.initialFocus.focus();
}
private collectContextMenuRect = (element) => {
// 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>(ARIA_MENU_ITEM_SELECTOR)
if (!first) { || element.querySelector<HTMLElement>('[tab-index]');
first = element.querySelector('[tab-index]');
}
if (first) { if (first) {
first.focus(); first.focus();
} }
@ -381,8 +372,10 @@ 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 } <FocusLock returnFocus={true}>
{ props.children } { chevron }
{ props.children }
</FocusLock>
</div> </div>
{ background } { background }
</div> </div>

View file

@ -17,7 +17,8 @@ limitations under the License.
import React, { useContext, useRef, useState } from "react"; import React, { useContext, useRef, useState } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { EventType, RoomType, RoomCreateTypeField } from "matrix-js-sdk/src/@types/event"; import { EventType, RoomType, RoomCreateTypeField } from "matrix-js-sdk/src/@types/event";
import FocusLock from "react-focus-lock"; import { Preset } from "matrix-js-sdk/src/@types/partials";
import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
@ -33,8 +34,6 @@ import { UserTab } from "../dialogs/UserSettingsDialog";
import Field from "../elements/Field"; import Field from "../elements/Field";
import withValidation from "../elements/Validation"; import withValidation from "../elements/Validation";
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView"; import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
import { Preset } from "matrix-js-sdk/src/@types/partials";
import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
import RoomAliasField from "../elements/RoomAliasField"; import RoomAliasField from "../elements/RoomAliasField";
const SpaceCreateMenuType = ({ title, description, className, onClick }) => { const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
@ -250,16 +249,14 @@ const SpaceCreateMenu = ({ onFinished }) => {
wrapperClassName="mx_SpaceCreateMenu_wrapper" wrapperClassName="mx_SpaceCreateMenu_wrapper"
managed={false} managed={false}
> >
<FocusLock returnFocus={true}> <BetaPill onClick={() => {
<BetaPill onClick={() => { onFinished();
onFinished(); defaultDispatcher.dispatch({
defaultDispatcher.dispatch({ action: Action.ViewUserSettings,
action: Action.ViewUserSettings, initialTabId: UserTab.Labs,
initialTabId: UserTab.Labs, });
}); }} />
}} /> { body }
{ body }
</FocusLock>
</ContextMenu>; </ContextMenu>;
}; };