Make everything use the KeyBindingManager
(#7907)
This commit is contained in:
parent
5f8441216c
commit
df591ee835
37 changed files with 529 additions and 277 deletions
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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]);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,
|
||||||
}, () => {
|
}, () => {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue