Skip to content

Commit b619946

Browse files
authored
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 '/'
1 parent 9542551 commit b619946

File tree

8 files changed

+117
-20
lines changed

8 files changed

+117
-20
lines changed

.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"ArrayLike": true,
3333
"InputEvent": true,
3434
"unknown": true,
35-
"requestAnimationFrame": true
35+
"requestAnimationFrame": true,
36+
"navigator": true
3637
}
3738
}

docs/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
### 2.29.1
4+
5+
- `Fix` — Toolbox wont be shown when Slash pressed with along with Shift or Alt
6+
- `Fix` — Toolbox will be opened when Slash pressed in non-US keyboard layout where there is no physical '/' key.
7+
38
### 2.29.0
49

510
- `New` — Editor Config now has the `style.nonce` attribute that could be used to allowlist editor style tag for Content Security Policy "style-src"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@editorjs/editorjs",
3-
"version": "2.29.0",
3+
"version": "2.29.1",
44
"description": "Editor.js — Native JS, based on API and Open Source",
55
"main": "dist/editorjs.umd.js",
66
"module": "dist/editorjs.mjs",

src/components/modules/blockEvents.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,24 @@ export default class BlockEvents extends Module {
5252
case _.keyCodes.TAB:
5353
this.tabPressed(event);
5454
break;
55-
case _.keyCodes.SLASH:
56-
if (event.ctrlKey || event.metaKey) {
57-
this.commandSlashPressed();
58-
} else {
59-
this.slashPressed();
60-
}
61-
break;
55+
}
56+
57+
/**
58+
* We check for "key" here since on different keyboard layouts "/" can be typed as "Shift + 7" etc
59+
*
60+
* @todo probably using "beforeInput" event would be better here
61+
*/
62+
if (event.key === '/' && !event.ctrlKey && !event.metaKey) {
63+
this.slashPressed();
64+
}
65+
66+
/**
67+
* If user pressed "Ctrl + /" or "Cmd + /" — open Block Settings
68+
* We check for "code" here since on different keyboard layouts there can be different keys in place of Slash.
69+
*/
70+
if (event.code === 'Slash' && (event.ctrlKey || event.metaKey)) {
71+
event.preventDefault();
72+
this.commandSlashPressed();
6273
}
6374
}
6475

src/components/modules/toolbar/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Toolbox, { ToolboxEvent } from '../../ui/toolbox';
1010
import { IconMenu, IconPlus } from '@codexteam/icons';
1111
import { BlockHovered } from '../../events/BlockHovered';
1212
import { beautifyShortcut } from '../../utils';
13+
import { getKeyboardKeyForCode } from '../../utils/keyboard';
1314

1415
/**
1516
* @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> {
352353
/**
353354
* Draws Toolbar elements
354355
*/
355-
private make(): void {
356+
private async make(): Promise<void> {
356357
this.nodes.wrapper = $.make('div', this.CSS.toolbar);
357358
/**
358359
* @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> {
414415

415416
const blockTunesTooltip = $.make('div');
416417
const blockTunesTooltipEl = $.text(I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'));
418+
const slashRealKey = await getKeyboardKeyForCode('Slash', '/');
417419

418420
blockTunesTooltip.appendChild(blockTunesTooltipEl);
419421
blockTunesTooltip.appendChild($.make('div', this.CSS.plusButtonShortcut, {
420-
textContent: beautifyShortcut('CMD + /'),
422+
textContent: beautifyShortcut(`CMD + ${slashRealKey}`),
421423
}));
422424

423425
tooltip.onHover(this.nodes.settingsToggler, blockTunesTooltip, {
@@ -585,7 +587,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
585587
/**
586588
* Make Toolbar
587589
*/
588-
this.make();
590+
void this.make();
589591
}
590592

591593
/**

src/components/utils/keyboard.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
declare global {
2+
/**
3+
* https://developer.mozilla.org/en-US/docs/Web/API/KeyboardLayoutMap
4+
*/
5+
interface KeyboardLayoutMap {
6+
get(key: string): string | undefined;
7+
has(key: string): boolean;
8+
size: number;
9+
entries(): IterableIterator<[string, string]>;
10+
keys(): IterableIterator<string>;
11+
values(): IterableIterator<string>;
12+
forEach(callbackfn: (value: string, key: string, map: KeyboardLayoutMap) => void, thisArg?: unknown): void;
13+
}
14+
15+
/**
16+
* The getLayoutMap() method of the Keyboard interface returns a Promise
17+
* that resolves with an instance of KeyboardLayoutMap which is a map-like object
18+
* with functions for retrieving the strings associated with specific physical keys.
19+
* https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/getLayoutMap
20+
*/
21+
interface Keyboard {
22+
getLayoutMap(): Promise<KeyboardLayoutMap>;
23+
}
24+
25+
interface Navigator {
26+
/**
27+
* Keyboard API. Not supported by Firefox and Safari.
28+
*/
29+
keyboard?: Keyboard;
30+
}
31+
}
32+
33+
/**
34+
* Returns real layout-related keyboard key for a given key code.
35+
* For example, for "Slash" it will return "/" on US keyboard and "-" on Spanish keyboard.
36+
*
37+
* Works with Keyboard API which is not supported by Firefox and Safari. So fallback is used for these browsers.
38+
*
39+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Keyboard
40+
* @param code - {@link https://www.w3.org/TR/uievents-code/#key-alphanumeric-writing-system}
41+
* @param fallback - fallback value to be returned if Keyboard API is not supported (Safari, Firefox)
42+
*/
43+
export async function getKeyboardKeyForCode(code: string, fallback: string): Promise<string> {
44+
const keyboard = navigator.keyboard;
45+
46+
if (!keyboard) {
47+
return fallback;
48+
}
49+
50+
const map = await keyboard.getLayoutMap();
51+
const key = map.get(code);
52+
53+
return key || fallback;
54+
}

test/cypress/tests/modules/BlockEvents/Slash.cy.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,37 @@ describe('Slash keydown', function () {
1919
.click()
2020
.type('/');
2121

22-
cy.get('[data-cy="toolbox"]')
23-
.get('.ce-popover')
22+
cy.get('[data-cy="toolbox"] .ce-popover')
2423
.should('be.visible');
2524
});
25+
26+
[
27+
'ctrl',
28+
'cmd',
29+
].forEach((key) => {
30+
it(`should not open Toolbox if Slash pressed with ${key}`, () => {
31+
cy.createEditor({
32+
data: {
33+
blocks: [
34+
{
35+
type: 'paragraph',
36+
data: {
37+
text: '',
38+
},
39+
},
40+
],
41+
},
42+
});
43+
44+
cy.get('[data-cy=editorjs]')
45+
.find('.ce-paragraph')
46+
.click()
47+
.type(`{${key}}/`);
48+
49+
cy.get('[data-cy="toolbox"] .ce-popover')
50+
.should('not.be.visible');
51+
});
52+
});
2653
});
2754

2855
describe('pressed in non-empty block', function () {
@@ -45,8 +72,7 @@ describe('Slash keydown', function () {
4572
.click()
4673
.type('/');
4774

48-
cy.get('[data-cy="toolbox"]')
49-
.get('.ce-popover')
75+
cy.get('[data-cy="toolbox"] .ce-popover')
5076
.should('not.be.visible');
5177

5278
/**
@@ -80,8 +106,7 @@ describe('CMD+Slash keydown', function () {
80106
.click()
81107
.type('{cmd}/');
82108

83-
cy.get('[data-cy="block-tunes"]')
84-
.get('.ce-popover')
109+
cy.get('[data-cy="block-tunes"] .ce-popover')
85110
.should('be.visible');
86111
});
87112
});

test/cypress/tests/utils/flipper.cy.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ class SomePlugin {
3838

3939
describe('Flipper', () => {
4040
it('should prevent plugins event handlers from being called while keyboard navigation', () => {
41-
const SLASH_KEY_CODE = 191;
4241
const ARROW_DOWN_KEY_CODE = 40;
4342
const ENTER_KEY_CODE = 13;
4443

@@ -72,7 +71,7 @@ describe('Flipper', () => {
7271
cy.get('[data-cy=editorjs]')
7372
.get('.cdx-some-plugin')
7473
// Open tunes menu
75-
.trigger('keydown', { keyCode: SLASH_KEY_CODE, ctrlKey: true })
74+
.trigger('keydown', { code: 'Slash', ctrlKey: true })
7675
// Navigate to delete button (the second button)
7776
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE })
7877
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE });

0 commit comments

Comments
 (0)