From 48c253299986f603f447cc13f9135a8491fcbf76 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Wed, 1 Sep 2021 10:49:59 +0200 Subject: [PATCH 1/6] C2LC-448: Added ctrl+shift key bindings. --- docs/keyboard.md | 55 ++++++++++++++ src/App.js | 2 +- src/App.test.js | 4 +- src/KeyboardInputModal.js | 14 ++++ src/KeyboardInputModal.test.js | 4 +- src/KeyboardInputSchemes.js | 79 +++++++++++++++++++- src/KeyboardInputSchemes.test.js | 120 ++++++++++++++++--------------- src/messages.json | 2 + 8 files changed, 214 insertions(+), 66 deletions(-) diff --git a/docs/keyboard.md b/docs/keyboard.md index 12897e97..621e6c49 100644 --- a/docs/keyboard.md +++ b/docs/keyboard.md @@ -17,6 +17,61 @@ divided using commas. For example, the shortcut `Ctrl + Alt + x, x` is activated by holding control, alt, and x at the same time, releasing all keys, and then hitting the x key again by itself. +## Control + Shift Key Bindings + +The default key bindings using control and shift for most commands, to avoid +conflicting with entering international characters in some keyboard layouts. + +| Keys | Command | +| ---- | ------- | +| Ctrl + Shift + a | addCommand | +| Ctrl + Shift + b | addCommandToBeginning | +| Ctrl + Shift + e | addCommandToEnd | +| Ctrl + Shift + d | deleteCurrentStep | +| Ctrl + Shift + i | announceScene | +| < | decreaseProgramSpeed | +| > | increaseProgramSpeed | +| Ctrl + Shift + p | playPauseProgram | +| Ctrl + Shift + r | refreshScene | +| ? | showHide | +| Ctrl + Shift + s | stopProgram | +| Ctrl + Shift + x, a, b, 1 | selectBackward1 | +| Ctrl + Shift + x, a, b, 2 | selectBackward2 | +| Ctrl + Shift + x, a, b, 3 | selectBackward3 | +| Ctrl + Shift + x, a, f, 1 | selectForward1 | +| Ctrl + Shift + x, a, f, 2 | selectForward2 | +| Ctrl + Shift + x, a, f, 3 | selectForward3 | +| Ctrl + Shift + x, a, l, 1 | selectLeft45 | +| Ctrl + Shift + x, a, l, 2 | selectLeft90 | +| Ctrl + Shift + x, a, l, 3 | selectLeft180 | +| Ctrl + Shift + x, a, r, 1 | selectRight45 | +| Ctrl + Shift + x, a, r, 2 | selectRight90 | +| Ctrl + Shift + x, a, r, 3 | selectRight180 | +| Ctrl + Shift + x, c, m, d | moveCharacterDown | +| Ctrl + Shift + x, c, m, l | moveCharacterLeft | +| Ctrl + Shift + x, c, m, r | moveCharacterRight | +| Ctrl + Shift + x, c, m, u | moveCharacterUp | +| Ctrl + Shift + x, c, t, l | turnCharacterLeft | +| Ctrl + Shift + x, c, t, r | turnCharacterRight | +| Ctrl + Shift + x, d | deleteAll | +| Ctrl + Shift + x, f, a | focusActions | +| Ctrl + Shift + x, f, c | focusCharacterPositionControls | +| Ctrl + Shift + x, f, h | focusAppHeader | +| Ctrl + Shift + x, f, p | focusPlayShare | +| Ctrl + Shift + x, f, q | focusProgramSequence | +| Ctrl + Shift + x, f, s | focusScene | +| Ctrl + Shift + x, f, t | focusAddNodeToggle | +| Ctrl + Shift + x, f, w | focusWorldSelector | +| Ctrl + Shift + x, f, x | focusCharacterColumnInput | +| Ctrl + Shift + x, f, y | focusCharacterRowInput | +| Ctrl + Shift + x, t, 1 | changeToDefaultTheme | +| Ctrl + Shift + x, t, 2 | changeToLightTheme | +| Ctrl + Shift + x, t, 3 | changeToDarkTheme | +| Ctrl + Shift + x, t, 4 | changeToGrayscaleTheme | +| Ctrl + Shift + x, t, 5 | changeToHighContrastTheme | +| Ctrl + Shift + x, x | toggleFeedbackAnnouncements | + + ## NVDA Key Bindings NVDA has many commands that make use of the alt key, to avoid conflicting with diff --git a/src/App.js b/src/App.js index ea788c09..715b5b2b 100644 --- a/src/App.js +++ b/src/App.js @@ -414,7 +414,7 @@ export class App extends React.Component { usedActions: {}, keyBindingsEnabled: true, showKeyboardModal: false, - keyboardInputSchemeName: "nvda" + keyboardInputSchemeName: "ctrlshift" }; // For FakeRobotDriver, replace with: diff --git a/src/App.test.js b/src/App.test.js index e134505b..75a5de29 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -98,14 +98,14 @@ it('Should be able to handle escaping out of a sequence', () => { expect(app.state().announcementsEnabled).toBe(true); const sequenceWithEscape = [ - new KeyboardEvent('keydown', { code: "KeyX", ctrlKey: true, altKey: true }), + new KeyboardEvent('keydown', { code: "KeyX", ctrlKey: true, shiftKey: true }), new KeyboardEvent('keydown', { code: "KeyA" }), // At this point our sequence of (Ctrl+Alt+x, a) matches the beginning // part of the 'select action' group of sequences. // Sending an Escape will break us out of the in-progress sequence. new KeyboardEvent('keydown', { key: "Escape" }), // So that we can send the key sequence to toggle the announcements - new KeyboardEvent('keydown', { code: "KeyX", ctrlKey: true, altKey: true }), + new KeyboardEvent('keydown', { code: "KeyX", ctrlKey: true, shiftKey: true }), new KeyboardEvent('keydown', { code: "KeyX" }), ]; diff --git a/src/KeyboardInputModal.js b/src/KeyboardInputModal.js index 532774d4..606189de 100644 --- a/src/KeyboardInputModal.js +++ b/src/KeyboardInputModal.js @@ -111,6 +111,20 @@ class KeyboardInputModal extends React.Component); + if (keyDef.shiftKey) { + const shiftKeyLabel = this.props.intl.formatMessage( + { id: "KeyboardInputModal.KeyLabels.Shift" } + ); + labelKeySegments.unshift(shiftKeyLabel); + + const shiftKeyIcon = this.props.intl.formatMessage( + { id: "KeyboardInputModal.KeyIcons.Shift" } + ); + icons.unshift(
+ {shiftKeyIcon} +
); + } + if (keyDef.altKey) { const altKeyLabel = this.props.intl.formatMessage( { id: "KeyboardInputModal.KeyLabels.Alt" } diff --git a/src/KeyboardInputModal.test.js b/src/KeyboardInputModal.test.js index ada08e2b..94ef98e6 100644 --- a/src/KeyboardInputModal.test.js +++ b/src/KeyboardInputModal.test.js @@ -21,7 +21,7 @@ function createShallowKeyboardInputModal(props) { const defaultWrapperProps = { keyBindingsEnabled: true, - keyboardInputSchemeName: "nvda", + keyboardInputSchemeName: "ctrlshift", onChangeKeyBindingsEnabled: onChangeKeyBindingsEnabled, onChangeKeyboardInputScheme: onChangeKeyboardInputScheme, onHide: onHide, @@ -100,7 +100,7 @@ it('should be able to cancel changes.', () => { expect(wrappedModal.state.keyBindingsEnabled).toBe(false); wrappedModal.cancelChanges(); - expect(wrappedModal.state.keyboardInputSchemeName).toBe("nvda"); + expect(wrappedModal.state.keyboardInputSchemeName).toBe("ctrlshift"); expect(wrappedModal.state.keyBindingsEnabled).toBe(true); expect(onChangeKeyBindingsEnabled.mock.calls.length).toBe(0); diff --git a/src/KeyboardInputSchemes.js b/src/KeyboardInputSchemes.js index 25567cb8..9fdfa771 100644 --- a/src/KeyboardInputSchemes.js +++ b/src/KeyboardInputSchemes.js @@ -1,13 +1,14 @@ //@flow import {extend} from './Utils'; -export type KeyboardInputSchemeName = "nvda" | "voiceover"; +export type KeyboardInputSchemeName = "ctrlshift"| "nvda" | "voiceover"; export type KeyDef = { code?: string, key?: string, altKey?: boolean, ctrlKey?: boolean, + shiftKey?: boolean, hidden?: boolean }; @@ -86,6 +87,7 @@ export type KeyboardInputScheme = { }; export type KeyboardInputSchemesType = { + "ctrlshift": KeyboardInputScheme, "nvda": KeyboardInputScheme, "voiceover": KeyboardInputScheme }; @@ -376,7 +378,7 @@ const NvdaInputScheme = Object.assign({ actionName: "refreshScene" }, showHide: { - keyDef: { key: "?", shiftKey: true }, + keyDef: { key: "?" }, actionName: "showHide" }, stopProgram: { @@ -385,9 +387,75 @@ const NvdaInputScheme = Object.assign({ }, }, NvdaExtendedKeyboardSequences); +const CtrlShiftExtendedKeyboardSequences = extend(ExtendedKeyboardSequences, { + extraSettings: { + keyDef: { ctrlKey: true, shiftKey: true, altKey: false } + }, + + focusChange: { + keyDef: {ctrlKey: true, shiftKey: true, altKey: false } + }, + + selectedActionChange: { + keyDef: { ctrlKey: true, shiftKey: true, altKey: false } + }, + + characterPosition: { + keyDef: { ctrlKey: true, shiftKey: true, altKey: false } + } +}); + +const CtrlShiftInputScheme = Object.assign({ + addCommand: { + keyDef: { code: "KeyA", key: "a", ctrlKey: true, shiftKey: true}, + actionName: "addCommand" + }, + addCommandToBeginning: { + keyDef: { code: "KeyB", key: "b", ctrlKey: true, shiftKey: true}, + actionName: "addCommandToBeginning" + }, + addCommandToEnd: { + keyDef: { code: "KeyE", key: "e", ctrlKey: true, shiftKey: true}, + actionName: "addCommandToEnd" + }, + deleteCurrentStep: { + keyDef: { code: "KeyD", key: "d", ctrlKey: true, shiftKey: true}, + actionName: "deleteCurrentStep" + }, + announceScene: { + keyDef: {code: "KeyI", key: "i", ctrlKey: true, shiftKey: true}, + actionName: "announceScene" + }, + decreaseProgramSpeed: { + keyDef: { key: "<", shiftKey: true, hidden: true}, + actionName: "decreaseProgramSpeed" + }, + increaseProgramSpeed: { + keyDef: { key: ">", shiftKey: true, hidden: true}, + actionName: "increaseProgramSpeed" + }, + playPauseProgram: { + keyDef: { code: "KeyP", key: "p", ctrlKey: true, shiftKey: true}, + actionName: "playPauseProgram" + }, + refreshScene: { + keyDef: { code: "KeyR", key: "r", ctrlKey: true, shiftKey: true}, + actionName: "refreshScene" + }, + showHide: { + keyDef: { key: "?" }, + actionName: "showHide" + }, + stopProgram: { + keyDef: {code: "KeyS", key: "s", ctrlKey: true, shiftKey: true}, + actionName: "stopProgram" + }, +}, CtrlShiftExtendedKeyboardSequences); + export const KeyboardInputSchemes:KeyboardInputSchemesType = { "nvda": NvdaInputScheme, - "voiceover": VoiceOverInputScheme + "voiceover": VoiceOverInputScheme, + "ctrlshift": CtrlShiftInputScheme }; const labelMessageKeysByCode = { @@ -456,6 +524,11 @@ export function keyboardEventMatchesKeyDef (e: KeyboardEvent, keyDef: KeyDef) { if (!!(keyDef.ctrlKey) !== !!(e.ctrlKey)) { return false; } + // We are more flexible about shift, which is only required if it's + // specified in the keydef. + if (keyDef.shiftKey && !e.shiftKey) { + return false + } return true; } diff --git a/src/KeyboardInputSchemes.test.js b/src/KeyboardInputSchemes.test.js index 08fb64ad..09ec8193 100644 --- a/src/KeyboardInputSchemes.test.js +++ b/src/KeyboardInputSchemes.test.js @@ -1,4 +1,4 @@ -import {keyboardEventMatchesKeyDef, findKeyboardEventSequenceMatches} from './KeyboardInputSchemes'; +import {keyboardEventMatchesKeyDef, findKeyboardEventSequenceMatches, KeyboardInputSchemes} from './KeyboardInputSchemes'; it('Should be able to handle unmodified keys', () => { const keyDef = { key: "?" }; @@ -70,60 +70,64 @@ it('Should be able to handle a partial sequence', () => { expect(result).toBe("partial"); }); -// Uncomment these out (and add KeyboardInputSchemes to your imports) to generate -// and output markdown representing the full set of key bindings. - -// function processSingleLevel (singleLevel, accumulatedSequence) { -// let levelSequences = []; -// const levelAccumulatedSequence = accumulatedSequence.concat([singleLevel.keyDef]); -// if (singleLevel.actionName) { -// levelAccumulatedSequence.push(singleLevel.actionName); -// levelSequences.push(levelAccumulatedSequence); -// } -// else { -// for (const [subEntryKey, subEntryValue] of Object.entries(singleLevel)) { -// if (subEntryKey !== "keyDef" && subEntryKey !== "commandName") { -// const subSequences = processSingleLevel(subEntryValue, levelAccumulatedSequence); -// levelSequences = levelSequences.concat(subSequences); -// } -// } -// } -// return levelSequences; -// } - -// function displayKeyBindings () { -// let markdown = ""; -// for (const [schemeName, keyboardInputScheme] of Object.entries(KeyboardInputSchemes)) { -// markdown += '## ' + schemeName + ' Key Bindings\n\n'; -// markdown += '| Keys | Command |\n' -// markdown += '| ---- | ------- |\n' -// for (const topLevelBinding of Object.values(keyboardInputScheme)) { -// const allSequences = processSingleLevel(topLevelBinding, []); -// const bindingEntries = []; -// for (const sequence of allSequences) { -// const keys = sequence.slice(0, sequence.length - 1); -// const commandName = sequence.slice(-1); -// let bindingText = ""; -// for (const keyDef of keys) { -// if (bindingText.length) { -// bindingText += ", "; -// } -// if (keyDef.ctrlKey) { -// bindingText += "Ctrl + "; -// } -// if (keyDef.altKey) { -// bindingText += "Alt + " -// } -// bindingText += keyDef.key || (keyDef.code && keyDef.code.replace("Key", "")); -// } -// bindingEntries.push('| ' + bindingText + ' | ' + commandName + ' |'); -// } -// markdown += bindingEntries.sort().join('\n') + '\n'; -// } -// markdown += "\n"; -// } -// /* eslint-disable-next-line no-console */ -// console.log(markdown); -// } - -// displayKeyBindings(); +function processSingleLevel (singleLevel, accumulatedSequence) { + let levelSequences = []; + const levelAccumulatedSequence = accumulatedSequence.concat([singleLevel.keyDef]); + if (singleLevel.actionName) { + levelAccumulatedSequence.push(singleLevel.actionName); + levelSequences.push(levelAccumulatedSequence); + } + else { + for (const [subEntryKey, subEntryValue] of Object.entries(singleLevel)) { + if (subEntryKey !== "keyDef" && subEntryKey !== "commandName") { + const subSequences = processSingleLevel(subEntryValue, levelAccumulatedSequence); + levelSequences = levelSequences.concat(subSequences); + } + } + } + return levelSequences; +} + +function displayKeyBindings () { + let markdown = ""; + for (const [schemeName, keyboardInputScheme] of Object.entries(KeyboardInputSchemes)) { + markdown += '## ' + schemeName + ' Key Bindings\n\n'; + markdown += '| Keys | Command |\n' + markdown += '| ---- | ------- |\n' + for (const topLevelBinding of Object.values(keyboardInputScheme)) { + const allSequences = processSingleLevel(topLevelBinding, []); + const bindingEntries = []; + for (const sequence of allSequences) { + const keys = sequence.slice(0, sequence.length - 1); + const commandName = sequence.slice(-1); + let bindingText = ""; + for (const keyDef of keys) { + if (bindingText.length) { + bindingText += ", "; + } + if (keyDef.ctrlKey) { + bindingText += "Ctrl + "; + } + if (keyDef.altKey) { + bindingText += "Alt + " + } + if (keyDef.shiftKey && ["<",">"].indexOf(keyDef.key) === -1) { + bindingText += "Shift + " + } + bindingText += keyDef.key || (keyDef.code && keyDef.code.replace("Key", "")); + } + bindingEntries.push('| ' + bindingText + ' | ' + commandName + ' |'); + } + markdown += bindingEntries.sort().join('\n') + '\n'; + } + markdown += "\n"; + } + /* eslint-disable-next-line no-console */ + console.log(markdown); +} + +// Set this to true to output markdown tables with all key bindings for the docs. +const logKeyBindings = true; +if (logKeyBindings) { + displayKeyBindings(); +} diff --git a/src/messages.json b/src/messages.json index 3ec7d76b..3307acb3 100644 --- a/src/messages.json +++ b/src/messages.json @@ -121,6 +121,7 @@ "KeyboardInputModal.Done": "Done", "KeyboardInputModal.KeyLabels.Alt": "Alt", "KeyboardInputModal.KeyLabels.Control": "Control", + "KeyboardInputModal.KeyLabels.Shift": "Shift", "KeyboardInputModal.KeyLabels.QuestionMark": "question mark", "KeyboardInputModal.KeyLabels.GreaterThan": "greater than", "KeyboardInputModal.KeyLabels.LessThan": "less than", @@ -146,6 +147,7 @@ "KeyboardInputModal.KeyIcons.P": "P", "KeyboardInputModal.KeyIcons.S": "S", "KeyboardInputModal.KeyIcons.R": "R", + "KeyboardInputModal.Scheme.Descriptions.ctrlshift": "Control + Shift key bindings", "KeyboardInputModal.Scheme.Descriptions.nvda": "NVDA key bindings", "KeyboardInputModal.Scheme.Descriptions.voiceover": "VoiceOver key bindings", "KeyboardInputModal.ShowHide.AriaLabel": "Display keyboard shortcuts menu", From e45ffc878f6be5611e1624d522531d762e0611ed Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Tue, 12 Oct 2021 14:40:14 +0200 Subject: [PATCH 2/6] C2LC-448: Changed control+shift keyboard input scheme name to match 'controlalt'. --- src/App.js | 2 +- src/KeyboardInputModal.test.js | 4 ++-- src/KeyboardInputSchemes.js | 6 +++--- src/messages.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/App.js b/src/App.js index 6c7e3b65..070ba5d9 100644 --- a/src/App.js +++ b/src/App.js @@ -417,7 +417,7 @@ export class App extends React.Component { keyBindingsEnabled: false, showKeyboardModal: false, showWorldSelector: false, - keyboardInputSchemeName: "ctrlshift" + keyboardInputSchemeName: "controlshift" }; // For FakeRobotDriver, replace with: diff --git a/src/KeyboardInputModal.test.js b/src/KeyboardInputModal.test.js index e84ea468..c3ee69e8 100644 --- a/src/KeyboardInputModal.test.js +++ b/src/KeyboardInputModal.test.js @@ -21,7 +21,7 @@ function createShallowKeyboardInputModal(props) { const defaultWrapperProps = { keyBindingsEnabled: true, - keyboardInputSchemeName: "ctrlshift", + keyboardInputSchemeName: "controlshift", onChangeKeyBindingsEnabled: onChangeKeyBindingsEnabled, onChangeKeyboardInputScheme: onChangeKeyboardInputScheme, onHide: onHide, @@ -100,7 +100,7 @@ it('should be able to cancel changes.', () => { expect(wrappedModal.state.keyBindingsEnabled).toBe(false); wrappedModal.cancelChanges(); - expect(wrappedModal.state.keyboardInputSchemeName).toBe("ctrlshift"); + expect(wrappedModal.state.keyboardInputSchemeName).toBe("controlshift"); expect(wrappedModal.state.keyBindingsEnabled).toBe(true); expect(onChangeKeyBindingsEnabled.mock.calls.length).toBe(0); diff --git a/src/KeyboardInputSchemes.js b/src/KeyboardInputSchemes.js index c12478f5..bd1106e2 100644 --- a/src/KeyboardInputSchemes.js +++ b/src/KeyboardInputSchemes.js @@ -1,7 +1,7 @@ //@flow import {extend} from './Utils'; -export type KeyboardInputSchemeName = "ctrlshift" | "controlalt" | "alt"; +export type KeyboardInputSchemeName = "controlshift" | "controlalt" | "alt"; export function isKeyboardInputSchemeName(str: ?string): boolean { return str === 'controlalt' || str === 'alt'; @@ -91,7 +91,7 @@ export type KeyboardInputScheme = { }; export type KeyboardInputSchemesType = { - "ctrlshift": KeyboardInputScheme, + "controlshift": KeyboardInputScheme, "controlalt": KeyboardInputScheme, "alt": KeyboardInputScheme }; @@ -457,7 +457,7 @@ const CtrlShiftInputScheme = Object.assign({ }, CtrlShiftExtendedKeyboardSequences); export const KeyboardInputSchemes:KeyboardInputSchemesType = { - "ctrlshift": CtrlShiftInputScheme, + "controlshift": CtrlShiftInputScheme, "controlalt": ControlAltInputScheme, "alt": AltInputScheme }; diff --git a/src/messages.json b/src/messages.json index 901e6fc4..6c329d01 100644 --- a/src/messages.json +++ b/src/messages.json @@ -164,7 +164,7 @@ "KeyboardInputModal.KeyIcons.P": "P", "KeyboardInputModal.KeyIcons.S": "S", "KeyboardInputModal.KeyIcons.R": "R", - "KeyboardInputModal.Scheme.Descriptions.ctrlshift": "Control + Shift key bindings", + "KeyboardInputModal.Scheme.Descriptions.controlshift": "Control + Shift key bindings", "KeyboardInputModal.Scheme.Descriptions.controlalt": "Control+Alt (Apple: Control+Option)", "KeyboardInputModal.Scheme.Descriptions.alt": "Alt (Apple: Option)", "KeyboardInputModal.ShowHide.AriaLabel": "Display keyboard shortcuts menu", From a38db2356faa5b9e2b41d301c464713b0b29764c Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Tue, 12 Oct 2021 15:41:37 +0200 Subject: [PATCH 3/6] C2LC-448: Cleaned up keyboard input scheme tests to fix flow coverage level --- src/KeyboardInputSchemes.js | 4 ++-- src/KeyboardInputSchemes.test.js | 31 +++++++++++++++++++------------ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/KeyboardInputSchemes.js b/src/KeyboardInputSchemes.js index bd1106e2..36d8ea23 100644 --- a/src/KeyboardInputSchemes.js +++ b/src/KeyboardInputSchemes.js @@ -76,12 +76,12 @@ export type ActionName = | "deleteAll" ; -type ActionKeyStep = { +export type ActionKeyStep = { actionName: ActionName, keyDef: KeyDef }; -type KeySequenceStep = { +export type KeySequenceStep = { keyDef: KeyDef, [string]: KeySequenceStep | ActionKeyStep }; diff --git a/src/KeyboardInputSchemes.test.js b/src/KeyboardInputSchemes.test.js index 34bfae6a..71c9b7c5 100644 --- a/src/KeyboardInputSchemes.test.js +++ b/src/KeyboardInputSchemes.test.js @@ -1,4 +1,6 @@ +// @flow import {keyboardEventMatchesKeyDef, findKeyboardEventSequenceMatches, isKeyboardInputSchemeName, KeyboardInputSchemes} from './KeyboardInputSchemes'; +import type {KeyDef} from './KeyboardInputSchemes'; it('isKeyboardInputSchemeName', () => { expect.assertions(5); @@ -10,42 +12,42 @@ it('isKeyboardInputSchemeName', () => { }); it('Should be able to handle unmodified keys', () => { - const keyDef = { key: "?" }; + const keyDef: KeyDef = { key: "?" }; - const unmodifiedKeyboardEvent = new KeyboardEvent('keydown', { key: "?"}); + const unmodifiedKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { key: "?"}); expect(keyboardEventMatchesKeyDef(unmodifiedKeyboardEvent, keyDef)).toBe(true); - const controlModifiedKeyboardEvent = new KeyboardEvent('keydown', { key: "?", ctrlKey: true}); + const controlModifiedKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { key: "?", ctrlKey: true}); expect(keyboardEventMatchesKeyDef(controlModifiedKeyboardEvent, keyDef)).toBe(false); - const altModifiedKeyboardEvent = new KeyboardEvent('keydown', { key: "?", altKey: true}); + const altModifiedKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { key: "?", altKey: true}); expect(keyboardEventMatchesKeyDef(altModifiedKeyboardEvent, keyDef)).toBe(false); }); it('Should be able to handle control keys', () => { - const keyDef = { key: "A", ctrlKey: true }; + const keyDef: KeyDef = { key: "A", ctrlKey: true }; - const controlModifiedKeyboardEvent = new KeyboardEvent('keydown', { key: "A", ctrlKey: true}); + const controlModifiedKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { key: "A", ctrlKey: true}); expect(keyboardEventMatchesKeyDef(controlModifiedKeyboardEvent, keyDef)).toBe(true); - const unmodifiedKeyboardEvent = new KeyboardEvent('keydown', { key: "A"}); + const unmodifiedKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { key: "A"}); expect(keyboardEventMatchesKeyDef(unmodifiedKeyboardEvent, keyDef)).toBe(false); - const controlAltModifiedKeyboardEvent = new KeyboardEvent('keydown', { key: "A", altKey: true, ctrlKey: true}); + const controlAltModifiedKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { key: "A", altKey: true, ctrlKey: true}); expect(keyboardEventMatchesKeyDef(controlAltModifiedKeyboardEvent, keyDef)).toBe(false); }); it('Should be able to handle alt keys', () => { - const keyDef = { key: "B", altKey: true }; + const keyDef: KeyDef = { key: "B", altKey: true }; - const altModifiedKeyboardEvent = new KeyboardEvent('keydown', { key: "B", altKey: true}); + const altModifiedKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { key: "B", altKey: true}); expect(keyboardEventMatchesKeyDef(altModifiedKeyboardEvent, keyDef)).toBe(true); - const unmodifiedKeyboardEvent = new KeyboardEvent('keydown', { key: "B"}); + const unmodifiedKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { key: "B"}); expect(keyboardEventMatchesKeyDef(unmodifiedKeyboardEvent, keyDef)).toBe(false); - const controlAltModifiedKeyboardEvent = new KeyboardEvent('keydown', { key: "B", altKey: true, ctrlKey: true}); + const controlAltModifiedKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { key: "B", altKey: true, ctrlKey: true}); expect(keyboardEventMatchesKeyDef(controlAltModifiedKeyboardEvent, keyDef)).toBe(false); }); @@ -81,12 +83,15 @@ it('Should be able to handle a partial sequence', () => { function processSingleLevel (singleLevel, accumulatedSequence) { let levelSequences = []; + // $FlowFixMe: This function generates docs, we don't particularly care about flow coverage here. const levelAccumulatedSequence = accumulatedSequence.concat([singleLevel.keyDef]); + // $FlowFixMe: This function generates docs, we don't particularly care about flow coverage here. if (singleLevel.actionName) { levelAccumulatedSequence.push(singleLevel.actionName); levelSequences.push(levelAccumulatedSequence); } else { + // $FlowFixMe: This function generates docs, we don't particularly care about flow coverage here. for (const [subEntryKey, subEntryValue] of Object.entries(singleLevel)) { if (subEntryKey !== "keyDef" && subEntryKey !== "commandName") { const subSequences = processSingleLevel(subEntryValue, levelAccumulatedSequence); @@ -103,6 +108,7 @@ function displayKeyBindings () { markdown += '## ' + schemeName + ' Key Bindings\n\n'; markdown += '| Keys | Command |\n' markdown += '| ---- | ------- |\n' + // $FlowFixMe: This function generates docs, we don't particularly care about flow coverage here. for (const topLevelBinding of Object.values(keyboardInputScheme)) { const allSequences = processSingleLevel(topLevelBinding, []); const bindingEntries = []; @@ -125,6 +131,7 @@ function displayKeyBindings () { } bindingText += keyDef.key || (keyDef.code && keyDef.code.replace("Key", "")); } + // $FlowFixMe: This function generates docs, we don't particularly care about flow coverage here. bindingEntries.push('| ' + bindingText + ' | ' + commandName + ' |'); } markdown += bindingEntries.sort().join('\n') + '\n'; From cee15be90678cbc32ae2ee575363f09d669b1b05 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Wed, 16 Feb 2022 10:20:16 +0100 Subject: [PATCH 4/6] C2LC-448: Fixed linting error introduced by merge. --- docs/keyboard.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/keyboard.md b/docs/keyboard.md index dea0a4d4..b3da5bdb 100644 --- a/docs/keyboard.md +++ b/docs/keyboard.md @@ -71,8 +71,6 @@ conflicting with entering international characters in some keyboard layouts. | Ctrl + Shift + x, t, 5 | changeToHighContrastTheme | | Ctrl + Shift + x, x | toggleFeedbackAnnouncements | - -## NVDA Key Bindings ## Control+Alt (Apple: Control+Option) Key Bindings NVDA has many commands that make use of the alt key, to avoid conflicting with From ab2cf298929d31c09d08dc51cda2ee3f12396ba1 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Fri, 18 Feb 2022 10:54:49 +0100 Subject: [PATCH 5/6] C2LC-448: Refactored to add modifiers to a single unified keyboard input scheme. --- src/KeyboardInputSchemes.js | 267 ++++++++----------------------- src/KeyboardInputSchemes.test.js | 3 +- src/Utils.js | 36 ++++- src/Utils.test.js | 12 +- 4 files changed, 118 insertions(+), 200 deletions(-) diff --git a/src/KeyboardInputSchemes.js b/src/KeyboardInputSchemes.js index 36d8ea23..e1fe84e2 100644 --- a/src/KeyboardInputSchemes.js +++ b/src/KeyboardInputSchemes.js @@ -1,10 +1,10 @@ //@flow -import {extend} from './Utils'; +import {overlayModifierKeys} from './Utils'; export type KeyboardInputSchemeName = "controlshift" | "controlalt" | "alt"; export function isKeyboardInputSchemeName(str: ?string): boolean { - return str === 'controlalt' || str === 'alt'; + return str === 'controlalt' || str === 'alt' || str === 'controlshift'; } export type KeyDef = { @@ -83,11 +83,11 @@ export type ActionKeyStep = { export type KeySequenceStep = { keyDef: KeyDef, - [string]: KeySequenceStep | ActionKeyStep + [subDefKey: string]: KeySequenceStep | ActionKeyStep }; export type KeyboardInputScheme = { - [string]: KeySequenceStep | ActionKeyStep + [subDefKey: string]: KeySequenceStep | ActionKeyStep }; export type KeyboardInputSchemesType = { @@ -96,9 +96,53 @@ export type KeyboardInputSchemesType = { "alt": KeyboardInputScheme }; -const ExtendedKeyboardSequences: KeyboardInputScheme = { +const BaseKeyboardSequences: KeyboardInputScheme = { + addCommand: { + keyDef: { code: "KeyA", key: "a"}, + actionName: "addCommand" + }, + addCommandToBeginning: { + keyDef: { code: "KeyB", key: "b"}, + actionName: "addCommandToBeginning" + }, + addCommandToEnd: { + keyDef: { code: "KeyE", key: "e"}, + actionName: "addCommandToEnd" + }, + deleteCurrentStep: { + keyDef: { code: "KeyD", key: "d"}, + actionName: "deleteCurrentStep" + }, + announceScene: { + keyDef: { code: "KeyI", key: "i"}, + actionName: "announceScene" + }, + decreaseProgramSpeed: { + keyDef: { key: "<", hidden: true}, + actionName: "decreaseProgramSpeed" + }, + increaseProgramSpeed: { + keyDef: { key: ">", hidden: true}, + actionName: "increaseProgramSpeed" + }, + playPauseProgram: { + keyDef: { code: "KeyP", key: "p"}, + actionName: "playPauseProgram" + }, + refreshScene: { + keyDef: { code: "KeyR", key: "r"}, + actionName: "refreshScene" + }, + showHide: { + keyDef: { key: "?"}, + actionName: "showHide" + }, + stopProgram: { + keyDef: { code: "KeyS", key: "s"}, + actionName: "stopProgram" + }, extraSettings: { - keyDef: { code: "KeyX", key: "x", altKey: true, hidden: true}, + keyDef: { code: "KeyX", key: "x"}, audioFeedback: { keyDef: { code: "KeyX", key: "x"}, actionName: "toggleFeedbackAnnouncements" @@ -155,60 +199,60 @@ const ExtendedKeyboardSequences: KeyboardInputScheme = { forward: { keyDef: { code: "KeyF", key: "f" }, forward1: { - keyDef: { key: "1"}, + keyDef: { key: "1", keyCode: "49"}, actionName: "selectForward1" }, forward2: { - keyDef: { key: "2"}, + keyDef: { key: "2", keyCode: "50"}, actionName: "selectForward2" }, forward3: { - keyDef: { key: "3"}, + keyDef: { key: "3", keyCode: "51"}, actionName: "selectForward3" } }, backward: { keyDef: { code: "KeyB", key: "b" }, backward1: { - keyDef: { key: "1"}, + keyDef: { key: "1", keyCode: "49"}, actionName: "selectBackward1" }, backward2: { - keyDef: { key: "2"}, + keyDef: { key: "2", keyCode: "50"}, actionName: "selectBackward2" }, backward3: { - keyDef: { key: "3"}, + keyDef: { key: "3", keyCode: "51"}, actionName: "selectBackward3" } }, left: { keyDef: { code: "KeyL", key: "l" }, left45: { - keyDef: { key: "1"}, + keyDef: { key: "1", keyCode: "49"}, actionName: "selectLeft45" }, left90: { - keyDef: { key: "2"}, + keyDef: { key: "2", keyCode: "50"}, actionName: "selectLeft90" }, left180: { - keyDef: { key: "3"}, + keyDef: { key: "3", keyCode: "51"}, actionName: "selectLeft180" } }, right: { keyDef: { code: "KeyR", key: "r" }, right45: { - keyDef: { key: "1"}, + keyDef: { key: "1", keyCode: "49"}, actionName: "selectRight45" }, right90: { - keyDef: { key: "2"}, + keyDef: { key: "2", keyCode: "50"}, actionName: "selectRight90" }, right180: { - keyDef: { key: "3"}, + keyDef: { key: "3", keyCode: "51"}, actionName: "selectRight180" } } @@ -251,23 +295,23 @@ const ExtendedKeyboardSequences: KeyboardInputScheme = { changeTheme: { keyDef: { code: "KeyT", key: "t" }, default: { - keyDef: { key: "1"}, + keyDef: { key: "1", keyCode: "49"}, actionName: "changeToDefaultTheme" }, light: { - keyDef: { key: "2"}, + keyDef: { key: "2", keyCode: "50"}, actionName: "changeToLightTheme" }, dark: { - keyDef: { key: "3"}, + keyDef: { key: "3", keyCode: "51"}, actionName: "changeToDarkTheme" }, grayscale: { - keyDef: { key: "4"}, + keyDef: { key: "4", keyCode: "52"}, actionName: "changeToGrayscaleTheme" }, highContrast: { - keyDef: { key: "5"}, + keyDef: { key: "5", keyCode: "53"}, actionName: "changeToHighContrastTheme" } }, @@ -279,182 +323,11 @@ const ExtendedKeyboardSequences: KeyboardInputScheme = { } } -const AltInputScheme: KeyboardInputScheme = Object.assign({ - addCommand: { - keyDef: { code: "KeyA", key: "a", altKey: true}, - actionName: "addCommand" - }, - addCommandToBeginning: { - keyDef: { code: "KeyB", key: "b", altKey: true}, - actionName: "addCommandToBeginning" - }, - addCommandToEnd: { - keyDef: { code: "KeyE", key: "e", altKey: true}, - actionName: "addCommandToEnd" - }, - deleteCurrentStep: { - keyDef: { code: "KeyD", key: "d", altKey: true}, - actionName: "deleteCurrentStep" - }, - announceScene: { - keyDef: { code: "KeyI", key: "i", altKey: true}, - actionName: "announceScene" - }, - decreaseProgramSpeed: { - keyDef: { key: "<", hidden: true}, - actionName: "decreaseProgramSpeed" - }, - increaseProgramSpeed: { - keyDef: { key: ">", hidden: true}, - actionName: "increaseProgramSpeed" - }, - playPauseProgram: { - keyDef: { code: "KeyP", key: "p", altKey: true}, - actionName: "playPauseProgram" - }, - refreshScene: { - keyDef: { code: "KeyR", key: "r", altKey: true}, - actionName: "refreshScene" - }, - showHide: { - keyDef: { key: "?"}, - actionName: "showHide" - }, - stopProgram: { - keyDef: { code: "KeyS", key: "s", altKey: true}, - actionName: "stopProgram" - } -}, ExtendedKeyboardSequences); - -const ControlAltExtendedKeyboardSequences = extend(ExtendedKeyboardSequences, { - extraSettings: { - keyDef: { ctrlKey: true } - }, - - focusChange: { - keyDef: {ctrlKey: true } - }, - - selectedActionChange: { - keyDef: { ctrlKey: true } - }, - - characterPosition: { - keyDef: { ctrlKey: true } - } -}); - -const ControlAltInputScheme = Object.assign({ - addCommand: { - keyDef: { code: "KeyA", key: "a", altKey: true, ctrlKey: true}, - actionName: "addCommand" - }, - addCommandToBeginning: { - keyDef: { code: "KeyB", key: "b", altKey: true, ctrlKey: true}, - actionName: "addCommandToBeginning" - }, - addCommandToEnd: { - keyDef: { code: "KeyE", key: "e", altKey: true, ctrlKey: true}, - actionName: "addCommandToEnd" - }, - deleteCurrentStep: { - keyDef: { code: "KeyD", key: "d", altKey: true, ctrlKey: true}, - actionName: "deleteCurrentStep" - }, - announceScene: { - keyDef: {code: "KeyI", key: "i", altKey: true, ctrlKey: true}, - actionName: "announceScene" - }, - decreaseProgramSpeed: { - keyDef: { key: "<", shiftKey: true, hidden: true}, - actionName: "decreaseProgramSpeed" - }, - increaseProgramSpeed: { - keyDef: { key: ">", shiftKey: true, hidden: true}, - actionName: "increaseProgramSpeed" - }, - playPauseProgram: { - keyDef: { code: "KeyP", key: "p", altKey: true, ctrlKey: true}, - actionName: "playPauseProgram" - }, - refreshScene: { - keyDef: { code: "KeyR", key: "r", altKey: true, ctrlKey: true }, - actionName: "refreshScene" - }, - showHide: { - keyDef: { key: "?" }, - actionName: "showHide" - }, - stopProgram: { - keyDef: {code: "KeyS", key: "s", altKey: true, ctrlKey: true}, - actionName: "stopProgram" - }, -}, ControlAltExtendedKeyboardSequences); - -const CtrlShiftExtendedKeyboardSequences = extend(ExtendedKeyboardSequences, { - extraSettings: { - keyDef: { ctrlKey: true, shiftKey: true, altKey: false } - }, - - focusChange: { - keyDef: {ctrlKey: true, shiftKey: true, altKey: false } - }, - - selectedActionChange: { - keyDef: { ctrlKey: true, shiftKey: true, altKey: false } - }, +const AltInputScheme: KeyboardInputScheme = overlayModifierKeys(BaseKeyboardSequences, { altKey: true }); - characterPosition: { - keyDef: { ctrlKey: true, shiftKey: true, altKey: false } - } -}); +const ControlAltInputScheme: KeyboardInputScheme = overlayModifierKeys(BaseKeyboardSequences, { altKey: true, ctrlKey: true }); -const CtrlShiftInputScheme = Object.assign({ - addCommand: { - keyDef: { code: "KeyA", key: "a", ctrlKey: true, shiftKey: true}, - actionName: "addCommand" - }, - addCommandToBeginning: { - keyDef: { code: "KeyB", key: "b", ctrlKey: true, shiftKey: true}, - actionName: "addCommandToBeginning" - }, - addCommandToEnd: { - keyDef: { code: "KeyE", key: "e", ctrlKey: true, shiftKey: true}, - actionName: "addCommandToEnd" - }, - deleteCurrentStep: { - keyDef: { code: "KeyD", key: "d", ctrlKey: true, shiftKey: true}, - actionName: "deleteCurrentStep" - }, - announceScene: { - keyDef: {code: "KeyI", key: "i", ctrlKey: true, shiftKey: true}, - actionName: "announceScene" - }, - decreaseProgramSpeed: { - keyDef: { key: "<", shiftKey: true, hidden: true}, - actionName: "decreaseProgramSpeed" - }, - increaseProgramSpeed: { - keyDef: { key: ">", shiftKey: true, hidden: true}, - actionName: "increaseProgramSpeed" - }, - playPauseProgram: { - keyDef: { code: "KeyP", key: "p", ctrlKey: true, shiftKey: true}, - actionName: "playPauseProgram" - }, - refreshScene: { - keyDef: { code: "KeyR", key: "r", ctrlKey: true, shiftKey: true}, - actionName: "refreshScene" - }, - showHide: { - keyDef: { key: "?" }, - actionName: "showHide" - }, - stopProgram: { - keyDef: {code: "KeyS", key: "s", ctrlKey: true, shiftKey: true}, - actionName: "stopProgram" - }, -}, CtrlShiftExtendedKeyboardSequences); +const CtrlShiftInputScheme: KeyboardInputScheme = overlayModifierKeys(BaseKeyboardSequences, { shiftKey: true, ctrlKey: true }, true); export const KeyboardInputSchemes:KeyboardInputSchemesType = { "controlshift": CtrlShiftInputScheme, diff --git a/src/KeyboardInputSchemes.test.js b/src/KeyboardInputSchemes.test.js index 71c9b7c5..856857b0 100644 --- a/src/KeyboardInputSchemes.test.js +++ b/src/KeyboardInputSchemes.test.js @@ -3,9 +3,10 @@ import {keyboardEventMatchesKeyDef, findKeyboardEventSequenceMatches, isKeyboard import type {KeyDef} from './KeyboardInputSchemes'; it('isKeyboardInputSchemeName', () => { - expect.assertions(5); + expect.assertions(6); expect(isKeyboardInputSchemeName('controlalt')).toBe(true); expect(isKeyboardInputSchemeName('alt')).toBe(true); + expect(isKeyboardInputSchemeName('controlshift')).toBe(true); expect(isKeyboardInputSchemeName('')).toBe(false); expect(isKeyboardInputSchemeName(null)).toBe(false); expect(isKeyboardInputSchemeName('UNKNOWN')).toBe(false); diff --git a/src/Utils.js b/src/Utils.js index 9ce6883d..4560d71f 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -3,6 +3,7 @@ import { isWorldName } from './Worlds'; import type { ThemeName } from './types'; import type { WorldName } from './Worlds'; +import type { KeyDef, KeyboardInputScheme} from './KeyboardInputSchemes'; let idCounter: number = 0; @@ -112,4 +113,37 @@ function parseLoopLabel(label: string): number { return n; }; -export { extend, focusByQuerySelector, generateId, makeDelayedPromise, generateEncodedProgramURL, getThemeFromString, getWorldFromString, generateLoopLabel, parseLoopLabel }; +function overlayModifierKeys (originalBindings: KeyboardInputScheme, modifiers: KeyDef, deep: ?boolean): KeyboardInputScheme { + const modifiedBindings = {}; + const excludedBindingKeys = ["showHide", "decreaseProgramSpeed", "increaseProgramSpeed"]; + + for (const key in originalBindings) { + const value = originalBindings[key] + const isExcluded = excludedBindingKeys.indexOf(key) !== -1; + // $FlowFixMe: Flow doesn't understand that shiftKey shouldn't have to match the [string] rules. + modifiedBindings[key] = isExcluded ? value : overlaySingleLevel(value, modifiers, deep); + } + return modifiedBindings; +} + +function overlaySingleLevel (originalLevel: KeyDef, modifiers: KeyDef, deep: ?boolean ): KeyDef { + const modifiedLevel = {}; + + for (const key in originalLevel) { + const originalValue = originalLevel[key]; + if (key === "keyDef") { + modifiedLevel[key] = extend({}, originalValue, modifiers); + } + else { + modifiedLevel[key] = originalValue; + } + + if (deep && typeof originalValue === "object" && !Array.isArray(originalValue)) { + modifiedLevel[key] = overlaySingleLevel(modifiedLevel[key], modifiers, deep); + } + } + + return modifiedLevel; +} + +export { extend, focusByQuerySelector, generateId, makeDelayedPromise, generateEncodedProgramURL, getThemeFromString, getWorldFromString, generateLoopLabel, parseLoopLabel, overlayModifierKeys }; diff --git a/src/Utils.test.js b/src/Utils.test.js index a3c620c5..04886458 100644 --- a/src/Utils.test.js +++ b/src/Utils.test.js @@ -1,6 +1,6 @@ // @flow -import { extend, generateEncodedProgramURL, getThemeFromString, getWorldFromString, focusByQuerySelector, generateLoopLabel, parseLoopLabel } from './Utils.js'; +import { extend, generateEncodedProgramURL, getThemeFromString, getWorldFromString, focusByQuerySelector, generateLoopLabel, parseLoopLabel, overlayModifierKeys } from './Utils.js'; import React from 'react'; import Adapter from 'enzyme-adapter-react-16'; import { mount, configure } from 'enzyme'; @@ -92,3 +92,13 @@ test('parseLoopLabel', () => { expect(parseLoopLabel('AZ')).toEqual(52); expect(parseLoopLabel('BA')).toEqual(53); }); + +test('Overlay modifiers on a key binding definition (shallow)', () => { + const modifiedBinding = overlayModifierKeys({ escape: { keyDef: { code: "esc" }}}, { altKey: true}); + expect(modifiedBinding).toEqual({ escape: { keyDef: { code: "esc", altKey: true}}}); +}); + +test('Overlay modifiers on a key binding definition (deep)', () => { + const modifiedBinding = overlayModifierKeys({ escape: { keyDef: { code: "esc", x: { keyDef: { code: "x"}} }}}, { ctrlKey: true}, true); + expect(modifiedBinding).toEqual({ escape: { keyDef: { code: "esc", ctrlKey: true, x: { keyDef: { code: "x", ctrlKey: true}}}}}); +}); \ No newline at end of file From 11e5c11210175a61cde3ef8b78ed161ac9e8d9a2 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Mon, 21 Feb 2022 10:18:32 +0100 Subject: [PATCH 6/6] C2LC-448: Updated keyboard tests following audio settings refactor. --- src/App.test.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/App.test.js b/src/App.test.js index cadc3c46..c9b8c0af 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -102,23 +102,24 @@ it('Should be able to handle escaping out of a sequence', () => { app.setState({ keyBindingsEnabled: true}); - expect(app.state().announcementsEnabled).toBe(true); + expect(app.state().settings.theme).toBe("default"); const sequenceWithEscape = [ new KeyboardEvent('keydown', { code: "KeyX", ctrlKey: true, shiftKey: true }), - new KeyboardEvent('keydown', { code: "KeyA" }), + new KeyboardEvent('keydown', { code: "KeyA", ctrlKey: true, shiftKey: true }), // At this point our sequence of (Ctrl+Alt+x, a) matches the beginning // part of the 'select action' group of sequences. // Sending an Escape will break us out of the in-progress sequence. new KeyboardEvent('keydown', { key: "Escape" }), // So that we can send the key sequence to toggle the announcements new KeyboardEvent('keydown', { code: "KeyX", ctrlKey: true, shiftKey: true }), - new KeyboardEvent('keydown', { code: "KeyX" }), + new KeyboardEvent('keydown', { code: "KeyT", ctrlKey: true, shiftKey: true }), + new KeyboardEvent('keydown', { key: "5", ctrlKey: true, shiftKey: true }) ]; for (const keyboardEvent of sequenceWithEscape) { window.document.dispatchEvent(keyboardEvent); } - expect(app.state().announcementsEnabled).toBe(false); + expect(app.state().settings.theme).toBe("contrast"); });