diff --git a/.eslintrc b/.eslintrc index 3ee5e6f0..41bddb05 100644 --- a/.eslintrc +++ b/.eslintrc @@ -32,6 +32,7 @@ "ArrayLike": true, "InputEvent": true, "unknown": true, - "requestAnimationFrame": true + "requestAnimationFrame": true, + "navigator": true } } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 295b7367..0a1108fc 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,7 +3,7 @@ ### 2.29.1 - `Fix` — Toolbox wont be shown when Slash pressed with along with Shift or Alt -- `Fix` — Toolbox will be opened when Slash pressed in ASCII-capable keyboard layout. +- `Fix` — Toolbox will be opened when Slash pressed in non-US keyboard layout where there is no physical '/' key. ### 2.29.0 diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts index 077a7dc2..e6425789 100644 --- a/src/components/modules/blockEvents.ts +++ b/src/components/modules/blockEvents.ts @@ -55,14 +55,21 @@ export default class BlockEvents extends Module { } /** - * Keyboard-layout independent handling of Slash key + * We check for "key" here since on different keyboard layouts "/" can be typed as "Shift + 7" etc + * + * @todo probably using "beforeInput" event would be better here */ - if (event.key === '/' || event.code === 'Slash') { - if (event.ctrlKey || event.metaKey) { - this.commandSlashPressed(); - } else if (!event.shiftKey && !event.altKey) { - this.slashPressed(); - } + if (event.key === '/' && !event.ctrlKey && !event.metaKey) { + this.slashPressed(); + } + + /** + * If user pressed "Ctrl + /" or "Cmd + /" — open Block Settings + * We check for "code" here since on different keyboard layouts there can be different keys in place of Slash. + */ + if (event.code === 'Slash' && (event.ctrlKey || event.metaKey)) { + event.preventDefault(); + this.commandSlashPressed(); } } diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index b17af30f..aff4dc4f 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -10,6 +10,7 @@ import Toolbox, { ToolboxEvent } from '../../ui/toolbox'; import { IconMenu, IconPlus } from '@codexteam/icons'; import { BlockHovered } from '../../events/BlockHovered'; import { beautifyShortcut } from '../../utils'; +import { getKeyboardKeyForCode } from '../../utils/keyboard'; /** * @todo Tab on non-empty block should open Block Settings of the hoveredBlock (not where caret is set) @@ -352,7 +353,7 @@ export default class Toolbar extends Module { /** * Draws Toolbar elements */ - private make(): void { + private async make(): Promise { this.nodes.wrapper = $.make('div', this.CSS.toolbar); /** * @todo detect test environment and add data-cy="toolbar" to use it in tests instead of class name @@ -414,10 +415,11 @@ export default class Toolbar extends Module { const blockTunesTooltip = $.make('div'); const blockTunesTooltipEl = $.text(I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune')); + const slashRealKey = await getKeyboardKeyForCode('Slash', '/'); blockTunesTooltip.appendChild(blockTunesTooltipEl); blockTunesTooltip.appendChild($.make('div', this.CSS.plusButtonShortcut, { - textContent: beautifyShortcut('CMD + /'), + textContent: beautifyShortcut(`CMD + ${slashRealKey}`), })); tooltip.onHover(this.nodes.settingsToggler, blockTunesTooltip, { @@ -585,7 +587,7 @@ export default class Toolbar extends Module { /** * Make Toolbar */ - this.make(); + void this.make(); } /** diff --git a/src/components/utils/keyboard.ts b/src/components/utils/keyboard.ts new file mode 100644 index 00000000..62586d57 --- /dev/null +++ b/src/components/utils/keyboard.ts @@ -0,0 +1,54 @@ +declare global { + /** + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardLayoutMap + */ + interface KeyboardLayoutMap { + get(key: string): string | undefined; + has(key: string): boolean; + size: number; + entries(): IterableIterator<[string, string]>; + keys(): IterableIterator; + values(): IterableIterator; + forEach(callbackfn: (value: string, key: string, map: KeyboardLayoutMap) => void, thisArg?: unknown): void; + } + + /** + * The getLayoutMap() method of the Keyboard interface returns a Promise + * that resolves with an instance of KeyboardLayoutMap which is a map-like object + * with functions for retrieving the strings associated with specific physical keys. + * https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/getLayoutMap + */ + interface Keyboard { + getLayoutMap(): Promise; + } + + interface Navigator { + /** + * Keyboard API. Not supported by Firefox and Safari. + */ + keyboard?: Keyboard; + } +} + +/** + * Returns real layout-related keyboard key for a given key code. + * For example, for "Slash" it will return "/" on US keyboard and "-" on Spanish keyboard. + * + * Works with Keyboard API which is not supported by Firefox and Safari. So fallback is used for these browsers. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Keyboard + * @param code - {@link https://www.w3.org/TR/uievents-code/#key-alphanumeric-writing-system} + * @param fallback - fallback value to be returned if Keyboard API is not supported (Safari, Firefox) + */ +export async function getKeyboardKeyForCode(code: string, fallback: string): Promise { + const keyboard = navigator.keyboard; + + if (!keyboard) { + return fallback; + } + + const map = await keyboard.getLayoutMap(); + const key = map.get(code); + + return key || fallback; +} diff --git a/test/cypress/tests/modules/BlockEvents/Slash.cy.ts b/test/cypress/tests/modules/BlockEvents/Slash.cy.ts index fcb78891..adf9a207 100644 --- a/test/cypress/tests/modules/BlockEvents/Slash.cy.ts +++ b/test/cypress/tests/modules/BlockEvents/Slash.cy.ts @@ -19,15 +19,13 @@ describe('Slash keydown', function () { .click() .type('/'); - cy.get('[data-cy="toolbox"]') - .get('.ce-popover') + cy.get('[data-cy="toolbox"] .ce-popover') .should('be.visible'); }); [ - '{shift}', - '{alt}', - '{option}', + 'ctrl', + 'cmd', ].forEach((key) => { it(`should not open Toolbox if Slash pressed with ${key}`, () => { cy.createEditor({ @@ -46,10 +44,9 @@ describe('Slash keydown', function () { cy.get('[data-cy=editorjs]') .find('.ce-paragraph') .click() - .type(`${key}/`); + .type(`{${key}}/`); - cy.get('[data-cy="toolbox"]') - .get('.ce-popover') + cy.get('[data-cy="toolbox"] .ce-popover') .should('not.be.visible'); }); }); @@ -75,8 +72,7 @@ describe('Slash keydown', function () { .click() .type('/'); - cy.get('[data-cy="toolbox"]') - .get('.ce-popover') + cy.get('[data-cy="toolbox"] .ce-popover') .should('not.be.visible'); /** @@ -110,8 +106,7 @@ describe('CMD+Slash keydown', function () { .click() .type('{cmd}/'); - cy.get('[data-cy="block-tunes"]') - .get('.ce-popover') + cy.get('[data-cy="block-tunes"] .ce-popover') .should('be.visible'); }); }); diff --git a/test/cypress/tests/utils/flipper.cy.ts b/test/cypress/tests/utils/flipper.cy.ts index 3ef8b01c..50037c9c 100644 --- a/test/cypress/tests/utils/flipper.cy.ts +++ b/test/cypress/tests/utils/flipper.cy.ts @@ -38,7 +38,6 @@ class SomePlugin { describe('Flipper', () => { it('should prevent plugins event handlers from being called while keyboard navigation', () => { - const SLASH_KEY_CODE = 191; const ARROW_DOWN_KEY_CODE = 40; const ENTER_KEY_CODE = 13; @@ -72,7 +71,7 @@ describe('Flipper', () => { cy.get('[data-cy=editorjs]') .get('.cdx-some-plugin') // Open tunes menu - .trigger('keydown', { keyCode: SLASH_KEY_CODE, ctrlKey: true }) + .trigger('keydown', { code: 'Slash', ctrlKey: true }) // Navigate to delete button (the second button) .trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE }) .trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE });