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 '/'
This commit is contained in:
Peter Savchenko 2024-01-28 13:45:01 +03:00 committed by GitHub
parent 9542551d84
commit b619946e8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 117 additions and 20 deletions

View file

@ -32,6 +32,7 @@
"ArrayLike": true,
"InputEvent": true,
"unknown": true,
"requestAnimationFrame": true
"requestAnimationFrame": true,
"navigator": true
}
}

View file

@ -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"

View file

@ -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",

View file

@ -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();
}
}

View file

@ -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<ToolbarNodes> {
/**
* Draws Toolbar elements
*/
private make(): void {
private async make(): Promise<void> {
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<ToolbarNodes> {
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<ToolbarNodes> {
/**
* Make Toolbar
*/
this.make();
void this.make();
}
/**

View file

@ -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<string>;
values(): IterableIterator<string>;
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<KeyboardLayoutMap>;
}
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<string> {
const keyboard = navigator.keyboard;
if (!keyboard) {
return fallback;
}
const map = await keyboard.getLayoutMap();
const key = map.get(code);
return key || fallback;
}

View file

@ -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');
});
});

View file

@ -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 });