diff --git a/packages/blockly/core/widgetdiv.ts b/packages/blockly/core/widgetdiv.ts index ab894c2820f..f576503e29d 100644 --- a/packages/blockly/core/widgetdiv.ts +++ b/packages/blockly/core/widgetdiv.ts @@ -259,13 +259,13 @@ export function hideIfOwnerIsInWorkspace(workspace: WorkspaceSvg) { let ownerIsInWorkspace = ownerWorkspace === null; // Check if the given workspace is a parent workspace of the one containing // our owner. - let currentWorkspace: WorkspaceSvg | null = workspace; + let currentWorkspace: WorkspaceSvg | null = ownerWorkspace; while (!ownerIsInWorkspace && currentWorkspace) { if (currentWorkspace === workspace) { ownerIsInWorkspace = true; break; } - currentWorkspace = workspace.options.parentWorkspace; + currentWorkspace = currentWorkspace.options.parentWorkspace; } if (ownerIsInWorkspace) { diff --git a/packages/blockly/tests/mocha/widget_div_test.ts b/packages/blockly/tests/mocha/widget_div_test.ts index f6ea1b545b1..e6ea808b394 100644 --- a/packages/blockly/tests/mocha/widget_div_test.ts +++ b/packages/blockly/tests/mocha/widget_div_test.ts @@ -477,4 +477,66 @@ suite('WidgetDiv', function () { ); }); }); + + suite('hideIfOwnerIsInWorkspace()', function () { + let otherDiv: HTMLDivElement; + let otherWorkspace: Blockly.WorkspaceSvg; + + setup(function () { + otherDiv = document.createElement('div'); + document.body.appendChild(otherDiv); + otherWorkspace = Blockly.inject(otherDiv, DEFAULT_INJECT_OPTIONS); + }); + + teardown(function () { + otherWorkspace.dispose(); + otherDiv.remove(); + }); + + async function openMutatorWorkspace() { + const block = workspace.newBlock('controls_if'); + block.initSvg(); + block.render(); + const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE); + assert.isDefined(icon); + await icon.setBubbleVisible(true); + const mutatorWorkspace = icon.getWorkspace(); + assert.isDefined(mutatorWorkspace); + return mutatorWorkspace; + } + + test('hides div owned by the given workspace', function () { + Blockly.WidgetDiv.show({}, false, () => {}, workspace); + + Blockly.WidgetDiv.hideIfOwnerIsInWorkspace(workspace); + + assert.isFalse(Blockly.WidgetDiv.isVisible()); + }); + + test('does not hide div owned by an unrelated workspace', function () { + Blockly.WidgetDiv.show({}, false, () => {}, workspace); + + Blockly.WidgetDiv.hideIfOwnerIsInWorkspace(otherWorkspace); + + assert.isTrue(Blockly.WidgetDiv.isVisible()); + }); + + test('hides div owned by a child mutator workspace', async function () { + const mutatorWorkspace = await openMutatorWorkspace(); + Blockly.WidgetDiv.show({}, false, () => {}, mutatorWorkspace); + + Blockly.WidgetDiv.hideIfOwnerIsInWorkspace(workspace); + + assert.isFalse(Blockly.WidgetDiv.isVisible()); + }); + + test('does not hide div owned by the parent of a mutator workspace', async function () { + const mutatorWorkspace = await openMutatorWorkspace(); + Blockly.WidgetDiv.show({}, false, () => {}, workspace); + + Blockly.WidgetDiv.hideIfOwnerIsInWorkspace(mutatorWorkspace); + + assert.isTrue(Blockly.WidgetDiv.isVisible()); + }); + }); }); diff --git a/packages/plugins/workspace-minimap/test/minimap_tests.mocha.js b/packages/plugins/workspace-minimap/test/minimap_tests.mocha.js index 493c52364f1..095b31e86bd 100644 --- a/packages/plugins/workspace-minimap/test/minimap_tests.mocha.js +++ b/packages/plugins/workspace-minimap/test/minimap_tests.mocha.js @@ -441,6 +441,7 @@ suite('Mirroring events', function () { '
', ); global.SVGElement = window.SVGElement; + global.FocusEvent = window.FocusEvent; window.HTMLCanvasElement.prototype.getContext = () => ({ measureText: () => ({ width: '', @@ -448,6 +449,14 @@ suite('Mirroring events', function () { fontBoundingBoxDescent: 4, }), }); + // jsdom does not implement CSS.escape, which keyboard navigation uses to + // look up elements by ID. + global.CSS = { + escape: (id) => + id.replace(/^\d|[^\w-]/g, (char, offset) => + offset === 0 && /\d/.test(char) ? `\\3${char} ` : `\\${char}`, + ), + }; this.clock = sinon.useFakeTimers(); window.requestAnimationFrame = (callback) => setTimeout(callback, 0); window.cancelAnimationFrame = (id) => clearTimeout(id); @@ -463,6 +472,7 @@ suite('Mirroring events', function () { this.workspace.dispose(); this.clock.runAll(); sinon.restore(); + delete global.CSS; this.jsdomCleanup(); }); @@ -481,6 +491,37 @@ suite('Mirroring events', function () { 'Minimap block should show the renamed variable', ); }); + + test('Tabbing between edited fields keeps the next editor open', async function () { + const block = Blockly.serialization.blocks.append( + { + type: 'controls_for', + inputs: { + FROM: {shadow: {type: 'math_number', fields: {NUM: 1}}}, + TO: {shadow: {type: 'math_number', fields: {NUM: 10}}}, + }, + }, + this.workspace, + ); + await this.clock.runAllAsync(); + const fromField = block.getInputTargetBlock('FROM').getField('NUM'); + const toField = block.getInputTargetBlock('TO').getField('NUM'); + + fromField.showEditor(); + fromField.setValue(2); + document + .querySelector('.blocklyHtmlInput') + .dispatchEvent( + new window.KeyboardEvent('keydown', {key: 'Tab', bubbles: true}), + ); + // Let the minimap mirror the change and zoom to fit. + await this.clock.runAllAsync(); + + assert.isTrue( + toField.isBeingEdited_, + 'The editor opened by Tab should stay open after the minimap updates', + ); + }); }); suite('Keyboard navigation', function () {