Make everything use the KeyBindingManager (#7907)

This commit is contained in:
Šimon Brandner 2022-02-28 17:05:52 +01:00 committed by GitHub
parent 5f8441216c
commit df591ee835
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 529 additions and 277 deletions

View file

@ -149,16 +149,11 @@ const roomBindings = (): KeyBinding[] => {
}; };
const navigationBindings = (): KeyBinding[] => { const navigationBindings = (): KeyBinding[] => {
const bindings = getBindingsByCategory(CategoryName.NAVIGATION); return getBindingsByCategory(CategoryName.NAVIGATION);
};
bindings.push({ const accessibilityBindings = (): KeyBinding[] => {
action: KeyBindingAction.CloseDialogOrContextMenu, return getBindingsByCategory(CategoryName.ACCESSIBILITY);
keyCombo: {
key: Key.ESCAPE,
},
});
return bindings;
}; };
const callBindings = (): KeyBinding[] => { const callBindings = (): KeyBinding[] => {
@ -177,6 +172,7 @@ export const defaultBindingsProvider: IKeyBindingsProvider = {
getRoomListBindings: roomListBindings, getRoomListBindings: roomListBindings,
getRoomBindings: roomBindings, getRoomBindings: roomBindings,
getNavigationBindings: navigationBindings, getNavigationBindings: navigationBindings,
getAccessibilityBindings: accessibilityBindings,
getCallBindings: callBindings, getCallBindings: callBindings,
getLabsBindings: labsBindings, getLabsBindings: labsBindings,
}; };

View file

@ -155,6 +155,10 @@ export class KeyBindingsManager {
return this.getAction(this.bindingsProviders.map(it => it.getNavigationBindings), ev); return this.getAction(this.bindingsProviders.map(it => it.getNavigationBindings), ev);
} }
getAccessibilityAction(ev: KeyboardEvent | React.KeyboardEvent): KeyBindingAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getAccessibilityBindings), ev);
}
getCallAction(ev: KeyboardEvent | React.KeyboardEvent): KeyBindingAction | undefined { getCallAction(ev: KeyboardEvent | React.KeyboardEvent): KeyBindingAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getCallBindings), ev); return this.getAction(this.bindingsProviders.map(it => it.getCallBindings), ev);
} }

View file

