Skip to content

Commit d2c4016

Browse files
authored
fix: Fix bug that prevented using keyboard shortcuts when the DropDownDiv is open. (#9085)
* fix: Fix bug that prevented using keyboard shortcuts when the DropDownDiv is open. * chore: Remove obsolete comment. * Refactor: Remove unreachable null check. * chore: Add tests for handling Escape to dismiss the Widget/DropDownDivs. * chore: Satisfy the linter. * fix: Fix post-merge test failure.
1 parent ab15372 commit d2c4016

File tree

6 files changed

+113
-38
lines changed

6 files changed

+113
-38
lines changed

core/common.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88

99
import type {Block} from './block.js';
1010
import {BlockDefinition, Blocks} from './blocks.js';
11+
import * as browserEvents from './browser_events.js';
1112
import type {Connection} from './connection.js';
1213
import {EventType} from './events/type.js';
1314
import * as eventUtils from './events/utils.js';
1415
import {getFocusManager} from './focus_manager.js';
1516
import {ISelectable, isSelectable} from './interfaces/i_selectable.js';
17+
import {ShortcutRegistry} from './shortcut_registry.js';
1618
import type {Workspace} from './workspace.js';
1719
import type {WorkspaceSvg} from './workspace_svg.js';
1820

@@ -310,4 +312,29 @@ export function defineBlocks(blocks: {[key: string]: BlockDefinition}) {
310312
}
311313
}
312314

315+
/**
316+
* Handle a key-down on SVG drawing surface. Does nothing if the main workspace
317+
* is not visible.
318+
*
319+
* @internal
320+
* @param e Key down event.
321+
*/
322+
export function globalShortcutHandler(e: KeyboardEvent) {
323+
const mainWorkspace = getMainWorkspace() as WorkspaceSvg;
324+
if (!mainWorkspace) {
325+
return;
326+
}
327+
328+
if (
329+
browserEvents.isTargetInput(e) ||
330+
(mainWorkspace.rendered && !mainWorkspace.isVisible())
331+
) {
332+
// When focused on an HTML text input widget, don't trap any keys.
333+
// Ignore keypresses on rendered workspaces that have been explicitly
334+
// hidden.
335+
return;
336+
}
337+
ShortcutRegistry.registry.onKeyDown(mainWorkspace, e);
338+
}
339+
313340
export const TEST_ONLY = {defineBlocksWithJsonArrayInternal};

core/dropdowndiv.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// Former goog.module ID: Blockly.dropDownDiv
1414

1515
import type {BlockSvg} from './block_svg.js';
16+
import * as browserEvents from './browser_events.js';
1617
import * as common from './common.js';
1718
import type {Field} from './field.js';
1819
import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js';
@@ -86,6 +87,9 @@ let positionToField: boolean | null = null;
8687
/** Callback to FocusManager to return ephemeral focus when the div closes. */
8788
let returnEphemeralFocus: ReturnEphemeralFocus | null = null;
8889

90+
/** Identifier for shortcut keydown listener used to unbind it. */
91+
let keydownListener: browserEvents.Data | null = null;
92+
8993
/**
9094
* Dropdown bounds info object used to encapsulate sizing information about a
9195
* bounding element (bounding box and width/height).
@@ -130,6 +134,13 @@ export function createDom() {
130134
content.className = 'blocklyDropDownContent';
131135
div.appendChild(content);
132136

137+
keydownListener = browserEvents.conditionalBind(
138+
content,
139+
'keydown',
140+
null,
141+
common.globalShortcutHandler,
142+
);
143+
133144
arrow = document.createElement('div');
134145
arrow.className = 'blocklyDropDownArrow';
135146
div.appendChild(arrow);
@@ -168,6 +179,10 @@ export function getContentDiv(): HTMLDivElement {
168179

169180
/** Clear the content of the drop-down. */
170181
export function clearContent() {
182+
if (keydownListener) {
183+
browserEvents.unbind(keydownListener);
184+
keydownListener = null;
185+
}
171186
div.remove();
172187
createDom();
173188
}

core/inject.ts

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import * as dropDownDiv from './dropdowndiv.js';
1515
import {Grid} from './grid.js';
1616
import {Options} from './options.js';
1717
import {ScrollbarPair} from './scrollbar_pair.js';
18-
import {ShortcutRegistry} from './shortcut_registry.js';
1918
import * as Tooltip from './tooltip.js';
2019
import * as Touch from './touch.js';
2120
import * as dom from './utils/dom.js';
@@ -72,17 +71,12 @@ export function inject(
7271
common.setMainWorkspace(workspace);
7372
});
7473

75-
browserEvents.conditionalBind(subContainer, 'keydown', null, onKeyDown);
7674
browserEvents.conditionalBind(
77-
dropDownDiv.getContentDiv(),
75+
subContainer,
7876
'keydown',
7977
null,
80-
onKeyDown,
78+
common.globalShortcutHandler,
8179
);
82-
const widgetContainer = WidgetDiv.getDiv();
83-
if (widgetContainer) {
84-
browserEvents.conditionalBind(widgetContainer, 'keydown', null, onKeyDown);
85-
}
8680

8781
return workspace;
8882
}
@@ -292,32 +286,6 @@ function init(mainWorkspace: WorkspaceSvg) {
292286
}
293287
}
294288

295-
/**
296-
* Handle a key-down on SVG drawing surface. Does nothing if the main workspace
297-
* is not visible.
298-
*
299-
* @param e Key down event.
300-
*/
301-
// TODO (https://github.com/google/blockly/issues/1998) handle cases where there
302-
// are multiple workspaces and non-main workspaces are able to accept input.
303-
function onKeyDown(e: KeyboardEvent) {
304-
const mainWorkspace = common.getMainWorkspace() as WorkspaceSvg;
305-
if (!mainWorkspace) {
306-
return;
307-
}
308-
309-
if (
310-
browserEvents.isTargetInput(e) ||
311-
(mainWorkspace.rendered && !mainWorkspace.isVisible())
312-
) {
313-
// When focused on an HTML text input widget, don't trap any keys.
314-
// Ignore keypresses on rendered workspaces that have been explicitly
315-
// hidden.
316-
return;
317-
}
318-
ShortcutRegistry.registry.onKeyDown(mainWorkspace, e);
319-
}
320-
321289
/**
322290
* Whether event handlers have been bound. Document event handlers will only
323291
* be bound once, even if Blockly is destroyed and reinjected.

core/widgetdiv.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
// Former goog.module ID: Blockly.WidgetDiv
88

9+
import * as browserEvents from './browser_events.js';
910
import * as common from './common.js';
1011
import {Field} from './field.js';
1112
import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js';
@@ -66,15 +67,23 @@ export function testOnly_setDiv(newDiv: HTMLDivElement | null) {
6667
export function createDom() {
6768
const container = common.getParentContainer() || document.body;
6869

69-
if (document.querySelector('.' + containerClassName)) {
70-
containerDiv = document.querySelector('.' + containerClassName);
70+
const existingContainer = document.querySelector('div.' + containerClassName);
71+
if (existingContainer) {
72+
containerDiv = existingContainer as HTMLDivElement;
7173
} else {
72-
containerDiv = document.createElement('div') as HTMLDivElement;
74+
containerDiv = document.createElement('div');
7375
containerDiv.className = containerClassName;
7476
containerDiv.tabIndex = -1;
7577
}
7678

77-
container.appendChild(containerDiv!);
79+
browserEvents.conditionalBind(
80+
containerDiv,
81+
'keydown',
82+
null,
83+
common.globalShortcutHandler,
84+
);
85+
86+
container.appendChild(containerDiv);
7887
}
7988

8089
/**

tests/mocha/dropdowndiv_test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,39 @@ suite('DropDownDiv', function () {
136136
});
137137
});
138138

139+
suite('Keyboard Shortcuts', function () {
140+
setup(function () {
141+
this.boundsStub = sinon
142+
.stub(Blockly.DropDownDiv.TEST_ONLY, 'getBoundsInfo')
143+
.returns({
144+
left: 0,
145+
right: 100,
146+
top: 0,
147+
bottom: 100,
148+
width: 100,
149+
height: 100,
150+
});
151+
this.workspace = Blockly.inject('blocklyDiv', {});
152+
});
153+
teardown(function () {
154+
this.boundsStub.restore();
155+
});
156+
test('Escape dismisses DropDownDiv', function () {
157+
let hidden = false;
158+
Blockly.DropDownDiv.show(this, false, 0, 0, 0, 0, false, () => {
159+
hidden = true;
160+
});
161+
assert.isFalse(hidden);
162+
Blockly.DropDownDiv.getContentDiv().dispatchEvent(
163+
new KeyboardEvent('keydown', {
164+
key: 'Escape',
165+
keyCode: 27, // example values.
166+
}),
167+
);
168+
assert.isTrue(hidden);
169+
});
170+
});
171+
139172
suite('show()', function () {
140173
test('without bounds set throws error', function () {
141174
const block = this.setUpBlockWithField();

tests/mocha/widget_div_test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,29 @@ suite('WidgetDiv', function () {
287287
});
288288
});
289289

290+
suite('Keyboard Shortcuts', function () {
291+
test('Escape dismisses WidgetDiv', function () {
292+
let hidden = false;
293+
Blockly.WidgetDiv.show(
294+
this,
295+
false,
296+
() => {
297+
hidden = true;
298+
},
299+
this.workspace,
300+
false,
301+
);
302+
assert.isFalse(hidden);
303+
Blockly.WidgetDiv.getDiv().dispatchEvent(
304+
new KeyboardEvent('keydown', {
305+
key: 'Escape',
306+
keyCode: 27, // example values.
307+
}),
308+
);
309+
assert.isTrue(hidden);
310+
});
311+
});
312+
290313
suite('show()', function () {
291314
test('shows nowhere', function () {
292315
const block = this.setUpBlockWithField();

0 commit comments

Comments
 (0)