From b619946e8f23ebc9ccfc41ef653439278e99491e Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Sun, 28 Jan 2024 13:45:01 +0300 Subject: [PATCH] fix(slash): do not handle / + shift/alt, support for ascii keyboard (#2599) * fix(slash): do not handle / + shift/alt, support for ascii keyboard * support keyboards without physical '/' --- .eslintrc | 3 +- docs/CHANGELOG.md | 5 ++ package.json | 2 +- src/components/modules/blockEvents.ts | 25 ++++++--- src/components/modules/toolbar/index.ts | 8 +-- src/components/utils/keyboard.ts | 54 +++++++++++++++++++ .../tests/modules/BlockEvents/Slash.cy.ts | 37 ++++++++++--- test/cypress/tests/utils/flipper.cy.ts | 3 +- 8 files changed, 117 insertions(+), 20 deletions(-) create mode 100644 src/components/utils/keyboard.ts 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 0f886c5c..0a1108fc 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### 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 non-US keyboard layout where there is no physical '/' key. + ### 2.29.0 - `New` — Editor Config now has the `style.nonce` attribute that could be used to allowlist editor style tag for Content Security Policy "style-src" diff --git a/package.json b/package.json index a45444db..d00b8af2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.29.0", + "version": "2.29.1", "description": "Editor.js — Native JS, based on API and Open Source", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts index ba5ffbed..e6425789 100644 --- a/src/components/modules/blockEvents.ts +++ b/src/components/modules/blockEvents.ts @@ -52,13 +52,24 @@ export default class BlockEvents extends Module { case _.keyCodes.TAB: this.tabPressed(event); break; - case _.keyCodes.SLASH: - if (event.ctrlKey || event.metaKey) { - this.commandSlashPressed(); - } else { - this.slashPressed(); - } - break; + } + + /** + * 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.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 281e3012..adf9a207 100644 --- a/test/cypress/tests/modules/BlockEvents/Slash.cy.ts +++ b/test/cypress/tests/modules/BlockEvents/Slash.cy.ts @@ -19,10 +19,37 @@ describe('Slash keydown', function () { .click() .type('/'); - cy.get('[data-cy="toolbox"]') - .get('.ce-popover') + cy.get('[data-cy="toolbox"] .ce-popover') .should('be.visible'); }); + + [ + 'ctrl', + 'cmd', + ].forEach((key) => { + it(`should not open Toolbox if Slash pressed with ${key}`, () => { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: '', + }, + }, + ], + }, + }); + + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .click() + .type(`{${key}}/`); + + cy.get('[data-cy="toolbox"] .ce-popover') + .should('not.be.visible'); + }); + }); }); describe('pressed in non-empty block', function () { @@ -45,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'); /** @@ -80,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 });