@ -130,10 +130,20 @@ export enum KeyBindingAction {
/** Toggles webcam while on a call */ /** Toggles webcam while on a call */
ToggleWebcamInCall = "KeyBinding.toggleWebcamInCall", ToggleWebcamInCall = "KeyBinding.toggleWebcamInCall",
/** Closes a dialog or a context menu */ /** Accessibility actions */
CloseDialogOrContextMenu = "KeyBinding.closeDialogOrContextMenu", Escape = "KeyBinding.escape",
/** Clicks the selected button */ Enter = "KeyBinding.enter",
ActivateSelectedButton = "KeyBinding.activateSelectedButton", Space = "KeyBinding.space",
Backspace = "KeyBinding.backspace",
Delete = "KeyBinding.delete",
Home = "KeyBinding.home",
End = "KeyBinding.end",
ArrowLeft = "KeyBinding.arrowLeft",
ArrowUp = "KeyBinding.arrowUp",
ArrowRight = "KeyBinding.arrowRight",
ArrowDown = "KeyBinding.arrowDown",
Tab = "KeyBinding.tab",
Comma = "KeyBinding.comma",
/** Toggle visibility of hidden events */ /** Toggle visibility of hidden events */
ToggleHiddenEventVisibility = 'KeyBinding.toggleHiddenEventVisibility', ToggleHiddenEventVisibility = 'KeyBinding.toggleHiddenEventVisibility',
@ -156,13 +166,14 @@ type IKeyboardShortcuts = {
}; };
export interface ICategory { export interface ICategory {
categoryLabel: string; categoryLabel?: string;
// TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager // TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager
settingNames: (KeyBindingAction)[]; settingNames: (KeyBindingAction)[];
} }
export enum CategoryName { export enum CategoryName {
NAVIGATION = "Navigation", NAVIGATION = "Navigation",
ACCESSIBILITY = "Accessibility",
CALLS = "Calls", CALLS = "Calls",
COMPOSER = "Composer", COMPOSER = "Composer",
ROOM_LIST = "Room List", ROOM_LIST = "Room List",
@ -245,12 +256,26 @@ export const CATEGORIES: Record<CategoryName, ICategory> = {
KeyBindingAction.NextRoom, KeyBindingAction.NextRoom,
KeyBindingAction.PrevRoom, KeyBindingAction.PrevRoom,
], ],
}, [CategoryName.ACCESSIBILITY]: {
categoryLabel: _td("Accessibility"),
settingNames: [
KeyBindingAction.Escape,
KeyBindingAction.Enter,
KeyBindingAction.Space,
KeyBindingAction.Backspace,
KeyBindingAction.Delete,
KeyBindingAction.Home,
KeyBindingAction.End,
KeyBindingAction.ArrowLeft,
KeyBindingAction.ArrowUp,
KeyBindingAction.ArrowRight,
KeyBindingAction.ArrowDown,
KeyBindingAction.Comma,
],
}, [CategoryName.NAVIGATION]: { }, [CategoryName.NAVIGATION]: {
categoryLabel: _td("Navigation"), categoryLabel: _td("Navigation"),
settingNames: [ settingNames: [
KeyBindingAction.ToggleUserMenu, KeyBindingAction.ToggleUserMenu,
KeyBindingAction.CloseDialogOrContextMenu,
KeyBindingAction.ActivateSelectedButton,
KeyBindingAction.ToggleRoomSidePanel, KeyBindingAction.ToggleRoomSidePanel,
KeyBindingAction.ToggleSpacePanel, KeyBindingAction.ToggleSpacePanel,
KeyBindingAction.ShowKeyboardSettings, KeyBindingAction.ShowKeyboardSettings,
@ -611,6 +636,68 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = {
}, },
displayName: _td("Open user settings"), displayName: _td("Open user settings"),
}, },
[KeyBindingAction.Escape]: {
default: {
key: Key.ESCAPE,
},
displayName: _td("Close dialog or context menu"),
},
[KeyBindingAction.Enter]: {
default: {
key: Key.ENTER,
},
displayName: _td("Activate selected button"),
},
[KeyBindingAction.Space]: {
default: {
key: Key.SPACE,
},
},
[KeyBindingAction.Backspace]: {
default: {
key: Key.BACKSPACE,
},
},
[KeyBindingAction.Delete]: {
default: {
key: Key.DELETE,
},
},
[KeyBindingAction.Home]: {
default: {
key: Key.HOME,
},
},
[KeyBindingAction.End]: {
default: {
key: Key.END,
},
},
[KeyBindingAction.ArrowLeft]: {
default: {
key: Key.ARROW_LEFT,
},
},
[KeyBindingAction.ArrowUp]: {
default: {
key: Key.ARROW_UP,
},
},
[KeyBindingAction.ArrowRight]: {
default: {
key: Key.ARROW_RIGHT,
},
},
[KeyBindingAction.ArrowDown]: {
default: {
key: Key.ARROW_DOWN,
},
},
[KeyBindingAction.Comma]: {
default: {
key: Key.COMMA,
},
},
}; };
// XXX: These have to be manually mirrored in KeyBindingDefaults // XXX: These have to be manually mirrored in KeyBindingDefaults
@ -651,18 +738,6 @@ const getNonCustomizableShortcuts = (): IKeyboardShortcuts => {
}, },
displayName: _td("Search (must be enabled)"), displayName: _td("Search (must be enabled)"),
}, },
[KeyBindingAction.CloseDialogOrContextMenu]: {
default: {
key: Key.ESCAPE,
},
displayName: _td("Close dialog or context menu"),
},
[KeyBindingAction.ActivateSelectedButton]: {
default: {
key: Key.ENTER,
},
displayName: _td("Activate selected button"),
},
}; };
if (PlatformPeg.get().overrideBrowserShortcuts()) { if (PlatformPeg.get().overrideBrowserShortcuts()) {

View file

@ -27,7 +27,8 @@ import React, {
RefObject, RefObject,
} from "react"; } from "react";
import { Key } from "../Keyboard"; import { getKeyBindingsManager } from "../KeyBindingsManager";
import { KeyBindingAction } from "./KeyboardShortcuts";
import { FocusHandler, Ref } from "./roving/types"; import { FocusHandler, Ref } from "./roving/types";
/** /**
@ -207,12 +208,13 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
} }
let handled = false; let handled = false;
const action = getKeyBindingsManager().getAccessibilityAction(ev);
let focusRef: RefObject<HTMLElement>; let focusRef: RefObject<HTMLElement>;
// Don't interfere with input default keydown behaviour // Don't interfere with input default keydown behaviour
// but allow people to move focus from it with Tab. // but allow people to move focus from it with Tab.
if (checkInputableElement(ev.target as HTMLElement)) { if (checkInputableElement(ev.target as HTMLElement)) {
switch (ev.key) { switch (action) {
case Key.TAB: case KeyBindingAction.Tab:
handled = true; handled = true;
if (context.state.refs.length > 0) { if (context.state.refs.length > 0) {
const idx = context.state.refs.indexOf(context.state.activeRef); const idx = context.state.refs.indexOf(context.state.activeRef);
@ -222,8 +224,8 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
} }
} else { } else {
// check if we actually have any items // check if we actually have any items
switch (ev.key) { switch (action) {
case Key.HOME: case KeyBindingAction.Home:
if (handleHomeEnd) { if (handleHomeEnd) {
handled = true; handled = true;
// move focus to first (visible) item // move focus to first (visible) item
@ -231,7 +233,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
} }
break; break;
case Key.END: case KeyBindingAction.End:
if (handleHomeEnd) { if (handleHomeEnd) {
handled = true; handled = true;
// move focus to last (visible) item // move focus to last (visible) item
@ -239,10 +241,10 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
} }
break; break;
case Key.ARROW_DOWN: case KeyBindingAction.ArrowDown:
case Key.ARROW_RIGHT: case KeyBindingAction.ArrowRight:
if ((ev.key === Key.ARROW_DOWN && handleUpDown) || if ((action === KeyBindingAction.ArrowDown && handleUpDown) ||
(ev.key === Key.ARROW_RIGHT && handleLeftRight) (action === KeyBindingAction.ArrowRight && handleLeftRight)
) { ) {
handled = true; handled = true;
if (context.state.refs.length > 0) { if (context.state.refs.length > 0) {
@ -252,9 +254,11 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
} }
break; break;
case Key.ARROW_UP: case KeyBindingAction.ArrowUp:
case Key.ARROW_LEFT: case KeyBindingAction.ArrowLeft:
if ((ev.key === Key.ARROW_UP && handleUpDown) || (ev.key === Key.ARROW_LEFT && handleLeftRight)) { if ((action === KeyBindingAction.ArrowUp && handleUpDown) ||
(action === KeyBindingAction.ArrowLeft && handleLeftRight)
) {
handled = true; handled = true;
if (context.state.refs.length > 0) { if (context.state.refs.length > 0) {
const idx = context.state.refs.indexOf(context.state.activeRef); const idx = context.state.refs.indexOf(context.state.activeRef);

View file

@ -17,7 +17,8 @@ limitations under the License.
import React from "react"; import React from "react";
import { RovingTabIndexProvider } from "./RovingTabIndex"; import { RovingTabIndexProvider } from "./RovingTabIndex";
import { Key } from "../Keyboard"; import { getKeyBindingsManager } from "../KeyBindingsManager";
import { KeyBindingAction } from "./KeyboardShortcuts";
interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> { interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> {
} }
@ -34,9 +35,10 @@ const Toolbar: React.FC<IProps> = ({ children, ...props }) => {
let handled = true; let handled = true;
// HOME and END are handled by RovingTabIndexProvider // HOME and END are handled by RovingTabIndexProvider
switch (ev.key) { const action = getKeyBindingsManager().getAccessibilityAction(ev);
case Key.ARROW_UP: switch (action) {
case Key.ARROW_DOWN: case KeyBindingAction.ArrowUp:
case KeyBindingAction.ArrowDown:
if (target.hasAttribute('aria-haspopup')) { if (target.hasAttribute('aria-haspopup')) {
target.click(); target.click();
} }

View file

@ -18,14 +18,15 @@ limitations under the License.
import React from "react"; import React from "react";
import { Key } from "../../Keyboard";
import { useRovingTabIndex } from "../RovingTabIndex"; import { useRovingTabIndex } from "../RovingTabIndex";
import StyledCheckbox from "../../components/views/elements/StyledCheckbox"; import StyledCheckbox from "../../components/views/elements/StyledCheckbox";
import { KeyBindingAction } from "../KeyboardShortcuts";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
interface IProps extends React.ComponentProps<typeof StyledCheckbox> { interface IProps extends React.ComponentProps<typeof StyledCheckbox> {
label?: string; label?: string;
onChange(); // we handle keyup/down ourselves so lose the ChangeEvent onChange(); // we handle keyup/down ourselves so lose the ChangeEvent
onClose(): void; // gets called after onChange on Key.ENTER onClose(): void; // gets called after onChange on KeyBindingAction.ActivateSelectedButton
} }
// Semantic component for representing a styled role=menuitemcheckbox // Semantic component for representing a styled role=menuitemcheckbox
@ -33,22 +34,37 @@ export const StyledMenuItemCheckbox: React.FC<IProps> = ({ children, label, onCh
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>(); const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>();
const onKeyDown = (e: React.KeyboardEvent) => { const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === Key.ENTER || e.key === Key.SPACE) { let handled = true;
const action = getKeyBindingsManager().getAccessibilityAction(e);
switch (action) {
case KeyBindingAction.Space:
onChange();
break;
case KeyBindingAction.Enter:
onChange();
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
onClose();
break;
default:
handled = false;
}
if (handled) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
onChange();
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
if (e.key === Key.ENTER) {
onClose();
}
} }
}; };
const onKeyUp = (e: React.KeyboardEvent) => { const onKeyUp = (e: React.KeyboardEvent) => {
// prevent the input default handler as we handle it on keydown to match const action = getKeyBindingsManager().getAccessibilityAction(e);
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html switch (action) {
if (e.key === Key.SPACE || e.key === Key.ENTER) { case KeyBindingAction.Space:
e.stopPropagation(); case KeyBindingAction.Enter:
e.preventDefault(); // prevent the input default handler as we handle it on keydown to match
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
e.stopPropagation();
e.preventDefault();
break;
} }
}; };
return ( return (

View file

@ -18,14 +18,15 @@ limitations under the License.
import React from "react"; import React from "react";
import { Key } from "../../Keyboard";
import { useRovingTabIndex } from "../RovingTabIndex"; import { useRovingTabIndex } from "../RovingTabIndex";
import StyledRadioButton from "../../components/views/elements/StyledRadioButton"; import StyledRadioButton from "../../components/views/elements/StyledRadioButton";
import { KeyBindingAction } from "../KeyboardShortcuts";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
interface IProps extends React.ComponentProps<typeof StyledRadioButton> { interface IProps extends React.ComponentProps<typeof StyledRadioButton> {
label?: string; label?: string;
onChange(); // we handle keyup/down ourselves so lose the ChangeEvent onChange(); // we handle keyup/down ourselves so lose the ChangeEvent
onClose(): void; // gets called after onChange on Key.ENTER onClose(): void; // gets called after onChange on KeyBindingAction.Enter
} }
// Semantic component for representing a styled role=menuitemradio // Semantic component for representing a styled role=menuitemradio
@ -33,22 +34,37 @@ export const StyledMenuItemRadio: React.FC<IProps> = ({ children, label, onChang
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>(); const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>();
const onKeyDown = (e: React.KeyboardEvent) => { const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === Key.ENTER || e.key === Key.SPACE) { let handled = true;
const action = getKeyBindingsManager().getAccessibilityAction(e);
switch (action) {
case KeyBindingAction.Space:
onChange();
break;
case KeyBindingAction.Enter:
onChange();
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
onClose();
break;
default:
handled = false;
}
if (handled) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
onChange();
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
if (e.key === Key.ENTER) {
onClose();
}
} }
}; };
const onKeyUp = (e: React.KeyboardEvent) => { const onKeyUp = (e: React.KeyboardEvent) => {
// prevent the input default handler as we handle it on keydown to match const action = getKeyBindingsManager().getAccessibilityAction(e);
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html switch (action) {
if (e.key === Key.SPACE || e.key === Key.ENTER) { case KeyBindingAction.Enter:
e.stopPropagation(); case KeyBindingAction.Space:
e.preventDefault(); // prevent the input default handler as we handle it on keydown to match
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
e.stopPropagation();
e.preventDefault();
break;
} }
}; };
return ( return (

View file

@ -21,11 +21,12 @@ import ReactDOM from "react-dom";
import classNames from "classnames"; import classNames from "classnames";
import FocusLock from "react-focus-lock"; import FocusLock from "react-focus-lock";
import { Key } from "../../Keyboard";
import { Writeable } from "../../@types/common"; import { Writeable } from "../../@types/common";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import UIStore from "../../stores/UIStore"; import UIStore from "../../stores/UIStore";
import { checkInputableElement, RovingTabIndexProvider } from "../../accessibility/RovingTabIndex"; import { checkInputableElement, RovingTabIndexProvider } from "../../accessibility/RovingTabIndex";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
// Shamelessly ripped off Modal.js. There's probably a better way // Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and // of doing reusable widgets like dialog boxes & menus where we go and
@ -191,30 +192,32 @@ export default class ContextMenu extends React.PureComponent<IProps, IState> {
private onKeyDown = (ev: React.KeyboardEvent) => { private onKeyDown = (ev: React.KeyboardEvent) => {
ev.stopPropagation(); // prevent keyboard propagating out of the context menu, we're focus-locked ev.stopPropagation(); // prevent keyboard propagating out of the context menu, we're focus-locked
const action = getKeyBindingsManager().getAccessibilityAction(ev);
// If someone is managing their own focus, we will only exit for them with 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. // They are probably using props.focusLock along with this option as well.
if (!this.props.managed) { if (!this.props.managed) {
if (ev.key === Key.ESCAPE) { if (action === KeyBindingAction.Escape) {
this.props.onFinished(); this.props.onFinished();
} }
return; return;
} }
// When an <input> is focused, only handle the Escape key // When an <input> is focused, only handle the Escape key
if (checkInputableElement(ev.target as HTMLElement) && ev.key !== Key.ESCAPE) { if (checkInputableElement(ev.target as HTMLElement) && action !== KeyBindingAction.Escape) {
return; return;
} }
if ( if ([
ev.key === Key.ESCAPE || KeyBindingAction.Escape,
// You can only navigate the ContextMenu by arrow keys and Home/End (see RovingTabIndex). // 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. // Tabbing to the next section of the page, will close the ContextMenu.
ev.key === Key.TAB || KeyBindingAction.Tab,
// When someone moves left or right along a <Toolbar /> (like the // When someone moves left or right along a <Toolbar /> (like the
// MessageActionBar), we should close any ContextMenu that is open. // MessageActionBar), we should close any ContextMenu that is open.
ev.key === Key.ARROW_LEFT || KeyBindingAction.ArrowLeft,
ev.key === Key.ARROW_RIGHT KeyBindingAction.ArrowRight,
) { ].includes(action)) {
this.props.onFinished(); this.props.onFinished();
} }
}; };

View file

@ -35,7 +35,6 @@ import { getKeyBindingsManager } from "../../KeyBindingsManager";
import UIStore from "../../stores/UIStore"; import UIStore from "../../stores/UIStore";
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex"; import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
import RoomListHeader from "../views/rooms/RoomListHeader"; import RoomListHeader from "../views/rooms/RoomListHeader";
import { Key } from "../../Keyboard";
import RecentlyViewedButton from "../views/rooms/RecentlyViewedButton"; import RecentlyViewedButton from "../views/rooms/RecentlyViewedButton";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
@ -316,12 +315,15 @@ export default class LeftPanel extends React.Component<IProps, IState> {
private onRoomListKeydown = (ev: React.KeyboardEvent) => { private onRoomListKeydown = (ev: React.KeyboardEvent) => {
if (ev.altKey || ev.ctrlKey || ev.metaKey) return; if (ev.altKey || ev.ctrlKey || ev.metaKey) return;
if (SettingsStore.getValue("feature_spotlight")) return; if (SettingsStore.getValue("feature_spotlight")) return;
const action = getKeyBindingsManager().getAccessibilityAction(ev);
// we cannot handle Space as that is an activation key for all focusable elements in this widget // we cannot handle Space as that is an activation key for all focusable elements in this widget
if (ev.key.length === 1) { if (ev.key.length === 1) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.roomSearchRef.current?.appendChar(ev.key); this.roomSearchRef.current?.appendChar(ev.key);
} else if (ev.key === Key.BACKSPACE) { } else if (action === KeyBindingAction.Backspace) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.roomSearchRef.current?.backspace(); this.roomSearchRef.current?.backspace();

View file

@ -20,7 +20,6 @@ import classNames from "classnames";
import AccessibleButton from "../views/elements/AccessibleButton"; import AccessibleButton from "../views/elements/AccessibleButton";
import { useRovingTabIndex } from "../../accessibility/RovingTabIndex"; import { useRovingTabIndex } from "../../accessibility/RovingTabIndex";
import { Key } from "../../Keyboard";
import { useLocalStorageState } from "../../hooks/useLocalStorageState"; import { useLocalStorageState } from "../../hooks/useLocalStorageState";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import WidgetUtils, { IWidgetEvent } from "../../utils/WidgetUtils"; import WidgetUtils, { IWidgetEvent } from "../../utils/WidgetUtils";
@ -28,6 +27,8 @@ import { useAccountData } from "../../hooks/useAccountData";
import AppTile from "../views/elements/AppTile"; import AppTile from "../views/elements/AppTile";
import { useSettingValue } from "../../hooks/useSettings"; import { useSettingValue } from "../../hooks/useSettings";
import UIStore from "../../stores/UIStore"; import UIStore from "../../stores/UIStore";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
const MIN_HEIGHT = 100; const MIN_HEIGHT = 100;
const MAX_HEIGHT = 500; // or 50% of the window height const MAX_HEIGHT = 500; // or 50% of the window height
@ -91,16 +92,16 @@ const LeftPanelWidget: React.FC = () => {
onFocus={onFocus} onFocus={onFocus}
className="mx_LeftPanelWidget_headerContainer" className="mx_LeftPanelWidget_headerContainer"
onKeyDown={(ev: React.KeyboardEvent) => { onKeyDown={(ev: React.KeyboardEvent) => {
switch (ev.key) { const action = getKeyBindingsManager().getAccessibilityAction(ev);
case Key.ARROW_LEFT: switch (action) {
case KeyBindingAction.ArrowLeft:
ev.stopPropagation(); ev.stopPropagation();
setExpanded(false); setExpanded(false);
break; break;
case Key.ARROW_RIGHT: { case KeyBindingAction.ArrowRight:
ev.stopPropagation(); ev.stopPropagation();
setExpanded(true); setExpanded(true);
break; break;
}
} }
}} }}
> >

View file

@ -19,9 +19,10 @@ import React, { createRef, HTMLProps } from 'react';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { Key } from '../../Keyboard';
import AccessibleButton from '../../components/views/elements/AccessibleButton'; import AccessibleButton from '../../components/views/elements/AccessibleButton';
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
interface IProps extends HTMLProps<HTMLInputElement> { interface IProps extends HTMLProps<HTMLInputElement> {
onSearch?: (query: string) => void; onSearch?: (query: string) => void;
@ -66,8 +67,9 @@ export default class SearchBox extends React.Component<IProps, IState> {
}, 200, { trailing: true, leading: true }); }, 200, { trailing: true, leading: true });
private onKeyDown = (ev: React.KeyboardEvent): void => { private onKeyDown = (ev: React.KeyboardEvent): void => {
switch (ev.key) { const action = getKeyBindingsManager().getAccessibilityAction(ev);
case Key.ESCAPE: switch (action) {
case KeyBindingAction.Escape:
this.clearSearch("keyboard"); this.clearSearch("keyboard");
break; break;
} }

View file

@ -54,7 +54,6 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { linkifyElement } from "../../HtmlUtils"; import { linkifyElement } from "../../HtmlUtils";
import { useDispatcher } from "../../hooks/useDispatcher"; import { useDispatcher } from "../../hooks/useDispatcher";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { Key } from "../../Keyboard";
import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex"; import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
import { getDisplayAliasForRoom } from "./RoomDirectory"; import { getDisplayAliasForRoom } from "./RoomDirectory";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
@ -64,6 +63,8 @@ import { awaitRoomDownSync } from "../../utils/RoomUpgrade";
import RoomViewStore from "../../stores/RoomViewStore"; import RoomViewStore from "../../stores/RoomViewStore";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomReadyPayload"; import { JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomReadyPayload";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
interface IProps { interface IProps {
space: Room; space: Room;
@ -247,10 +248,13 @@ const Tile: React.FC<ITileProps> = ({
if (showChildren) { if (showChildren) {
const onChildrenKeyDown = (e) => { const onChildrenKeyDown = (e) => {
if (e.key === Key.ARROW_LEFT) { const action = getKeyBindingsManager().getAccessibilityAction(e);
e.preventDefault(); switch (action) {
e.stopPropagation(); case KeyBindingAction.ArrowLeft:
ref.current?.focus(); e.preventDefault();
e.stopPropagation();
ref.current?.focus();
break;
} }
}; };
@ -266,15 +270,16 @@ const Tile: React.FC<ITileProps> = ({
onKeyDown = (e) => { onKeyDown = (e) => {
let handled = false; let handled = false;
switch (e.key) { const action = getKeyBindingsManager().getAccessibilityAction(e);
case Key.ARROW_LEFT: switch (action) {
case KeyBindingAction.ArrowLeft:
if (showChildren) { if (showChildren) {
handled = true; handled = true;
toggleShowChildren(); toggleShowChildren();
} }
break; break;
case Key.ARROW_RIGHT: case KeyBindingAction.ArrowRight:
handled = true; handled = true;
if (showChildren) { if (showChildren) {
const childSection = ref.current?.nextElementSibling; const childSection = ref.current?.nextElementSibling;
@ -700,7 +705,11 @@ const SpaceHierarchy = ({
} }
const onKeyDown = (ev: KeyboardEvent, state: IState): void => { const onKeyDown = (ev: KeyboardEvent, state: IState): void => {
if (ev.key === Key.ARROW_DOWN && ev.currentTarget.classList.contains("mx_SpaceHierarchy_search")) { const action = getKeyBindingsManager().getAccessibilityAction(ev);
if (
action === KeyBindingAction.ArrowDown &&
ev.currentTarget.classList.contains("mx_SpaceHierarchy_search")
) {
state.refs[0]?.current?.focus(); state.refs[0]?.current?.focus();
} }
}; };

View file

@ -37,7 +37,6 @@ import UserActivity from "../../UserActivity";
import Modal from "../../Modal"; import Modal from "../../Modal";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { Action } from '../../dispatcher/actions'; import { Action } from '../../dispatcher/actions';
import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer'; import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent'; import shouldHideEvent from '../../shouldHideEvent';
import { haveTileForEvent } from "../views/rooms/EventTile"; import { haveTileForEvent } from "../views/rooms/EventTile";
@ -54,6 +53,8 @@ import EditorStateTransfer from '../../utils/EditorStateTransfer';
import ErrorDialog from '../views/dialogs/ErrorDialog'; import ErrorDialog from '../views/dialogs/ErrorDialog';
import CallEventGrouper, { buildCallEventGroupers } from "./CallEventGrouper"; import CallEventGrouper, { buildCallEventGroupers } from "./CallEventGrouper";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
const PAGINATE_SIZE = 20; const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20; const INITIAL_SIZE = 20;
@ -1086,11 +1087,12 @@ class TimelinePanel extends React.Component<IProps, IState> {
* We pass it down to the scroll panel. * We pass it down to the scroll panel.
*/ */
public handleScrollKey = ev => { public handleScrollKey = ev => {
if (!this.messagePanel.current) { return; } if (!this.messagePanel.current) return;
// jump to the live timeline on ctrl-end, rather than the end of the // jump to the live timeline on ctrl-end, rather than the end of the
// timeline window. // timeline window.
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && ev.key === Key.END) { const action = getKeyBindingsManager().getRoomAction(ev);
if (action === KeyBindingAction.JumpToLatestMessage) {
this.jumpToLiveTimeline(); this.jumpToLiveTimeline();
} else { } else {
this.messagePanel.current.handleScrollKey(ev); this.messagePanel.current.handleScrollKey(ev);

View file

@ -20,11 +20,12 @@ import PlayPauseButton from "./PlayPauseButton";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { formatBytes } from "../../../utils/FormattingUtils"; import { formatBytes } from "../../../utils/FormattingUtils";
import DurationClock from "./DurationClock"; import DurationClock from "./DurationClock";
import { Key } from "../../../Keyboard";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import SeekBar from "./SeekBar"; import SeekBar from "./SeekBar";
import PlaybackClock from "./PlaybackClock"; import PlaybackClock from "./PlaybackClock";
import AudioPlayerBase from "./AudioPlayerBase"; import AudioPlayerBase from "./AudioPlayerBase";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
@replaceableComponent("views.audio_messages.AudioPlayer") @replaceableComponent("views.audio_messages.AudioPlayer")
export default class AudioPlayer extends AudioPlayerBase { export default class AudioPlayer extends AudioPlayerBase {
@ -32,18 +33,29 @@ export default class AudioPlayer extends AudioPlayerBase {
private seekRef: RefObject<SeekBar> = createRef(); private seekRef: RefObject<SeekBar> = createRef();
private onKeyDown = (ev: React.KeyboardEvent) => { private onKeyDown = (ev: React.KeyboardEvent) => {
let handled = true;
const action = getKeyBindingsManager().getAccessibilityAction(ev);
switch (action) {
case KeyBindingAction.Space:
this.playPauseRef.current?.toggleState();
break;
case KeyBindingAction.ArrowLeft:
this.seekRef.current?.left();
break;
case KeyBindingAction.ArrowRight:
this.seekRef.current?.right();
break;
default:
handled = false;
break;
}
// stopPropagation() prevents the FocusComposer catch-all from triggering, // stopPropagation() prevents the FocusComposer catch-all from triggering,
// but we need to do it on key down instead of press (even though the user // but we need to do it on key down instead of press (even though the user
// interaction is typically on press). // interaction is typically on press).
if (ev.key === Key.SPACE) { if (handled) {
ev.stopPropagation(); ev.stopPropagation();
this.playPauseRef.current?.toggleState();
} else if (ev.key === Key.ARROW_LEFT) {
ev.stopPropagation();
this.seekRef.current?.left();
} else if (ev.key === Key.ARROW_RIGHT) {
ev.stopPropagation();
this.seekRef.current?.right();
} }
}; };

View file

@ -31,7 +31,6 @@ import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import RoomListActions from "../../../actions/RoomListActions"; import RoomListActions from "../../../actions/RoomListActions";
import { Key } from "../../../Keyboard";
import { EchoChamber } from "../../../stores/local-echo/EchoChamber"; import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
import { RoomNotifState } from "../../../RoomNotifs"; import { RoomNotifState } from "../../../RoomNotifs";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
@ -47,6 +46,8 @@ import DMRoomMap from "../../../utils/DMRoomMap";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import PosthogTrackers from "../../../PosthogTrackers"; import PosthogTrackers from "../../../PosthogTrackers";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IProps extends IContextMenuProps { interface IProps extends IContextMenuProps {
room: Room; room: Room;
@ -267,9 +268,12 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
logger.warn(`Unexpected tag ${tagId} applied to ${room.roomId}`); logger.warn(`Unexpected tag ${tagId} applied to ${room.roomId}`);
} }
if ((ev as React.KeyboardEvent).key === Key.ENTER) { const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12 switch (action) {
onFinished(); case KeyBindingAction.Enter:
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
onFinished();
break;
} }
}; };

View file

@ -30,7 +30,6 @@ import * as Email from '../../../email';
import IdentityAuthClient from '../../../IdentityAuthClient'; import IdentityAuthClient from '../../../IdentityAuthClient';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
import { abbreviateUrl } from '../../../utils/UrlUtils'; import { abbreviateUrl } from '../../../utils/UrlUtils';
import { Key } from "../../../Keyboard";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import AddressSelector from '../elements/AddressSelector'; import AddressSelector from '../elements/AddressSelector';
@ -38,6 +37,8 @@ import AddressTile from '../elements/AddressTile';
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
const TRUNCATE_QUERY_LIST = 40; const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -167,40 +168,38 @@ export default class AddressPickerDialog extends React.Component<IProps, IState>
private onKeyDown = (e: React.KeyboardEvent): void => { private onKeyDown = (e: React.KeyboardEvent): void => {
const textInput = this.textinput.current ? this.textinput.current.value : undefined; const textInput = this.textinput.current ? this.textinput.current.value : undefined;
let handled = true;
const action = getKeyBindingsManager().getAccessibilityAction(e);
if (e.key === Key.ESCAPE) { if (action === KeyBindingAction.Escape) {
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false); this.props.onFinished(false);
} else if (e.key === Key.ARROW_UP) { } else if (e.key === KeyBindingAction.ArrowUp) {
e.stopPropagation(); this.addressSelector.current?.moveSelectionUp();
e.preventDefault(); } else if (e.key === KeyBindingAction.ArrowDown) {
if (this.addressSelector.current) this.addressSelector.current.moveSelectionUp(); this.addressSelector.current?.moveSelectionDown();
} else if (e.key === Key.ARROW_DOWN) { } else if (
e.stopPropagation(); [KeyBindingAction.Comma, KeyBindingAction.Enter, KeyBindingAction.Tab].includes(action) &&
e.preventDefault(); this.state.suggestedList.length > 0
if (this.addressSelector.current) this.addressSelector.current.moveSelectionDown(); ) {
} else if (this.state.suggestedList.length > 0 && [Key.COMMA, Key.ENTER, Key.TAB].includes(e.key)) { this.addressSelector.current?.chooseSelection();
e.stopPropagation(); } else if (textInput.length === 0 && this.state.selectedList.length && action === KeyBindingAction.Backspace) {
e.preventDefault();
if (this.addressSelector.current) this.addressSelector.current.chooseSelection();
} else if (textInput.length === 0 && this.state.selectedList.length && e.key === Key.BACKSPACE) {
e.stopPropagation();
e.preventDefault();
this.onDismissed(this.state.selectedList.length - 1)(); this.onDismissed(this.state.selectedList.length - 1)();
} else if (e.key === Key.ENTER) { } else if (e.key === KeyBindingAction.Enter) {
e.stopPropagation();
e.preventDefault();
if (textInput === '') { if (textInput === '') {
// if there's nothing in the input box, submit the form // if there's nothing in the input box, submit the form
this.onButtonClick(); this.onButtonClick();
} else { } else {
this.addAddressesToList([textInput]); this.addAddressesToList([textInput]);
} }
} else if (textInput && (e.key === Key.COMMA || e.key === Key.TAB)) { } else if (textInput && [KeyBindingAction.Comma, KeyBindingAction.Tab].includes(action)) {
this.addAddressesToList([textInput]);
} else {
handled = false;
}
if (handled) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.addAddressesToList([textInput]);
} }
}; };

View file

@ -21,7 +21,6 @@ import FocusLock from 'react-focus-lock';
import classNames from 'classnames'; import classNames from 'classnames';
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { Key } from '../../../Keyboard';
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton'; import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
@ -30,6 +29,8 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import Heading from '../typography/Heading'; import Heading from '../typography/Heading';
import { IDialogProps } from "./IDialogProps"; import { IDialogProps } from "./IDialogProps";
import { PosthogScreenTracker, ScreenName } from "../../../PosthogTrackers"; import { PosthogScreenTracker, ScreenName } from "../../../PosthogTrackers";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
// Whether the dialog should have a 'close' button that will // Whether the dialog should have a 'close' button that will
@ -99,10 +100,16 @@ export default class BaseDialog extends React.Component<IProps> {
if (this.props.onKeyDown) { if (this.props.onKeyDown) {
this.props.onKeyDown(e); this.props.onKeyDown(e);
} }
if (this.props.hasCancel && e.key === Key.ESCAPE) {
e.stopPropagation(); const action = getKeyBindingsManager().getAccessibilityAction(e);
e.preventDefault(); switch (action) {
this.props.onFinished(false); case KeyBindingAction.Escape:
if (!this.props.hasCancel) break;
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
break;
} }
}; };

View file

@ -23,7 +23,6 @@ import SdkConfig from '../../../SdkConfig';
import withValidation, { IFieldState } from '../elements/Validation'; import withValidation, { IFieldState } from '../elements/Validation';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { Key } from "../../../Keyboard";
import { IOpts, privateShouldBeEncrypted } from "../../../createRoom"; import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -34,6 +33,8 @@ import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog"; import BaseDialog from "../dialogs/BaseDialog";
import SpaceStore from "../../../stores/spaces/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import JoinRuleDropdown from "../elements/JoinRuleDropdown"; import JoinRuleDropdown from "../elements/JoinRuleDropdown";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IProps { interface IProps {
defaultPublic?: boolean; defaultPublic?: boolean;
@ -136,10 +137,13 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
} }
private onKeyDown = (event: KeyboardEvent) => { private onKeyDown = (event: KeyboardEvent) => {
if (event.key === Key.ENTER) { const action = getKeyBindingsManager().getAccessibilityAction(event);
this.onOk(); switch (action) {
event.preventDefault(); case KeyBindingAction.Enter:
event.stopPropagation(); this.onOk();
event.preventDefault();
event.stopPropagation();
break;
} }
}; };

View file

@ -45,7 +45,6 @@ import {
showAnyInviteErrors, showAnyInviteErrors,
showCommunityInviteDialog, showCommunityInviteDialog,
} from "../../../RoomInvite"; } from "../../../RoomInvite";
import { Key } from "../../../Keyboard";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { DefaultTagID } from "../../../stores/room-list/models"; import { DefaultTagID } from "../../../stores/room-list/models";
import RoomListStore from "../../../stores/room-list/RoomListStore"; import RoomListStore from "../../../stores/room-list/RoomListStore";
@ -71,6 +70,8 @@ import UserIdentifierCustomisations from '../../../customisations/UserIdentifier
import CopyableText from "../elements/CopyableText"; import CopyableText from "../elements/CopyableText";
import { ScreenName } from '../../../PosthogTrackers'; import { ScreenName } from '../../../PosthogTrackers';
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here. // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -803,20 +804,36 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
private onKeyDown = (e) => { private onKeyDown = (e) => {
if (this.state.busy) return; if (this.state.busy) return;
let handled = true;
const value = e.target.value.trim(); const value = e.target.value.trim();
const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey; const action = getKeyBindingsManager().getAccessibilityAction(e);
if (!value && this.state.targets.length > 0 && e.key === Key.BACKSPACE && !hasModifiers) {
// when the field is empty and the user hits backspace remove the right-most target switch (action) {
case KeyBindingAction.Backspace:
if (value || this.state.targets.length <= 0) break;
// when the field is empty and the user hits backspace remove the right-most target
this.removeMember(this.state.targets[this.state.targets.length - 1]);
break;
case KeyBindingAction.Space:
if (!value || !value.includes("@") || value.includes(" ")) break;
// when the user hits space and their input looks like an e-mail/MXID then try to convert it
this.convertFilter();
break;
case KeyBindingAction.Enter:
if (!value) break;
// when the user hits enter with something in their field try to convert it
this.convertFilter();
break;
default:
handled = false;
}
if (handled) {
e.preventDefault(); e.preventDefault();
this.removeMember(this.state.targets[this.state.targets.length - 1]);
} else if (value && e.key === Key.ENTER && !hasModifiers) {
// when the user hits enter with something in their field try to convert it
e.preventDefault();
this.convertFilter();
} else if (value && e.key === Key.SPACE && !hasModifiers && value.includes("@") && !value.includes(" ")) {
// when the user hits space and their input looks like an e-mail/MXID then try to convert it
e.preventDefault();
this.convertFilter();
} }
}; };

