Make everything use the `KeyBindingManager` (#7907)

pull/21833/head
Š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 bindings = getBindingsByCategory(CategoryName.NAVIGATION);
return getBindingsByCategory(CategoryName.NAVIGATION);
};
bindings.push({
action: KeyBindingAction.CloseDialogOrContextMenu,
keyCombo: {
key: Key.ESCAPE,
},
});
return bindings;
const accessibilityBindings = (): KeyBinding[] => {
return getBindingsByCategory(CategoryName.ACCESSIBILITY);
};
const callBindings = (): KeyBinding[] => {
@ -177,6 +172,7 @@ export const defaultBindingsProvider: IKeyBindingsProvider = {
getRoomListBindings: roomListBindings,
getRoomBindings: roomBindings,
getNavigationBindings: navigationBindings,
getAccessibilityBindings: accessibilityBindings,
getCallBindings: callBindings,
getLabsBindings: labsBindings,
};

View File

@ -155,6 +155,10 @@ export class KeyBindingsManager {
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 {
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 */
ToggleWebcamInCall = "KeyBinding.toggleWebcamInCall",
/** Closes a dialog or a context menu */
CloseDialogOrContextMenu = "KeyBinding.closeDialogOrContextMenu",
/** Clicks the selected button */
ActivateSelectedButton = "KeyBinding.activateSelectedButton",
/** Accessibility actions */
Escape = "KeyBinding.escape",
Enter = "KeyBinding.enter",
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 */
ToggleHiddenEventVisibility = 'KeyBinding.toggleHiddenEventVisibility',
@ -156,13 +166,14 @@ type IKeyboardShortcuts = {
};
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
settingNames: (KeyBindingAction)[];
}
export enum CategoryName {
NAVIGATION = "Navigation",
ACCESSIBILITY = "Accessibility",
CALLS = "Calls",
COMPOSER = "Composer",
ROOM_LIST = "Room List",
@ -245,12 +256,26 @@ export const CATEGORIES: Record<CategoryName, ICategory> = {
KeyBindingAction.NextRoom,
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]: {
categoryLabel: _td("Navigation"),
settingNames: [
KeyBindingAction.ToggleUserMenu,
KeyBindingAction.CloseDialogOrContextMenu,
KeyBindingAction.ActivateSelectedButton,
KeyBindingAction.ToggleRoomSidePanel,
KeyBindingAction.ToggleSpacePanel,
KeyBindingAction.ShowKeyboardSettings,
@ -611,6 +636,68 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = {
},
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
@ -651,18 +738,6 @@ const getNonCustomizableShortcuts = (): IKeyboardShortcuts => {
},
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()) {

View File

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

View File

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

View File

@ -18,14 +18,15 @@ limitations under the License.
import React from "react";
import { Key } from "../../Keyboard";
import { useRovingTabIndex } from "../RovingTabIndex";
import StyledCheckbox from "../../components/views/elements/StyledCheckbox";
import { KeyBindingAction } from "../KeyboardShortcuts";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
interface IProps extends React.ComponentProps<typeof StyledCheckbox> {
label?: string;
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
@ -33,22 +34,37 @@ export const StyledMenuItemCheckbox: React.FC<IProps> = ({ children, label, onCh
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>();
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.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) => {
// 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
if (e.key === Key.SPACE || e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
const action = getKeyBindingsManager().getAccessibilityAction(e);
switch (action) {
case KeyBindingAction.Space:
case KeyBindingAction.Enter:
// 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 (

View File

@ -18,14 +18,15 @@ limitations under the License.
import React from "react";
import { Key } from "../../Keyboard";
import { useRovingTabIndex } from "../RovingTabIndex";
import StyledRadioButton from "../../components/views/elements/StyledRadioButton";
import { KeyBindingAction } from "../KeyboardShortcuts";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
interface IProps extends React.ComponentProps<typeof StyledRadioButton> {
label?: string;
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
@ -33,22 +34,37 @@ export const StyledMenuItemRadio: React.FC<IProps> = ({ children, label, onChang
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>();
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.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) => {
// 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
if (e.key === Key.SPACE || e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
const action = getKeyBindingsManager().getAccessibilityAction(e);
switch (action) {
case KeyBindingAction.Enter:
case KeyBindingAction.Space:
// 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 (

View File

@ -21,11 +21,12 @@ import ReactDOM from "react-dom";
import classNames from "classnames";
import FocusLock from "react-focus-lock";
import { Key } from "../../Keyboard";
import { Writeable } from "../../@types/common";
import { replaceableComponent } from "../../utils/replaceableComponent";
import UIStore from "../../stores/UIStore";
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
// 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) => {
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.
// They are probably using props.focusLock along with this option as well.
if (!this.props.managed) {
if (ev.key === Key.ESCAPE) {
if (action === KeyBindingAction.Escape) {
this.props.onFinished();
}
return;
}
// 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;
}
if (
ev.key === Key.ESCAPE ||
if ([
KeyBindingAction.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 ||
KeyBindingAction.Tab,
// When someone moves left or right along a <Toolbar /> (like the
// MessageActionBar), we should close any ContextMenu that is open.
ev.key === Key.ARROW_LEFT ||
ev.key === Key.ARROW_RIGHT
) {
KeyBindingAction.ArrowLeft,
KeyBindingAction.ArrowRight,
].includes(action)) {
this.props.onFinished();
}
};

View File

@ -35,7 +35,6 @@ import { getKeyBindingsManager } from "../../KeyBindingsManager";
import UIStore from "../../stores/UIStore";
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
import RoomListHeader from "../views/rooms/RoomListHeader";
import { Key } from "../../Keyboard";
import RecentlyViewedButton from "../views/rooms/RecentlyViewedButton";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
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) => {
if (ev.altKey || ev.ctrlKey || ev.metaKey) 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
if (ev.key.length === 1) {
ev.preventDefault();
ev.stopPropagation();
this.roomSearchRef.current?.appendChar(ev.key);
} else if (ev.key === Key.BACKSPACE) {
} else if (action === KeyBindingAction.Backspace) {
ev.preventDefault();
ev.stopPropagation();
this.roomSearchRef.current?.backspace();

View File

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

View File

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

View File

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

View File

@ -37,7 +37,6 @@ import UserActivity from "../../UserActivity";
import Modal from "../../Modal";
import dis from "../../dispatcher/dispatcher";
import { Action } from '../../dispatcher/actions';
import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent';
import { haveTileForEvent } from "../views/rooms/EventTile";
@ -54,6 +53,8 @@ import EditorStateTransfer from '../../utils/EditorStateTransfer';
import ErrorDialog from '../views/dialogs/ErrorDialog';
import CallEventGrouper, { buildCallEventGroupers } from "./CallEventGrouper";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
const PAGINATE_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.
*/
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
// 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();
} else {
this.messagePanel.current.handleScrollKey(ev);

View File

@ -20,11 +20,12 @@ import PlayPauseButton from "./PlayPauseButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { formatBytes } from "../../../utils/FormattingUtils";
import DurationClock from "./DurationClock";
import { Key } from "../../../Keyboard";
import { _t } from "../../../languageHandler";
import SeekBar from "./SeekBar";
import PlaybackClock from "./PlaybackClock";
import AudioPlayerBase from "./AudioPlayerBase";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
@replaceableComponent("views.audio_messages.AudioPlayer")
export default class AudioPlayer extends AudioPlayerBase {
@ -32,18 +33,29 @@ export default class AudioPlayer extends AudioPlayerBase {
private seekRef: RefObject<SeekBar> = createRef();
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,
// but we need to do it on key down instead of press (even though the user
// interaction is typically on press).
if (ev.key === Key.SPACE) {
if (handled) {
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 dis from "../../../dispatcher/dispatcher";
import RoomListActions from "../../../actions/RoomListActions";
import { Key } from "../../../Keyboard";
import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
import { RoomNotifState } from "../../../RoomNotifs";
import Modal from "../../../Modal";
@ -47,6 +46,8 @@ import DMRoomMap from "../../../utils/DMRoomMap";
import { Action } from "../../../dispatcher/actions";
import PosthogTrackers from "../../../PosthogTrackers";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IProps extends IContextMenuProps {
room: Room;
@ -267,9 +268,12 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
logger.warn(`Unexpected tag ${tagId} applied to ${room.roomId}`);
}
if ((ev as React.KeyboardEvent).key === Key.ENTER) {
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
onFinished();
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
switch (action) {
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 { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
import { abbreviateUrl } from '../../../utils/UrlUtils';
import { Key } from "../../../Keyboard";
import { Action } from "../../../dispatcher/actions";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AddressSelector from '../elements/AddressSelector';
@ -38,6 +37,8 @@ import AddressTile from '../elements/AddressTile';
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import AccessibleButton from '../elements/AccessibleButton';
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
const TRUNCATE_QUERY_LIST = 40;
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 => {
const textInput = this.textinput.current ? this.textinput.current.value : undefined;
let handled = true;
const action = getKeyBindingsManager().getAccessibilityAction(e);
if (e.key === Key.ESCAPE) {
e.stopPropagation();
e.preventDefault();
if (action === KeyBindingAction.Escape) {
this.props.onFinished(false);
} else if (e.key === Key.ARROW_UP) {
e.stopPropagation();
e.preventDefault();
if (this.addressSelector.current) this.addressSelector.current.moveSelectionUp();
} else if (e.key === Key.ARROW_DOWN) {
e.stopPropagation();
e.preventDefault();
if (this.addressSelector.current) this.addressSelector.current.moveSelectionDown();
} else if (this.state.suggestedList.length > 0 && [Key.COMMA, Key.ENTER, Key.TAB].includes(e.key)) {
e.stopPropagation();
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();
} else if (e.key === KeyBindingAction.ArrowUp) {
this.addressSelector.current?.moveSelectionUp();
} else if (e.key === KeyBindingAction.ArrowDown) {
this.addressSelector.current?.moveSelectionDown();
} else if (
[KeyBindingAction.Comma, KeyBindingAction.Enter, KeyBindingAction.Tab].includes(action) &&
this.state.suggestedList.length > 0
) {
this.addressSelector.current?.chooseSelection();
} else if (textInput.length === 0 && this.state.selectedList.length && action === KeyBindingAction.Backspace) {
this.onDismissed(this.state.selectedList.length - 1)();
} else if (e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
} else if (e.key === KeyBindingAction.Enter) {
if (textInput === '') {
// if there's nothing in the input box, submit the form
this.onButtonClick();
} else {
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.preventDefault();
this.addAddressesToList([textInput]);
}
};

View File

@ -21,7 +21,6 @@ import FocusLock from 'react-focus-lock';
import classNames from 'classnames';
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Key } from '../../../Keyboard';
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
@ -30,6 +29,8 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import Heading from '../typography/Heading';
import { IDialogProps } from "./IDialogProps";
import { PosthogScreenTracker, ScreenName } from "../../../PosthogTrackers";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IProps extends IDialogProps {
// 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) {
this.props.onKeyDown(e);
}
if (this.props.hasCancel && e.key === Key.ESCAPE) {
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
const action = getKeyBindingsManager().getAccessibilityAction(e);
switch (action) {
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 { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { Key } from "../../../Keyboard";
import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -34,6 +33,8 @@ import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IProps {
defaultPublic?: boolean;
@ -136,10 +137,13 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
}
private onKeyDown = (event: KeyboardEvent) => {
if (event.key === Key.ENTER) {
this.onOk();
event.preventDefault();
event.stopPropagation();
const action = getKeyBindingsManager().getAccessibilityAction(event);
switch (action) {
case KeyBindingAction.Enter:
this.onOk();
event.preventDefault();
event.stopPropagation();
break;
}
};

View File

@ -45,7 +45,6 @@ import {
showAnyInviteErrors,
showCommunityInviteDialog,
} from "../../../RoomInvite";
import { Key } from "../../../Keyboard";
import { Action } from "../../../dispatcher/actions";
import { DefaultTagID } from "../../../stores/room-list/models";
import RoomListStore from "../../../stores/room-list/RoomListStore";
@ -71,6 +70,8 @@ import UserIdentifierCustomisations from '../../../customisations/UserIdentifier
import CopyableText from "../elements/CopyableText";
import { ScreenName } from '../../../PosthogTrackers';
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.
/* eslint-disable camelcase */
@ -803,20 +804,36 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
private onKeyDown = (e) => {
if (this.state.busy) return;
let handled = true;
const value = e.target.value.trim();
const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey;
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
const action = getKeyBindingsManager().getAccessibilityAction(e);
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();
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 { MatrixClientPeg } from "../../../MatrixClientPeg";
import { Key } from "../../../Keyboard";
import { IDialogProps } from "./IDialogProps";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
export interface IScrollableBaseState {
canSubmit: boolean;
@ -45,10 +46,13 @@ export default abstract class ScrollableBaseModal<TProps extends IDialogProps, T
}
private onKeyDown = (e: KeyboardEvent | React.KeyboardEvent): void => {
if (e.key === Key.ESCAPE) {
e.stopPropagation();
e.preventDefault();
this.cancel();
const action = getKeyBindingsManager().getAccessibilityAction(e);
switch (action) {
case KeyBindingAction.Escape:
e.stopPropagation();
e.preventDefault();
this.cancel();
break;
}
};

View File

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

View File

@ -17,7 +17,8 @@
import React, { ReactHTML } from 'react';
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>;
@ -92,29 +93,36 @@ export default function AccessibleButton({
// Browsers handle space and enter keypresses differently and we are only adjusting to the
// inconsistencies here
newProps.onKeyDown = (e) => {
if (e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
return onClick(e);
}
if (e.key === Key.SPACE) {
e.stopPropagation();
e.preventDefault();
} else {
onKeyDown?.(e);
const action = getKeyBindingsManager().getAccessibilityAction(e);
switch (action) {
case KeyBindingAction.Enter:
e.stopPropagation();
e.preventDefault();
return onClick(e);
case KeyBindingAction.Space:
e.stopPropagation();
e.preventDefault();
break;
default:
onKeyDown?.(e);
}
};
newProps.onKeyUp = (e) => {
if (e.key === Key.SPACE) {
e.stopPropagation();
e.preventDefault();
return onClick(e);
}
if (e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
} else {
onKeyUp?.(e);
const action = getKeyBindingsManager().getAccessibilityAction(e);
switch (action) {
case KeyBindingAction.Enter:
e.stopPropagation();
e.preventDefault();
break;
case KeyBindingAction.Space:
e.stopPropagation();
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 { _t } from '../../../languageHandler';
import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IMenuOptionProps {
children: ReactElement;
@ -181,10 +182,12 @@ export default class Dropdown extends React.Component<IProps, IState> {
private onAccessibleButtonClick = (ev: ButtonEvent) => {
if (this.props.disabled) return;
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
if (!this.state.expanded) {
this.setState({ expanded: true });
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
this.props.onOptionChange(this.state.highlightedOption);
this.close();
@ -214,14 +217,15 @@ export default class Dropdown extends React.Component<IProps, IState> {
let handled = true;
// These keys don't generate keypress events and so needs to be on keyup
switch (e.key) {
case Key.ENTER:
const action = getKeyBindingsManager().getAccessibilityAction(e);
switch (action) {
case KeyBindingAction.Enter:
this.props.onOptionChange(this.state.highlightedOption);
// fallthrough
case Key.ESCAPE:
case KeyBindingAction.Escape:
this.close();
break;
case Key.ARROW_DOWN:
case KeyBindingAction.ArrowDown:
if (this.state.expanded) {
this.setState({
highlightedOption: this.nextOption(this.state.highlightedOption),
@ -230,7 +234,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
this.setState({ expanded: true });
}
break;
case Key.ARROW_UP:
case KeyBindingAction.ArrowUp:
if (this.state.expanded) {
this.setState({
highlightedOption: this.prevOption(this.state.highlightedOption),

View File

@ -17,7 +17,8 @@ limitations under the License.
import React, { createRef } from 'react';
import { Key } from "../../../Keyboard";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { replaceableComponent } from "../../../utils/replaceableComponent";
enum Phases {
@ -124,9 +125,12 @@ export default class EditableText extends React.Component<IProps, IState> {
this.showPlaceholder(false);
}
if (ev.key === Key.ENTER) {
ev.stopPropagation();
ev.preventDefault();
const action = getKeyBindingsManager().getAccessibilityAction(ev);
switch (action) {
case KeyBindingAction.Enter:
ev.stopPropagation();
ev.preventDefault();
break;
}
// 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;
}
if (ev.key === Key.ENTER) {
this.onFinish(ev);
} else if (ev.key === Key.ESCAPE) {
this.cancelEdit();
const action = getKeyBindingsManager().getAccessibilityAction(ev);
switch (action) {
case KeyBindingAction.Escape:
this.cancelEdit();
break;
case KeyBindingAction.Enter:
this.onFinish(ev);
break;
}
// 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 => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
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({
phase: Phases.Display,
}, () => {

View File

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

View File

@ -19,8 +19,9 @@ import React from 'react';
import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler';
import Field from "./Field";
import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
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 => {
if (event.key === Key.ENTER) {
event.preventDefault();
event.stopPropagation();
const action = getKeyBindingsManager().getAccessibilityAction(event);
switch (action) {
case KeyBindingAction.Enter:
event.preventDefault();
event.stopPropagation();
// 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
// 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
// handle the onBlur safely.
(event.target as HTMLInputElement).blur();
// 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
// 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
// handle the onBlur safely.
(event.target as HTMLInputElement).blur();
break;
}
};

View File

@ -19,9 +19,10 @@ import React from 'react';
import classNames from "classnames";
import { _t } from "../../../languageHandler";
import { Key } from "../../../Keyboard";
import { CategoryKey, ICategory } from "./Category";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IProps {
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
private onKeyDown = (ev: React.KeyboardEvent) => {
let handled = true;
switch (ev.key) {
case Key.ARROW_LEFT:
const action = getKeyBindingsManager().getAccessibilityAction(ev);
switch (action) {
case KeyBindingAction.ArrowLeft:
this.changeCategoryRelative(-1);
break;
case Key.ARROW_RIGHT:
case KeyBindingAction.ArrowRight:
this.changeCategoryRelative(1);
break;
case Key.HOME:
case KeyBindingAction.Home:
this.changeCategoryAbsolute(0);
break;
case Key.END:
case KeyBindingAction.End:
this.changeCategoryAbsolute(this.props.categories.length - 1, -1);
break;
default:

View File

@ -18,8 +18,9 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
interface IProps {
query: string;
@ -37,10 +38,13 @@ class Search extends React.PureComponent<IProps> {
}
private onKeyDown = (ev: React.KeyboardEvent) => {
if (ev.key === Key.ENTER) {
this.props.onEnter();
ev.stopPropagation();
ev.preventDefault();
const action = getKeyBindingsManager().getAccessibilityAction(ev);
switch (action) {
case KeyBindingAction.Enter:
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 accessibilityAction = getKeyBindingsManager().getAccessibilityAction(event);
if (model.autoComplete?.hasCompletions()) {
const autoComplete = model.autoComplete;
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
this.tabCompleteName();
handled = true;
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
} else if ([KeyBindingAction.Delete, KeyBindingAction.Backspace].includes(accessibilityAction)) {
this.formatBarRef.current.hide();
}

View File

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

View File

@ -25,7 +25,6 @@ import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleBu
import dis from '../../../dispatcher/dispatcher';
import defaultDispatcher from '../../../dispatcher/dispatcher';
import { Action } from "../../../dispatcher/actions";
import { Key } from "../../../Keyboard";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
import { _t } from "../../../languageHandler";
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
@ -54,6 +53,8 @@ import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/Community
import { replaceableComponent } from "../../../utils/replaceableComponent";
import PosthogTrackers from "../../../PosthogTrackers";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
interface IProps {
room: Room;
@ -240,11 +241,14 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
private onTileClick = (ev: React.KeyboardEvent) => {
ev.preventDefault();
ev.stopPropagation();
const action = getKeyBindingsManager().getAccessibilityAction(ev);
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
show_room_tile: true, // make sure the room is visible in the list
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",
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}`);
}
if ((ev as React.KeyboardEvent).key === Key.ENTER) {
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
this.setState({ generalMenuPosition: null }); // hide the menu
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
switch (action) {
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;
const key = (ev as React.KeyboardEvent).key;
if (key === Key.ENTER) {
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
this.setState({ notificationsMenuPosition: null }); // hide the menu
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
switch (action) {
case KeyBindingAction.Enter:
// 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 { _t } from '../../../languageHandler';
import { Key } from "../../../Keyboard";
import DesktopBuildsNotice, { WarningKind } from "../elements/DesktopBuildsNotice";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { PosthogScreenTracker } from '../../../PosthogTrackers';
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IProps {
onCancelClick: () => void;
@ -61,11 +62,12 @@ export default class SearchBar extends React.Component<IProps, IState> {
};
private onSearchChange = (e: React.KeyboardEvent) => {
switch (e.key) {
case Key.ENTER:
const action = getKeyBindingsManager().getAccessibilityAction(e);
switch (action) {
case KeyBindingAction.Enter:
this.onSearch();
break;
case Key.ESCAPE:
case KeyBindingAction.Escape:
this.props.onCancelClick();
break;
}

View File

@ -18,10 +18,11 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ActionPayload } from '../../../dispatcher/payloads';
import Spinner from "../elements/Spinner";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
interface IProps {
// 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 => {
if (ev.key === Key.ESCAPE) {
ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
const action = getKeyBindingsManager().getAccessibilityAction(ev);
switch (action) {
case KeyBindingAction.Escape:
ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
break;
}
};

View File

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

View File

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

View File

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

View File

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