From 7dfbe7a0680596cffab2f7b506027622384774de Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Dec 2021 15:15:16 +0000 Subject: [PATCH 01/30] Fix room list roving treeview New TooltipTarget & TextWithTooltip were not roving-accessible --- .../views/avatars/DecoratedRoomAvatar.tsx | 3 +++ src/components/views/elements/TextWithTooltip.tsx | 2 +- src/components/views/rooms/RoomTile.tsx | 15 +++++++-------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 99f2b70efcc..6ba507c0cc2 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -31,6 +31,7 @@ import TextWithTooltip from "../elements/TextWithTooltip"; import DMRoomMap from "../../../utils/DMRoomMap"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { IOOBData } from "../../../stores/ThreepidInviteStore"; +import TooltipTarget from "../elements/TooltipTarget"; interface IProps { room: Room; @@ -39,6 +40,7 @@ interface IProps { forceCount?: boolean; oobData?: IOOBData; viewAvatarOnClick?: boolean; + tooltipProps?: Omit, "label" | "tooltipClassName" | "className">; } interface IState { @@ -189,6 +191,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent; } diff --git a/src/components/views/elements/TextWithTooltip.tsx b/src/components/views/elements/TextWithTooltip.tsx index d5a37e16e79..2b5926f3d77 100644 --- a/src/components/views/elements/TextWithTooltip.tsx +++ b/src/components/views/elements/TextWithTooltip.tsx @@ -24,7 +24,7 @@ interface IProps { class?: string; tooltipClass?: string; tooltip: React.ReactNode; - tooltipProps?: {}; + tooltipProps?: Omit, "label" | "tooltipClassName" | "className">; onClick?: (ev?: React.MouseEvent) => void; } diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index d6916f50348..25603c6e4c1 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -566,13 +566,6 @@ export default class RoomTile extends React.PureComponent { if (typeof name !== 'string') name = ''; name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon - const roomAvatar = ; - let badge: React.ReactNode; if (!this.props.isMinimized && this.notificationState) { // aria-hidden because we summarise the unread count/highlight status in a manual aria-label below @@ -663,7 +656,13 @@ export default class RoomTile extends React.PureComponent { aria-selected={this.state.selected} aria-describedby={ariaDescribedBy} > - { roomAvatar } + { nameContainer } { badge } { this.renderGeneralMenu() } From e6e788536fbf5f9b2000f68d3a23119a842e2dd0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Dec 2021 15:27:38 +0000 Subject: [PATCH 02/30] Fix programmatic focus management in roving tab index not triggering onFocus handler --- src/accessibility/RovingTabIndex.tsx | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index cdd937bba3b..68ebe43d6af 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -131,6 +131,7 @@ export const reducer = (state: IState, action: IAction) => { } case Type.SetFocus: { + if (state.activeRef === action.payload.ref) return state; // update active ref state.activeRef = action.payload.ref; return { ...state }; @@ -194,6 +195,7 @@ export const RovingTabIndexProvider: React.FC = ({ } let handled = false; + let focusRef: RefObject; // Don't interfere with input default keydown behaviour if (ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") { // check if we actually have any items @@ -202,7 +204,7 @@ export const RovingTabIndexProvider: React.FC = ({ if (handleHomeEnd) { handled = true; // move focus to first (visible) item - findSiblingElement(context.state.refs, 0)?.current?.focus(); + focusRef = findSiblingElement(context.state.refs, 0); } break; @@ -210,7 +212,7 @@ export const RovingTabIndexProvider: React.FC = ({ if (handleHomeEnd) { handled = true; // move focus to last (visible) item - findSiblingElement(context.state.refs, context.state.refs.length - 1, true)?.current?.focus(); + focusRef = findSiblingElement(context.state.refs, context.state.refs.length - 1, true); } break; @@ -220,7 +222,7 @@ export const RovingTabIndexProvider: React.FC = ({ handled = true; if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); - findSiblingElement(context.state.refs, idx - 1)?.current?.focus(); + focusRef = findSiblingElement(context.state.refs, idx + 1); } } break; @@ -231,7 +233,7 @@ export const RovingTabIndexProvider: React.FC = ({ handled = true; if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); - findSiblingElement(context.state.refs, idx + 1, true)?.current?.focus(); + focusRef = findSiblingElement(context.state.refs, idx - 1, true); } } break; @@ -242,7 +244,17 @@ export const RovingTabIndexProvider: React.FC = ({ ev.preventDefault(); ev.stopPropagation(); } - }, [context.state, onKeyDown, handleHomeEnd, handleUpDown, handleLeftRight]); + + if (focusRef) { + focusRef.current?.focus(); + dispatch({ + type: Type.SetFocus, + payload: { + ref: focusRef, + }, + }); + } + }, [context, onKeyDown, handleHomeEnd, handleUpDown, handleLeftRight]); return { children({ onKeyDownHandler }) } @@ -283,7 +295,7 @@ export const useRovingTabIndex = (inputRef?: Ref): [FocusHandler, boolean, Ref] type: Type.SetFocus, payload: { ref }, }); - }, [ref, context]); + }, []); // eslint-disable-line react-hooks/exhaustive-deps const isActive = context.state.activeRef === ref; return [onFocus, isActive, ref]; From 08b1fdb4b733fc1074fd6e14ad60e04b2e225d10 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Dec 2021 15:28:06 +0000 Subject: [PATCH 03/30] Fix toolbar no longer handling left & right arrows --- src/accessibility/Toolbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx index 6e99c7f1fa2..c0f2b567484 100644 --- a/src/accessibility/Toolbar.tsx +++ b/src/accessibility/Toolbar.tsx @@ -52,7 +52,7 @@ const Toolbar: React.FC = ({ children, ...props }) => { } }; - return + return { ({ onKeyDownHandler }) =>
{ children }
} From 7c226b94e9dfb2b75c466724aba13760261c3b6b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Dec 2021 15:29:27 +0000 Subject: [PATCH 04/30] Fix roving tab index focus tracking on interactive element like context menu trigger --- src/components/views/messages/MessageActionBar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index abaf78797e2..74217b131a0 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -67,8 +67,9 @@ const OptionsButton: React.FC = ({ const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [onFocus, isActive, ref] = useRovingTabIndex(button); useEffect(() => { + onFocus(); onFocusChange(menuDisplayed); - }, [onFocusChange, menuDisplayed]); + }, [onFocus, onFocusChange, menuDisplayed]); let contextMenu: ReactElement | null; if (menuDisplayed) { From 4ddc97de844b56535af732b714e33b8750700381 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Dec 2021 15:42:56 +0000 Subject: [PATCH 05/30] Fix thread list context menu roving --- .../context_menus/ThreadListContextMenu.tsx | 62 ++++++++++++------- src/components/views/rooms/EventTile.tsx | 4 +- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/components/views/context_menus/ThreadListContextMenu.tsx b/src/components/views/context_menus/ThreadListContextMenu.tsx index f9aa7a4b9fc..012b2dbae4e 100644 --- a/src/components/views/context_menus/ThreadListContextMenu.tsx +++ b/src/components/views/context_menus/ThreadListContextMenu.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useCallback, useEffect, useState } from "react"; +import React, { RefObject, useCallback, useEffect } from "react"; import { MatrixEvent } from "matrix-js-sdk/src"; import { ButtonEvent } from "../elements/AccessibleButton"; @@ -22,11 +22,12 @@ import dis from '../../../dispatcher/dispatcher'; import { Action } from "../../../dispatcher/actions"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../../utils/strings"; -import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu"; +import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; import { _t } from "../../../languageHandler"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; interface IProps { mxEvent: MatrixEvent; @@ -34,6 +35,13 @@ interface IProps { onMenuToggle?: (open: boolean) => void; } +interface IExtendedProps extends IProps { + // Props for making the button into a roving one + tabIndex?: number; + inputRef?: RefObject; + onFocus?(): void; +} + const contextMenuBelow = (elementRect: DOMRect) => { // align the context menu's icons with the icon which opened the context menu const left = elementRect.left + window.pageXOffset + elementRect.width; @@ -42,11 +50,27 @@ const contextMenuBelow = (elementRect: DOMRect) => { return { left, top, chevronFace }; }; -const ThreadListContextMenu: React.FC = ({ mxEvent, permalinkCreator, onMenuToggle }) => { - const [optionsPosition, setOptionsPosition] = useState(null); - const closeThreadOptions = useCallback(() => { - setOptionsPosition(null); - }, []); +export const RovingThreadListContextMenu: React.FC = (props) => { + const [onFocus, isActive, ref] = useRovingTabIndex(); + + return ; +}; + +const ThreadListContextMenu: React.FC = ({ + mxEvent, + permalinkCreator, + onMenuToggle, + onFocus, + inputRef, + ...props +}) => { + const [menuDisplayed, _ref, openMenu, closeThreadOptions] = useContextMenu(); + const button = inputRef ?? _ref; // prefer the ref we receive via props in case we are being controlled const viewInRoom = useCallback((evt: ButtonEvent): void => { evt.preventDefault(); @@ -68,37 +92,31 @@ const ThreadListContextMenu: React.FC = ({ mxEvent, permalinkCreator, on closeThreadOptions(); }, [mxEvent, closeThreadOptions, permalinkCreator]); - const toggleOptionsMenu = useCallback((ev: ButtonEvent): void => { - if (!!optionsPosition) { - closeThreadOptions(); - } else { - const position = ev.currentTarget.getBoundingClientRect(); - setOptionsPosition(position); - } - }, [closeThreadOptions, optionsPosition]); - useEffect(() => { if (onMenuToggle) { - onMenuToggle(!!optionsPosition); + onMenuToggle(menuDisplayed); } - }, [optionsPosition, onMenuToggle]); + onFocus?.(); + }, [menuDisplayed, onMenuToggle, onFocus]); const isMainSplitTimelineShown = !WidgetLayoutStore.instance.hasMaximisedWidget( MatrixClientPeg.get().getRoom(mxEvent.getRoomId()), ); return - { !!optionsPosition && ( { isMainSplitTimelineShown && diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 077ef3d1c8a..4c134f62264 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -66,7 +66,7 @@ import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import Toolbar from '../../../accessibility/Toolbar'; import { POLL_START_EVENT_TYPE } from '../../../polls/consts'; import { RovingAccessibleTooltipButton } from '../../../accessibility/roving/RovingAccessibleTooltipButton'; -import ThreadListContextMenu from '../context_menus/ThreadListContextMenu'; +import { RovingThreadListContextMenu } from '../context_menus/ThreadListContextMenu'; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -1382,7 +1382,7 @@ export default class EventTile extends React.Component { onClick={() => dispatchShowThreadEvent(this.props.mxEvent)} key="thread" /> - Date: Fri, 10 Dec 2021 15:44:43 +0000 Subject: [PATCH 06/30] add comment --- src/accessibility/RovingTabIndex.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 68ebe43d6af..7eefe97f0f9 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -247,6 +247,7 @@ export const RovingTabIndexProvider: React.FC = ({ if (focusRef) { focusRef.current?.focus(); + // programmatic focus doesn't fire the onFocus handler so we must do the do ourselves dispatch({ type: Type.SetFocus, payload: { From 1784b444ef054a4fc769044547888ef25686ebde Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Dec 2021 15:52:20 +0000 Subject: [PATCH 07/30] fix comment --- src/accessibility/RovingTabIndex.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 7eefe97f0f9..769b0b683f9 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -247,7 +247,7 @@ export const RovingTabIndexProvider: React.FC = ({ if (focusRef) { focusRef.current?.focus(); - // programmatic focus doesn't fire the onFocus handler so we must do the do ourselves + // programmatic focus doesn't fire the onFocus handler, so we must do the do ourselves dispatch({ type: Type.SetFocus, payload: { From 64eef3a4ce36516daacdb260de7b09a98c7677f0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Dec 2021 09:38:54 +0000 Subject: [PATCH 08/30] Fix handling vertical arrows in the wrong direction --- src/accessibility/RovingTabIndex.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 769b0b683f9..bb4e66f467c 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -216,9 +216,11 @@ export const RovingTabIndexProvider: React.FC = ({ } break; - case Key.ARROW_UP: + case Key.ARROW_DOWN: case Key.ARROW_RIGHT: - if ((ev.key === Key.ARROW_UP && handleUpDown) || (ev.key === Key.ARROW_RIGHT && handleLeftRight)) { + if ((ev.key === Key.ARROW_DOWN && handleUpDown) || + (ev.key === Key.ARROW_RIGHT && handleLeftRight) + ) { handled = true; if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); @@ -227,9 +229,9 @@ export const RovingTabIndexProvider: React.FC = ({ } break; - case Key.ARROW_DOWN: + case Key.ARROW_UP: case Key.ARROW_LEFT: - if ((ev.key === Key.ARROW_DOWN && handleUpDown) || (ev.key === Key.ARROW_LEFT && handleLeftRight)) { + if ((ev.key === Key.ARROW_UP && handleUpDown) || (ev.key === Key.ARROW_LEFT && handleLeftRight)) { handled = true; if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); From 6ce07989df4034884ab16796cce29118fbf982c6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Dec 2021 23:43:45 +0000 Subject: [PATCH 09/30] iterate PR --- src/accessibility/RovingTabIndex.tsx | 1 + src/components/views/messages/MessageActionBar.tsx | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index bb4e66f467c..65494a210d8 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -131,6 +131,7 @@ export const reducer = (state: IState, action: IAction) => { } case Type.SetFocus: { + // if the ref doesn't change just return the same object reference to skip a re-render if (state.activeRef === action.payload.ref) return state; // update active ref state.activeRef = action.payload.ref; diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 74217b131a0..41adcdbaddf 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -67,6 +67,7 @@ const OptionsButton: React.FC = ({ const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [onFocus, isActive, ref] = useRovingTabIndex(button); useEffect(() => { + // when the context menu is opened directly, e.g via mouse click, the onFocus handle is skipped so call manually onFocus(); onFocusChange(menuDisplayed); }, [onFocus, onFocusChange, menuDisplayed]); @@ -113,8 +114,10 @@ const ReactButton: React.FC = ({ mxEvent, reactions, onFocusC const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [onFocus, isActive, ref] = useRovingTabIndex(button); useEffect(() => { + // when the context menu is opened directly, e.g via mouse click, the onFocus handle is skipped so call manually + onFocus(); onFocusChange(menuDisplayed); - }, [onFocusChange, menuDisplayed]); + }, [onFocus, onFocusChange, menuDisplayed]); let contextMenu; if (menuDisplayed) { From 2d0475d4d5fdec89253da3b5a989724c2db1560a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 14 Dec 2021 00:58:05 +0000 Subject: [PATCH 10/30] delint --- src/components/views/rooms/EventTile.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 83c35e9bfae..80d5e3bbbae 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -67,12 +67,11 @@ import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import Toolbar from '../../../accessibility/Toolbar'; import { POLL_START_EVENT_TYPE } from '../../../polls/consts'; import { RovingAccessibleTooltipButton } from '../../../accessibility/roving/RovingAccessibleTooltipButton'; -import ThreadListContextMenu from '../context_menus/ThreadListContextMenu'; +import { RovingThreadListContextMenu } from '../context_menus/ThreadListContextMenu'; import { ThreadNotificationState } from '../../../stores/notifications/ThreadNotificationState'; import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNotificationStateStore'; import { NotificationStateEvents } from '../../../stores/notifications/NotificationState'; import { NotificationColor } from '../../../stores/notifications/NotificationColor'; -import { RovingThreadListContextMenu } from '../context_menus/ThreadListContextMenu'; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', From bb1cbf7e0d49285862de01906f70d97d3cbdcb23 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 13 Dec 2021 21:40:13 -0600 Subject: [PATCH 11/30] Refactor ContextMenu to use RovingTabIndex --- src/accessibility/context_menu/MenuItem.tsx | 14 ++-- src/components/structures/ContextMenu.tsx | 87 ++++++--------------- 2 files changed, 31 insertions(+), 70 deletions(-) diff --git a/src/accessibility/context_menu/MenuItem.tsx b/src/accessibility/context_menu/MenuItem.tsx index 9c0b2482740..7f231c7bc0f 100644 --- a/src/accessibility/context_menu/MenuItem.tsx +++ b/src/accessibility/context_menu/MenuItem.tsx @@ -18,10 +18,9 @@ limitations under the License. import React from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; -import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; +import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../RovingTabIndex"; -interface IProps extends React.ComponentProps { +interface IProps extends React.ComponentProps { label?: string; tooltip?: string; } @@ -31,15 +30,14 @@ export const MenuItem: React.FC = ({ children, label, tooltip, ...props const ariaLabel = props["aria-label"] || label; if (tooltip) { - return + return { children } - ; + ; } return ( - + { children } - + ); }; - diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index e1aa014bdcd..ceeb0909507 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -26,6 +26,7 @@ import { Writeable } from "../../@types/common"; import { replaceableComponent } from "../../utils/replaceableComponent"; import UIStore from "../../stores/UIStore"; import { getInputableElement } from "./LoggedInView"; +import { RovingTabIndexProvider } from "../../accessibility/RovingTabIndex"; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -238,52 +239,10 @@ export default class ContextMenu extends React.PureComponent { }; private onKeyDown = (ev: React.KeyboardEvent) => { - // don't let keyboard handling escape the context menu - ev.stopPropagation(); - - if (!this.props.managed) { - if (ev.key === Key.ESCAPE) { - this.props.onFinished(); - ev.preventDefault(); - } - return; - } - - // only handle escape when in an input field - if (ev.key !== Key.ESCAPE && getInputableElement(ev.target as HTMLElement)) return; - - let handled = true; - - switch (ev.key) { - // XXX: this is imitating roving behaviour, it should really use the RovingTabIndex utils - // to inherit proper handling of unmount edge cases - case Key.TAB: - case Key.ESCAPE: - case Key.ARROW_LEFT: // close on left and right arrows too for when it is a context menu on a - case Key.ARROW_RIGHT: - this.props.onFinished(); - break; - case Key.ARROW_UP: - this.onMoveFocus(ev.target as Element, true); - break; - case Key.ARROW_DOWN: - this.onMoveFocus(ev.target as Element, false); - break; - case Key.HOME: - this.onMoveFocusHomeEnd(this.state.contextMenuElem, true); - break; - case Key.END: - this.onMoveFocusHomeEnd(this.state.contextMenuElem, false); - break; - default: - handled = false; - } - - if (handled) { - // consume all other keys in context menu - ev.preventDefault(); + if ((ev.key === Key.TAB && !this.props.focusLock) || ev.key === Key.ESCAPE) { + this.props.onFinished(); } - }; + } protected renderMenu(hasBackground = this.props.hasBackground) { const position: Partial> = {}; @@ -408,23 +367,27 @@ export default class ContextMenu extends React.PureComponent { } return ( -
- { background } -
- { body } -
-
+ + { ({ onKeyDownHandler }) => ( +
+ { background } +
+ { body } +
+
+ ) } +
); } From 1979f77d62279d913587692de68050762c122c81 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 13 Dec 2021 22:05:02 -0600 Subject: [PATCH 12/30] Restore some lost functionality for existing a ContextMenu --- src/components/structures/ContextMenu.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index ceeb0909507..2fed568a69d 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -239,7 +239,23 @@ export default class ContextMenu extends React.PureComponent { }; private onKeyDown = (ev: React.KeyboardEvent) => { - if ((ev.key === Key.TAB && !this.props.focusLock) || ev.key === Key.ESCAPE) { + // If someone is managing their own focus, we will only exit for them with Escape. + // They are probably using props.focusLock along with this option as well. + if(!this.props.managed && ev.key === Key.ESCAPE) { + this.props.onFinished(); + return; + } + + if ( + ev.key === Key.ESCAPE || + // You can only navigate the ContextMenu by arrow keys and Home/End (see RovingTabIndex). + // Tabbing to the next section of the page, will close the ContextMenu. + ev.key === Key.TAB || + // When someone moves left or right along a (like the + // MessageActionBar), we should close any ContextMenu that is open. + ev.key === Key.ARROW_LEFT || + ev.key === Key.ARROW_RIGHT + ) { this.props.onFinished(); } } From 283aece5ac068d948361befcab414fe10d11f7ac Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 13 Dec 2021 22:39:21 -0600 Subject: [PATCH 13/30] Add to other Menu options --- src/accessibility/context_menu/MenuItemCheckbox.tsx | 9 ++++----- src/accessibility/context_menu/MenuItemRadio.tsx | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/accessibility/context_menu/MenuItemCheckbox.tsx b/src/accessibility/context_menu/MenuItemCheckbox.tsx index 67da4cc85a3..6eb66102f3a 100644 --- a/src/accessibility/context_menu/MenuItemCheckbox.tsx +++ b/src/accessibility/context_menu/MenuItemCheckbox.tsx @@ -18,9 +18,9 @@ limitations under the License. import React from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import { RovingAccessibleButton } from "../RovingTabIndex"; -interface IProps extends React.ComponentProps { +interface IProps extends React.ComponentProps { label?: string; active: boolean; } @@ -28,16 +28,15 @@ interface IProps extends React.ComponentProps { // Semantic component for representing a role=menuitemcheckbox export const MenuItemCheckbox: React.FC = ({ children, label, active, disabled, ...props }) => { return ( - { children } - + ); }; diff --git a/src/accessibility/context_menu/MenuItemRadio.tsx b/src/accessibility/context_menu/MenuItemRadio.tsx index eb50d458365..f8c85dd8f9a 100644 --- a/src/accessibility/context_menu/MenuItemRadio.tsx +++ b/src/accessibility/context_menu/MenuItemRadio.tsx @@ -18,9 +18,9 @@ limitations under the License. import React from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import { RovingAccessibleButton } from "../RovingTabIndex"; -interface IProps extends React.ComponentProps { +interface IProps extends React.ComponentProps { label?: string; active: boolean; } @@ -28,16 +28,15 @@ interface IProps extends React.ComponentProps { // Semantic component for representing a role=menuitemradio export const MenuItemRadio: React.FC = ({ children, label, active, disabled, ...props }) => { return ( - { children } - + ); }; From ece5d7f57965c7513764932b03c08ccdf76064cd Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 13 Dec 2021 23:13:02 -0600 Subject: [PATCH 14/30] Refactor styled options to roving --- src/accessibility/RovingTabIndex.tsx | 4 ++-- .../context_menu/StyledMenuItemCheckbox.tsx | 7 ++++++- .../context_menu/StyledMenuItemRadio.tsx | 6 ++++++ src/components/views/elements/StyledCheckbox.tsx | 12 ++++++++++-- src/components/views/elements/StyledRadioButton.tsx | 11 +++++++++-- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 65494a210d8..832cb8dd48e 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -270,9 +270,9 @@ export const RovingTabIndexProvider: React.FC = ({ // onFocus should be called when the index gained focus in any manner // isActive should be used to set tabIndex in a manner such as `tabIndex={isActive ? 0 : -1}` // ref should be passed to a DOM node which will be used for DOM compareDocumentPosition -export const useRovingTabIndex = (inputRef?: Ref): [FocusHandler, boolean, Ref] => { +export const useRovingTabIndex = (inputRef?: RefObject): [FocusHandler, boolean, RefObject] => { const context = useContext(RovingTabIndexContext); - let ref = useRef(null); + let ref = useRef(null); if (inputRef) { // if we are given a ref, use it instead of ours diff --git a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx index 66846cc4849..7349646f2ba 100644 --- a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx +++ b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx @@ -19,6 +19,7 @@ limitations under the License. import React from "react"; import { Key } from "../../Keyboard"; +import { useRovingTabIndex } from "../RovingTabIndex"; import StyledCheckbox from "../../components/views/elements/StyledCheckbox"; interface IProps extends React.ComponentProps { @@ -29,6 +30,8 @@ interface IProps extends React.ComponentProps { // Semantic component for representing a styled role=menuitemcheckbox export const StyledMenuItemCheckbox: React.FC = ({ children, label, onChange, onClose, ...props }) => { + const [onFocus, isActive, ref] = useRovingTabIndex(); + const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === Key.ENTER || e.key === Key.SPACE) { e.stopPropagation(); @@ -52,11 +55,13 @@ export const StyledMenuItemCheckbox: React.FC = ({ children, label, onCh { children } diff --git a/src/accessibility/context_menu/StyledMenuItemRadio.tsx b/src/accessibility/context_menu/StyledMenuItemRadio.tsx index e3d340ef3e8..091f2a6b97f 100644 --- a/src/accessibility/context_menu/StyledMenuItemRadio.tsx +++ b/src/accessibility/context_menu/StyledMenuItemRadio.tsx @@ -19,6 +19,7 @@ limitations under the License. import React from "react"; import { Key } from "../../Keyboard"; +import { useRovingTabIndex } from "../RovingTabIndex"; import StyledRadioButton from "../../components/views/elements/StyledRadioButton"; interface IProps extends React.ComponentProps { @@ -29,6 +30,8 @@ interface IProps extends React.ComponentProps { // Semantic component for representing a styled role=menuitemradio export const StyledMenuItemRadio: React.FC = ({ children, label, onChange, onClose, ...props }) => { + const [onFocus, isActive, ref] = useRovingTabIndex(); + const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === Key.ENTER || e.key === Key.SPACE) { e.stopPropagation(); @@ -57,6 +60,9 @@ export const StyledMenuItemRadio: React.FC = ({ children, label, onChang onChange={onChange} onKeyDown={onKeyDown} onKeyUp={onKeyUp} + onFocus={onFocus} + inputRef={ref} + tabIndex={isActive ? 0 : -1} > { children } diff --git a/src/components/views/elements/StyledCheckbox.tsx b/src/components/views/elements/StyledCheckbox.tsx index 868791151b7..814ed6b38f5 100644 --- a/src/components/views/elements/StyledCheckbox.tsx +++ b/src/components/views/elements/StyledCheckbox.tsx @@ -26,6 +26,7 @@ export enum CheckboxStyle { } interface IProps extends React.InputHTMLAttributes { + inputRef?: React.RefObject; kind?: CheckboxStyle; } @@ -48,7 +49,8 @@ export default class StyledCheckbox extends React.PureComponent public render() { /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ - const { children, className, kind = CheckboxStyle.Solid, ...otherProps } = this.props; + const { children, className, kind = CheckboxStyle.Solid, inputRef, ...otherProps } = this.props; + const newClassName = classnames( "mx_Checkbox", className, @@ -58,7 +60,13 @@ export default class StyledCheckbox extends React.PureComponent }, ); return - +