View file

@ -19,11 +19,12 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import FocusLock from "react-focus-lock"; import FocusLock from "react-focus-lock";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { Key } from "../../../Keyboard";
import { IDialogProps } from "./IDialogProps"; import { IDialogProps } from "./IDialogProps";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
export interface IScrollableBaseState { export interface IScrollableBaseState {
canSubmit: boolean; canSubmit: boolean;
@ -45,10 +46,13 @@ export default abstract class ScrollableBaseModal<TProps extends IDialogProps, T
} }
private onKeyDown = (e: KeyboardEvent | React.KeyboardEvent): void => { private onKeyDown = (e: KeyboardEvent | React.KeyboardEvent): void => {
if (e.key === Key.ESCAPE) { const action = getKeyBindingsManager().getAccessibilityAction(e);
e.stopPropagation(); switch (action) {
e.preventDefault(); case KeyBindingAction.Escape:
this.cancel(); e.stopPropagation();
e.preventDefault();
this.cancel();
break;
} }
}; };

View file

@ -46,7 +46,6 @@ import {
Type, Type,
useRovingTabIndex, useRovingTabIndex,
} from "../../../accessibility/RovingTabIndex"; } from "../../../accessibility/RovingTabIndex";
import { Key } from "../../../Keyboard";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import SpaceStore from "../../../stores/spaces/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
@ -596,23 +595,33 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
} }
const onDialogKeyDown = (ev: KeyboardEvent) => { const onDialogKeyDown = (ev: KeyboardEvent) => {
const navAction = getKeyBindingsManager().getNavigationAction(ev); const navigationAction = getKeyBindingsManager().getNavigationAction(ev);
switch (navAction) { switch (navigationAction) {
case "KeyBinding.closeDialogOrContextMenu" as KeyBindingAction:
case KeyBindingAction.FilterRooms: case KeyBindingAction.FilterRooms:
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
onFinished(); onFinished();
break; break;
} }
const accessibilityAction = getKeyBindingsManager().getAccessibilityAction(ev);
switch (accessibilityAction) {
case KeyBindingAction.Escape:
ev.stopPropagation();
ev.preventDefault();
onFinished();
break;
}
}; };
const onKeyDown = (ev: KeyboardEvent) => { const onKeyDown = (ev: KeyboardEvent) => {
let ref: RefObject<HTMLElement>; let ref: RefObject<HTMLElement>;
switch (ev.key) { const action = getKeyBindingsManager().getAccessibilityAction(ev);
case Key.ARROW_UP:
case Key.ARROW_DOWN: switch (action) {
case KeyBindingAction.ArrowUp:
case KeyBindingAction.ArrowDown:
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
@ -629,12 +638,12 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
} }
const idx = refs.indexOf(rovingContext.state.activeRef); const idx = refs.indexOf(rovingContext.state.activeRef);
ref = findSiblingElement(refs, idx + (ev.key === Key.ARROW_UP ? -1 : 1)); ref = findSiblingElement(refs, idx + (action === KeyBindingAction.ArrowUp ? -1 : 1));
} }
break; break;
case Key.ARROW_LEFT: case KeyBindingAction.ArrowLeft:
case Key.ARROW_RIGHT: case KeyBindingAction.ArrowRight:
// only handle these keys when we are in the recently viewed row of options // only handle these keys when we are in the recently viewed row of options
if (!query && if (!query &&
rovingContext.state.refs.length > 0 && rovingContext.state.refs.length > 0 &&
@ -646,11 +655,10 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
const refs = rovingContext.state.refs.filter(refIsForRecentlyViewed); const refs = rovingContext.state.refs.filter(refIsForRecentlyViewed);
const idx = refs.indexOf(rovingContext.state.activeRef); const idx = refs.indexOf(rovingContext.state.activeRef);
ref = findSiblingElement(refs, idx + (ev.key === Key.ARROW_LEFT ? -1 : 1)); ref = findSiblingElement(refs, idx + (action === KeyBindingAction.ArrowLeft ? -1 : 1));
} }
break; break;
case KeyBindingAction.Enter:
case Key.ENTER:
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
rovingContext.state.activeRef?.current?.click(); rovingContext.state.activeRef?.current?.click();

View file

@ -17,7 +17,8 @@
import React, { ReactHTML } from 'react'; import React, { ReactHTML } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { Key } from '../../../Keyboard'; import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element> | React.FormEvent<Element>; export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element> | React.FormEvent<Element>;
@ -92,29 +93,36 @@ export default function AccessibleButton({
// Browsers handle space and enter keypresses differently and we are only adjusting to the // Browsers handle space and enter keypresses differently and we are only adjusting to the
// inconsistencies here // inconsistencies here
newProps.onKeyDown = (e) => { newProps.onKeyDown = (e) => {
if (e.key === Key.ENTER) { const action = getKeyBindingsManager().getAccessibilityAction(e);
e.stopPropagation();
e.preventDefault(); switch (action) {
return onClick(e); case KeyBindingAction.Enter:
} e.stopPropagation();
if (e.key === Key.SPACE) { e.preventDefault();
e.stopPropagation(); return onClick(e);
e.preventDefault(); case KeyBindingAction.Space:
} else { e.stopPropagation();
onKeyDown?.(e); e.preventDefault();
break;
default:
onKeyDown?.(e);
} }
}; };
newProps.onKeyUp = (e) => { newProps.onKeyUp = (e) => {
if (e.key === Key.SPACE) { const action = getKeyBindingsManager().getAccessibilityAction(e);
e.stopPropagation();
e.preventDefault(); switch (action) {
return onClick(e); case KeyBindingAction.Enter:
} e.stopPropagation();
if (e.key === Key.ENTER) { e.preventDefault();
e.stopPropagation(); break;
e.preventDefault(); case KeyBindingAction.Space:
} else { e.stopPropagation();
onKeyUp?.(e); e.preventDefault();
return onClick(e);
default:
onKeyUp?.(e);
break;
} }
}; };
} }

