Use FocusLock around ContextMenus to simplify focus management
This commit is contained in:
parent
780c413b5d
commit
171874ae30
2 changed files with 20 additions and 30 deletions
|
@ -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>
|
||||||
|
|
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue