mirror of
https://github.com/google/blockly.git
synced 2026-01-07 17:10:11 +01:00
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.
This commit is contained in:
@@ -8,11 +8,13 @@
|
|||||||
|
|
||||||
import type {Block} from './block.js';
|
import type {Block} from './block.js';
|
||||||
import {BlockDefinition, Blocks} from './blocks.js';
|
import {BlockDefinition, Blocks} from './blocks.js';
|
||||||
|
import * as browserEvents from './browser_events.js';
|
||||||
import type {Connection} from './connection.js';
|
import type {Connection} from './connection.js';
|
||||||
import {EventType} from './events/type.js';
|
import {EventType} from './events/type.js';
|
||||||
import * as eventUtils from './events/utils.js';
|
import * as eventUtils from './events/utils.js';
|
||||||
import {getFocusManager} from './focus_manager.js';
|
import {getFocusManager} from './focus_manager.js';
|
||||||
import {ISelectable, isSelectable} from './interfaces/i_selectable.js';
|
import {ISelectable, isSelectable} from './interfaces/i_selectable.js';
|
||||||
|
import {ShortcutRegistry} from './shortcut_registry.js';
|
||||||
import type {Workspace} from './workspace.js';
|
import type {Workspace} from './workspace.js';
|
||||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||||
|
|
||||||
@@ -310,4 +312,29 @@ export function defineBlocks(blocks: {[key: string]: BlockDefinition}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a key-down on SVG drawing surface. Does nothing if the main workspace
|
||||||
|
* is not visible.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* @param e Key down event.
|
||||||
|
*/
|
||||||
|
export function globalShortcutHandler(e: KeyboardEvent) {
|
||||||
|
const mainWorkspace = getMainWorkspace() as WorkspaceSvg;
|
||||||
|
if (!mainWorkspace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
browserEvents.isTargetInput(e) ||
|
||||||
|
(mainWorkspace.rendered && !mainWorkspace.isVisible())
|
||||||
|
) {
|
||||||
|
// When focused on an HTML text input widget, don't trap any keys.
|
||||||
|
// Ignore keypresses on rendered workspaces that have been explicitly
|
||||||
|
// hidden.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ShortcutRegistry.registry.onKeyDown(mainWorkspace, e);
|
||||||
|
}
|
||||||
|
|
||||||
export const TEST_ONLY = {defineBlocksWithJsonArrayInternal};
|
export const TEST_ONLY = {defineBlocksWithJsonArrayInternal};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
// Former goog.module ID: Blockly.dropDownDiv
|
// Former goog.module ID: Blockly.dropDownDiv
|
||||||
|
|
||||||
import type {BlockSvg} from './block_svg.js';
|
import type {BlockSvg} from './block_svg.js';
|
||||||
|
import * as browserEvents from './browser_events.js';
|
||||||
import * as common from './common.js';
|
import * as common from './common.js';
|
||||||
import type {Field} from './field.js';
|
import type {Field} from './field.js';
|
||||||
import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js';
|
import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js';
|
||||||
@@ -86,6 +87,9 @@ let positionToField: boolean | null = null;
|
|||||||
/** Callback to FocusManager to return ephemeral focus when the div closes. */
|
/** Callback to FocusManager to return ephemeral focus when the div closes. */
|
||||||
let returnEphemeralFocus: ReturnEphemeralFocus | null = null;
|
let returnEphemeralFocus: ReturnEphemeralFocus | null = null;
|
||||||
|
|
||||||
|
/** Identifier for shortcut keydown listener used to unbind it. */
|
||||||
|
let keydownListener: browserEvents.Data | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dropdown bounds info object used to encapsulate sizing information about a
|
* Dropdown bounds info object used to encapsulate sizing information about a
|
||||||
* bounding element (bounding box and width/height).
|
* bounding element (bounding box and width/height).
|
||||||
@@ -130,6 +134,13 @@ export function createDom() {
|
|||||||
content.className = 'blocklyDropDownContent';
|
content.className = 'blocklyDropDownContent';
|
||||||
div.appendChild(content);
|
div.appendChild(content);
|
||||||
|
|
||||||
|
keydownListener = browserEvents.conditionalBind(
|
||||||
|
content,
|
||||||
|
'keydown',
|
||||||
|
null,
|
||||||
|
common.globalShortcutHandler,
|
||||||
|
);
|
||||||
|
|
||||||
arrow = document.createElement('div');
|
arrow = document.createElement('div');
|
||||||
arrow.className = 'blocklyDropDownArrow';
|
arrow.className = 'blocklyDropDownArrow';
|
||||||
div.appendChild(arrow);
|
div.appendChild(arrow);
|
||||||
@@ -168,6 +179,10 @@ export function getContentDiv(): HTMLDivElement {
|
|||||||
|
|
||||||
/** Clear the content of the drop-down. */
|
/** Clear the content of the drop-down. */
|
||||||
export function clearContent() {
|
export function clearContent() {
|
||||||
|
if (keydownListener) {
|
||||||
|
browserEvents.unbind(keydownListener);
|
||||||
|
keydownListener = null;
|
||||||
|
}
|
||||||
div.remove();
|
div.remove();
|
||||||
createDom();
|
createDom();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import * as dropDownDiv from './dropdowndiv.js';
|
|||||||
import {Grid} from './grid.js';
|
import {Grid} from './grid.js';
|
||||||
import {Options} from './options.js';
|
import {Options} from './options.js';
|
||||||
import {ScrollbarPair} from './scrollbar_pair.js';
|
import {ScrollbarPair} from './scrollbar_pair.js';
|
||||||
import {ShortcutRegistry} from './shortcut_registry.js';
|
|
||||||
import * as Tooltip from './tooltip.js';
|
import * as Tooltip from './tooltip.js';
|
||||||
import * as Touch from './touch.js';
|
import * as Touch from './touch.js';
|
||||||
import * as dom from './utils/dom.js';
|
import * as dom from './utils/dom.js';
|
||||||
@@ -72,17 +71,12 @@ export function inject(
|
|||||||
common.setMainWorkspace(workspace);
|
common.setMainWorkspace(workspace);
|
||||||
});
|
});
|
||||||
|
|
||||||
browserEvents.conditionalBind(subContainer, 'keydown', null, onKeyDown);
|
|
||||||
browserEvents.conditionalBind(
|
browserEvents.conditionalBind(
|
||||||
dropDownDiv.getContentDiv(),
|
subContainer,
|
||||||
'keydown',
|
'keydown',
|
||||||
null,
|
null,
|
||||||
onKeyDown,
|
common.globalShortcutHandler,
|
||||||
);
|
);
|
||||||
const widgetContainer = WidgetDiv.getDiv();
|
|
||||||
if (widgetContainer) {
|
|
||||||
browserEvents.conditionalBind(widgetContainer, 'keydown', null, onKeyDown);
|
|
||||||
}
|
|
||||||
|
|
||||||
return workspace;
|
return workspace;
|
||||||
}
|
}
|
||||||
@@ -292,32 +286,6 @@ function init(mainWorkspace: WorkspaceSvg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a key-down on SVG drawing surface. Does nothing if the main workspace
|
|
||||||
* is not visible.
|
|
||||||
*
|
|
||||||
* @param e Key down event.
|
|
||||||
*/
|
|
||||||
// TODO (https://github.com/google/blockly/issues/1998) handle cases where there
|
|
||||||
// are multiple workspaces and non-main workspaces are able to accept input.
|
|
||||||
function onKeyDown(e: KeyboardEvent) {
|
|
||||||
const mainWorkspace = common.getMainWorkspace() as WorkspaceSvg;
|
|
||||||
if (!mainWorkspace) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
browserEvents.isTargetInput(e) ||
|
|
||||||
(mainWorkspace.rendered && !mainWorkspace.isVisible())
|
|
||||||
) {
|
|
||||||
// When focused on an HTML text input widget, don't trap any keys.
|
|
||||||
// Ignore keypresses on rendered workspaces that have been explicitly
|
|
||||||
// hidden.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ShortcutRegistry.registry.onKeyDown(mainWorkspace, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether event handlers have been bound. Document event handlers will only
|
* Whether event handlers have been bound. Document event handlers will only
|
||||||
* be bound once, even if Blockly is destroyed and reinjected.
|
* be bound once, even if Blockly is destroyed and reinjected.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
// Former goog.module ID: Blockly.WidgetDiv
|
// Former goog.module ID: Blockly.WidgetDiv
|
||||||
|
|
||||||
|
import * as browserEvents from './browser_events.js';
|
||||||
import * as common from './common.js';
|
import * as common from './common.js';
|
||||||
import {Field} from './field.js';
|
import {Field} from './field.js';
|
||||||
import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js';
|
import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js';
|
||||||
@@ -66,15 +67,23 @@ export function testOnly_setDiv(newDiv: HTMLDivElement | null) {
|
|||||||
export function createDom() {
|
export function createDom() {
|
||||||
const container = common.getParentContainer() || document.body;
|
const container = common.getParentContainer() || document.body;
|
||||||
|
|
||||||
if (document.querySelector('.' + containerClassName)) {
|
const existingContainer = document.querySelector('div.' + containerClassName);
|
||||||
containerDiv = document.querySelector('.' + containerClassName);
|
if (existingContainer) {
|
||||||
|
containerDiv = existingContainer as HTMLDivElement;
|
||||||
} else {
|
} else {
|
||||||
containerDiv = document.createElement('div') as HTMLDivElement;
|
containerDiv = document.createElement('div');
|
||||||
containerDiv.className = containerClassName;
|
containerDiv.className = containerClassName;
|
||||||
containerDiv.tabIndex = -1;
|
containerDiv.tabIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.appendChild(containerDiv!);
|
browserEvents.conditionalBind(
|
||||||
|
containerDiv,
|
||||||
|
'keydown',
|
||||||
|
null,
|
||||||
|
common.globalShortcutHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
container.appendChild(containerDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -136,6 +136,39 @@ suite('DropDownDiv', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
suite('Keyboard Shortcuts', function () {
|
||||||
|
setup(function () {
|
||||||
|
this.boundsStub = sinon
|
||||||
|
.stub(Blockly.DropDownDiv.TEST_ONLY, 'getBoundsInfo')
|
||||||
|
.returns({
|
||||||
|
left: 0,
|
||||||
|
right: 100,
|
||||||
|
top: 0,
|
||||||
|
bottom: 100,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||||
|
});
|
||||||
|
teardown(function () {
|
||||||
|
this.boundsStub.restore();
|
||||||
|
});
|
||||||
|
test('Escape dismisses DropDownDiv', function () {
|
||||||
|
let hidden = false;
|
||||||
|
Blockly.DropDownDiv.show(this, false, 0, 0, 0, 0, false, () => {
|
||||||
|
hidden = true;
|
||||||
|
});
|
||||||
|
assert.isFalse(hidden);
|
||||||
|
Blockly.DropDownDiv.getContentDiv().dispatchEvent(
|
||||||
|
new KeyboardEvent('keydown', {
|
||||||
|
key: 'Escape',
|
||||||
|
keyCode: 27, // example values.
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert.isTrue(hidden);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
suite('show()', function () {
|
suite('show()', function () {
|
||||||
test('without bounds set throws error', function () {
|
test('without bounds set throws error', function () {
|
||||||
const block = this.setUpBlockWithField();
|
const block = this.setUpBlockWithField();
|
||||||
|
|||||||
@@ -287,6 +287,29 @@ suite('WidgetDiv', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
suite('Keyboard Shortcuts', function () {
|
||||||
|
test('Escape dismisses WidgetDiv', function () {
|
||||||
|
let hidden = false;
|
||||||
|
Blockly.WidgetDiv.show(
|
||||||
|
this,
|
||||||
|
false,
|
||||||
|
() => {
|
||||||
|
hidden = true;
|
||||||
|
},
|
||||||
|
this.workspace,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.isFalse(hidden);
|
||||||
|
Blockly.WidgetDiv.getDiv().dispatchEvent(
|
||||||
|
new KeyboardEvent('keydown', {
|
||||||
|
key: 'Escape',
|
||||||
|
keyCode: 27, // example values.
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert.isTrue(hidden);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
suite('show()', function () {
|
suite('show()', function () {
|
||||||
test('shows nowhere', function () {
|
test('shows nowhere', function () {
|
||||||
const block = this.setUpBlockWithField();
|
const block = this.setUpBlockWithField();
|
||||||
|
|||||||
Reference in New Issue
Block a user