View file

@ -20,8 +20,9 @@ import classnames from 'classnames';
import AccessibleButton, { ButtonEvent } from './AccessibleButton'; import AccessibleButton, { ButtonEvent } from './AccessibleButton';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IMenuOptionProps { interface IMenuOptionProps {
children: ReactElement; children: ReactElement;
@ -181,10 +182,12 @@ export default class Dropdown extends React.Component<IProps, IState> {
private onAccessibleButtonClick = (ev: ButtonEvent) => { private onAccessibleButtonClick = (ev: ButtonEvent) => {
if (this.props.disabled) return; if (this.props.disabled) return;
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
if (!this.state.expanded) { if (!this.state.expanded) {
this.setState({ expanded: true }); this.setState({ expanded: true });
ev.preventDefault(); ev.preventDefault();
} else if ((ev as React.KeyboardEvent).key === Key.ENTER) { } else if (action === KeyBindingAction.Enter) {
// the accessible button consumes enter onKeyDown for firing onClick, so handle it here // the accessible button consumes enter onKeyDown for firing onClick, so handle it here
this.props.onOptionChange(this.state.highlightedOption); this.props.onOptionChange(this.state.highlightedOption);
this.close(); this.close();
@ -214,14 +217,15 @@ export default class Dropdown extends React.Component<IProps, IState> {
let handled = true; let handled = true;
// These keys don't generate keypress events and so needs to be on keyup // These keys don't generate keypress events and so needs to be on keyup
switch (e.key) { const action = getKeyBindingsManager().getAccessibilityAction(e);
case Key.ENTER: switch (action) {
case KeyBindingAction.Enter:
this.props.onOptionChange(this.state.highlightedOption); this.props.onOptionChange(this.state.highlightedOption);
// fallthrough // fallthrough
case Key.ESCAPE: case KeyBindingAction.Escape:
this.close(); this.close();
break; break;
case Key.ARROW_DOWN: case KeyBindingAction.ArrowDown:
if (this.state.expanded) { if (this.state.expanded) {
this.setState({ this.setState({
highlightedOption: this.nextOption(this.state.highlightedOption), highlightedOption: this.nextOption(this.state.highlightedOption),
@ -230,7 +234,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
this.setState({ expanded: true }); this.setState({ expanded: true });
} }
break; break;
case Key.ARROW_UP: case KeyBindingAction.ArrowUp:
if (this.state.expanded) { if (this.state.expanded) {
this.setState({ this.setState({
highlightedOption: this.prevOption(this.state.highlightedOption), highlightedOption: this.prevOption(this.state.highlightedOption),

View file

@ -17,7 +17,8 @@ limitations under the License.
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import { Key } from "../../../Keyboard"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
enum Phases { enum Phases {
@ -124,9 +125,12 @@ export default class EditableText extends React.Component<IProps, IState> {
this.showPlaceholder(false); this.showPlaceholder(false);
} }
if (ev.key === Key.ENTER) { const action = getKeyBindingsManager().getAccessibilityAction(ev);
ev.stopPropagation(); switch (action) {
ev.preventDefault(); case KeyBindingAction.Enter:
ev.stopPropagation();
ev.preventDefault();
break;
} }
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
@ -141,10 +145,14 @@ export default class EditableText extends React.Component<IProps, IState> {
this.value = (ev.target as HTMLDivElement).textContent; this.value = (ev.target as HTMLDivElement).textContent;
} }
if (ev.key === Key.ENTER) { const action = getKeyBindingsManager().getAccessibilityAction(ev);
this.onFinish(ev); switch (action) {
} else if (ev.key === Key.ESCAPE) { case KeyBindingAction.Escape:
this.cancelEdit(); this.cancelEdit();
break;
case KeyBindingAction.Enter:
this.onFinish(ev);
break;
} }
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
@ -179,7 +187,8 @@ export default class EditableText extends React.Component<IProps, IState> {
): void => { ): void => {
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this; const self = this;
const submit = ("key" in ev && ev.key === Key.ENTER) || shouldSubmit; const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
const submit = action === KeyBindingAction.Enter || shouldSubmit;
this.setState({ this.setState({
phase: Phases.Display, phase: Phases.Display,
}, () => { }, () => {

View file

@ -22,7 +22,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import AccessibleTooltipButton from "./AccessibleTooltipButton"; import AccessibleTooltipButton from "./AccessibleTooltipButton";
import { Key } from "../../../Keyboard";
import MemberAvatar from "../avatars/MemberAvatar"; import MemberAvatar from "../avatars/MemberAvatar";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import MessageContextMenu from "../context_menus/MessageContextMenu"; import MessageContextMenu from "../context_menus/MessageContextMenu";
@ -38,6 +37,8 @@ import { normalizeWheelEvent } from "../../../utils/Mouse";
import { IDialogProps } from '../dialogs/IDialogProps'; import { IDialogProps } from '../dialogs/IDialogProps';
import UIStore from '../../../stores/UIStore'; import UIStore from '../../../stores/UIStore';
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
// Max scale to keep gaps around the image // Max scale to keep gaps around the image
const MAX_SCALE = 0.95; const MAX_SCALE = 0.95;
@ -292,10 +293,13 @@ export default class ImageView extends React.Component<IProps, IState> {
}; };
private onKeyDown = (ev: KeyboardEvent) => { private onKeyDown = (ev: KeyboardEvent) => {
if (ev.key === Key.ESCAPE) { const action = getKeyBindingsManager().getAccessibilityAction(ev);
ev.stopPropagation(); switch (action) {
ev.preventDefault(); case KeyBindingAction.Escape:
this.props.onFinished(); ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
break;
} }
}; };

View file

@ -19,8 +19,9 @@ import React from 'react';
import * as Roles from '../../../Roles'; import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Field from "./Field"; import Field from "./Field";
import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
const CUSTOM_VALUE = "SELECT_VALUE_CUSTOM"; const CUSTOM_VALUE = "SELECT_VALUE_CUSTOM";
@ -130,16 +131,19 @@ export default class PowerSelector extends React.Component<IProps, IState> {
}; };
private onCustomKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => { private onCustomKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
if (event.key === Key.ENTER) { const action = getKeyBindingsManager().getAccessibilityAction(event);
event.preventDefault(); switch (action) {
event.stopPropagation(); case KeyBindingAction.Enter:
event.preventDefault();
event.stopPropagation();
// Do not call the onChange handler directly here - it can cause an infinite loop. // Do not call the onChange handler directly here - it can cause an infinite loop.
// Long story short, a user hits Enter to submit the value which onChange handles as // Long story short, a user hits Enter to submit the value which onChange handles as
// raising a dialog which causes a blur which causes a dialog which causes a blur and // raising a dialog which causes a blur which causes a dialog which causes a blur and
// so on. By not causing the onChange to be called here, we avoid the loop because we // so on. By not causing the onChange to be called here, we avoid the loop because we
// handle the onBlur safely. // handle the onBlur safely.
(event.target as HTMLInputElement).blur(); (event.target as HTMLInputElement).blur();
break;
} }
}; };

View file

@ -19,9 +19,10 @@ import React from 'react';
import classNames from "classnames"; import classNames from "classnames";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { Key } from "../../../Keyboard";
import { CategoryKey, ICategory } from "./Category"; import { CategoryKey, ICategory } from "./Category";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IProps { interface IProps {
categories: ICategory[]; categories: ICategory[];
@ -57,18 +58,20 @@ class Header extends React.PureComponent<IProps> {
// https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html // https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html
private onKeyDown = (ev: React.KeyboardEvent) => { private onKeyDown = (ev: React.KeyboardEvent) => {
let handled = true; let handled = true;
switch (ev.key) {
case Key.ARROW_LEFT: const action = getKeyBindingsManager().getAccessibilityAction(ev);
switch (action) {
case KeyBindingAction.ArrowLeft:
this.changeCategoryRelative(-1); this.changeCategoryRelative(-1);
break; break;
case Key.ARROW_RIGHT: case KeyBindingAction.ArrowRight:
this.changeCategoryRelative(1); this.changeCategoryRelative(1);
break; break;
case Key.HOME: case KeyBindingAction.Home:
this.changeCategoryAbsolute(0); this.changeCategoryAbsolute(0);
break; break;
case Key.END: case KeyBindingAction.End:
this.changeCategoryAbsolute(this.props.categories.length - 1, -1); this.changeCategoryAbsolute(this.props.categories.length - 1, -1);
break; break;
default: default:

View file

@ -18,8 +18,9 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
interface IProps { interface IProps {
query: string; query: string;
@ -37,10 +38,13 @@ class Search extends React.PureComponent<IProps> {
} }
private onKeyDown = (ev: React.KeyboardEvent) => { private onKeyDown = (ev: React.KeyboardEvent) => {
if (ev.key === Key.ENTER) { const action = getKeyBindingsManager().getAccessibilityAction(ev);
this.props.onEnter(); switch (action) {
ev.stopPropagation(); case KeyBindingAction.Enter:
ev.preventDefault(); this.props.onEnter();
ev.stopPropagation();
ev.preventDefault();
break;
} }
}; };

View file

@ -480,6 +480,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
} }
const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event); const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event);
const accessibilityAction = getKeyBindingsManager().getAccessibilityAction(event);
if (model.autoComplete?.hasCompletions()) { if (model.autoComplete?.hasCompletions()) {
const autoComplete = model.autoComplete; const autoComplete = model.autoComplete;
switch (autocompleteAction) { switch (autocompleteAction) {
@ -509,7 +510,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
// there is no current autocomplete window, try to open it // there is no current autocomplete window, try to open it
this.tabCompleteName(); this.tabCompleteName();
handled = true; handled = true;
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { } else if ([KeyBindingAction.Delete, KeyBindingAction.Backspace].includes(accessibilityAction)) {
this.formatBarRef.current.hide(); this.formatBarRef.current.hide();
} }

View file

@ -44,7 +44,6 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import NotificationBadge from "./NotificationBadge"; import NotificationBadge from "./NotificationBadge";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { Key } from "../../../Keyboard";
import { ActionPayload } from "../../../dispatcher/payloads"; import { ActionPayload } from "../../../dispatcher/payloads";
import { polyfillTouchEvent } from "../../../@types/polyfill"; import { polyfillTouchEvent } from "../../../@types/polyfill";
import ResizeNotifier from "../../../utils/ResizeNotifier"; import ResizeNotifier from "../../../utils/ResizeNotifier";
@ -503,14 +502,15 @@ export default class RoomSublist extends React.Component<IProps, IState> {
}; };
private onKeyDown = (ev: React.KeyboardEvent) => { private onKeyDown = (ev: React.KeyboardEvent) => {
switch (ev.key) { const action = getKeyBindingsManager().getAccessibilityAction(ev);
// On ARROW_LEFT go to the sublist header switch (action) {
case Key.ARROW_LEFT: // On ArrowLeft go to the sublist header
case KeyBindingAction.ArrowLeft:
ev.stopPropagation(); ev.stopPropagation();
this.headerButton.current.focus(); this.headerButton.current.focus();
break; break;
// Consume ARROW_RIGHT so it doesn't cause focus to get sent to composer // Consume ArrowRight so it doesn't cause focus to get sent to composer
case Key.ARROW_RIGHT: case KeyBindingAction.ArrowRight:
ev.stopPropagation(); ev.stopPropagation();
} }
}; };

View file

@ -25,7 +25,6 @@ import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleBu
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import defaultDispatcher from '../../../dispatcher/dispatcher'; import defaultDispatcher from '../../../dispatcher/dispatcher';
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { Key } from "../../../Keyboard";
import ActiveRoomObserver from "../../../ActiveRoomObserver"; import ActiveRoomObserver from "../../../ActiveRoomObserver";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu"; import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
@ -54,6 +53,8 @@ import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/Community
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import PosthogTrackers from "../../../PosthogTrackers"; import PosthogTrackers from "../../../PosthogTrackers";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
interface IProps { interface IProps {
room: Room; room: Room;
@ -240,11 +241,14 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
private onTileClick = (ev: React.KeyboardEvent) => { private onTileClick = (ev: React.KeyboardEvent) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
const action = getKeyBindingsManager().getAccessibilityAction(ev);
dis.dispatch<ViewRoomPayload>({ dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom, action: Action.ViewRoom,
show_room_tile: true, // make sure the room is visible in the list show_room_tile: true, // make sure the room is visible in the list
room_id: this.props.room.roomId, room_id: this.props.room.roomId,
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)), clear_search: [KeyBindingAction.Enter, KeyBindingAction.Space].includes(action),
metricsTrigger: "RoomList", metricsTrigger: "RoomList",
metricsViaKeyboard: ev.type !== "click", metricsViaKeyboard: ev.type !== "click",
}); });
@ -313,9 +317,12 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
logger.warn(`Unexpected tag ${tagId} applied to ${this.props.room.roomId}`); logger.warn(`Unexpected tag ${tagId} applied to ${this.props.room.roomId}`);
} }
if ((ev as React.KeyboardEvent).key === Key.ENTER) { const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12 switch (action) {
this.setState({ generalMenuPosition: null }); // hide the menu case KeyBindingAction.Enter:
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
this.setState({ generalMenuPosition: null }); // hide the menu
break;
} }
}; };
@ -387,10 +394,12 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.roomProps.notificationVolume = newState; this.roomProps.notificationVolume = newState;
const key = (ev as React.KeyboardEvent).key; const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
if (key === Key.ENTER) { switch (action) {
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12 case KeyBindingAction.Enter:
this.setState({ notificationsMenuPosition: null }); // hide the menu // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
this.setState({ notificationsMenuPosition: null }); // hide the menu
break;
} }
} }

View file

@ -20,10 +20,11 @@ import classNames from "classnames";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { Key } from "../../../Keyboard";
import DesktopBuildsNotice, { WarningKind } from "../elements/DesktopBuildsNotice"; import DesktopBuildsNotice, { WarningKind } from "../elements/DesktopBuildsNotice";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { PosthogScreenTracker } from '../../../PosthogTrackers'; import { PosthogScreenTracker } from '../../../PosthogTrackers';
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IProps { interface IProps {
onCancelClick: () => void; onCancelClick: () => void;
@ -61,11 +62,12 @@ export default class SearchBar extends React.Component<IProps, IState> {
}; };
private onSearchChange = (e: React.KeyboardEvent) => { private onSearchChange = (e: React.KeyboardEvent) => {
switch (e.key) { const action = getKeyBindingsManager().getAccessibilityAction(e);
case Key.ENTER: switch (action) {
case KeyBindingAction.Enter:
this.onSearch(); this.onSearch();
break; break;
case Key.ESCAPE: case KeyBindingAction.Escape:
this.props.onCancelClick(); this.props.onCancelClick();
break; break;
} }

View file

@ -18,10 +18,11 @@ import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ActionPayload } from '../../../dispatcher/payloads'; import { ActionPayload } from '../../../dispatcher/payloads';
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IProps { interface IProps {
// false to display an error saying that we couldn't connect to the integration manager // false to display an error saying that we couldn't connect to the integration manager
@ -65,10 +66,13 @@ export default class IntegrationManager extends React.Component<IProps, IState>
} }
private onKeyDown = (ev: KeyboardEvent): void => { private onKeyDown = (ev: KeyboardEvent): void => {
if (ev.key === Key.ESCAPE) { const action = getKeyBindingsManager().getAccessibilityAction(ev);
ev.stopPropagation(); switch (action) {
ev.preventDefault(); case KeyBindingAction.Escape:
this.props.onFinished(); ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
break;
} }
}; };

View file

@ -35,8 +35,8 @@ const getKeyboardShortcutValue = (name: string): KeyBindingConfig => {
return getKeyboardShortcuts()[name]?.default; return getKeyboardShortcuts()[name]?.default;
}; };
const getKeyboardShortcutDisplayName = (name: string): string => { const getKeyboardShortcutDisplayName = (name: string): string | null => {
const keyboardShortcutDisplayName = getKeyboardShortcuts()[name]?.displayName as string; const keyboardShortcutDisplayName = getKeyboardShortcuts()[name]?.displayName;
return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName); return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName);
}; };
@ -93,8 +93,11 @@ const visibleCategories = Object.entries(CATEGORIES).filter(([categoryName]) =>
categoryName !== CategoryName.LABS || SdkConfig.get()['showLabsSettings']); categoryName !== CategoryName.LABS || SdkConfig.get()['showLabsSettings']);
const KeyboardShortcutRow: React.FC<IKeyboardShortcutRowProps> = ({ name }) => { const KeyboardShortcutRow: React.FC<IKeyboardShortcutRowProps> = ({ name }) => {
const displayName = getKeyboardShortcutDisplayName(name);
if (!displayName) return null;
return <div className="mx_KeyboardShortcut_shortcutRow"> return <div className="mx_KeyboardShortcut_shortcutRow">
{ getKeyboardShortcutDisplayName(name) } { displayName }
<KeyboardShortcut name={name} /> <KeyboardShortcut name={name} />
</div>; </div>;
}; };
@ -105,6 +108,8 @@ interface IKeyboardShortcutSectionProps {
} }
const KeyboardShortcutSection: React.FC<IKeyboardShortcutSectionProps> = ({ categoryName, category }) => { const KeyboardShortcutSection: React.FC<IKeyboardShortcutSectionProps> = ({ categoryName, category }) => {
if (!category.categoryLabel) return null;
return <div className="mx_SettingsTab_section" key={categoryName}> return <div className="mx_SettingsTab_section" key={categoryName}>
<div className="mx_SettingsTab_subheading">{ _t(category.categoryLabel) }</div> <div className="mx_SettingsTab_subheading">{ _t(category.categoryLabel) }</div>
<div> { category.settingNames.map((shortcutName) => { <div> { category.settingNames.map((shortcutName) => {

View file

@ -38,7 +38,8 @@ import SettingsStore from "../../../settings/SettingsStore";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { UserTab } from "../dialogs/UserSettingsDialog"; import { UserTab } from "../dialogs/UserSettingsDialog";
import { Key } from "../../../Keyboard"; import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
export const createSpace = async ( export const createSpace = async (
name: string, name: string,
@ -159,8 +160,11 @@ export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
const domain = cli.getDomain(); const domain = cli.getDomain();
const onKeyDown = (ev: KeyboardEvent) => { const onKeyDown = (ev: KeyboardEvent) => {
if (ev.key === Key.ENTER) { const action = getKeyBindingsManager().getAccessibilityAction(ev);
onSubmit(ev); switch (action) {
case KeyBindingAction.Enter:
onSubmit(ev);
break;
} }
}; };

View file

@ -3404,6 +3404,7 @@
"[number]": "[number]", "[number]": "[number]",
"Calls": "Calls", "Calls": "Calls",
"Room List": "Room List", "Room List": "Room List",
"Accessibility": "Accessibility",
"Navigation": "Navigation", "Navigation": "Navigation",
"Autocomplete": "Autocomplete", "Autocomplete": "Autocomplete",
"Toggle Bold": "Toggle Bold", "Toggle Bold": "Toggle Bold",
@ -3451,9 +3452,9 @@
"Next recently visited room or community": "Next recently visited room or community", "Next recently visited room or community": "Next recently visited room or community",
"Switch to space by number": "Switch to space by number", "Switch to space by number": "Switch to space by number",
"Open user settings": "Open user settings", "Open user settings": "Open user settings",
"Close dialog or context menu": "Close dialog or context menu",
"Activate selected button": "Activate selected button",
"New line": "New line", "New line": "New line",
"Force complete": "Force complete", "Force complete": "Force complete",
"Search (must be enabled)": "Search (must be enabled)", "Search (must be enabled)": "Search (must be enabled)"
"Close dialog or context menu": "Close dialog or context menu",
"Activate selected button": "Activate selected button"
} }

View file

@ -28,6 +28,7 @@ import { findById } from '../../../test-utils';
import { SettingLevel } from '../../../../src/settings/SettingLevel'; import { SettingLevel } from '../../../../src/settings/SettingLevel';
import dis from '../../../../src/dispatcher/dispatcher'; import dis from '../../../../src/dispatcher/dispatcher';
import { Action } from '../../../../src/dispatcher/actions'; import { Action } from '../../../../src/dispatcher/actions';
import PlatformPeg from "../../../../src/PlatformPeg";
jest.mock('../../../../src/theme'); jest.mock('../../../../src/theme');
jest.mock('../../../../src/components/views/settings/ThemeChoicePanel', () => ({ jest.mock('../../../../src/components/views/settings/ThemeChoicePanel', () => ({
@ -44,6 +45,8 @@ jest.mock('../../../../src/dispatcher/dispatcher', () => ({
register: jest.fn(), register: jest.fn(),
})); }));
PlatformPeg.get = () => ({ overrideBrowserShortcuts: () => false });
describe('<QuickThemeSwitcher />', () => { describe('<QuickThemeSwitcher />', () => {
const defaultProps = { const defaultProps = {
requestClose: jest.fn(), requestClose: jest.fn(),