diff --git a/packages/blockly/core/dropdowndiv.ts b/packages/blockly/core/dropdowndiv.ts index 95b6c66e141..f35ee2fc8bf 100644 --- a/packages/blockly/core/dropdowndiv.ts +++ b/packages/blockly/core/dropdowndiv.ts @@ -17,7 +17,9 @@ import * as browserEvents from './browser_events.js'; import * as common from './common.js'; import type {Field} from './field.js'; import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js'; +import * as aria from './utils/aria.js'; import * as dom from './utils/dom.js'; +import * as idGenerator from './utils/idgenerator.js'; import * as math from './utils/math.js'; import {Rect} from './utils/rect.js'; import type {Size} from './utils/size.js'; @@ -127,6 +129,7 @@ export function createDom() { div = document.createElement('div'); div.className = 'blocklyDropDownDiv'; div.tabIndex = -1; + div.id = idGenerator.getNextUniqueId(); const parentDiv = common.getParentContainer() || document.body; parentDiv.appendChild(div); @@ -399,6 +402,16 @@ export function show( dom.addClass(div, renderedClassName); dom.addClass(div, themeClassName); + const existingOwnership = aria.getState( + mainWorkspace.getFocusableElement(), + aria.State.OWNS, + ); + aria.setState( + mainWorkspace.getFocusableElement(), + aria.State.OWNS, + existingOwnership ? [existingOwnership, div.id] : div.id, + ); + // When we change `translate` multiple times in close succession, // Chrome may choose to wait and apply them all at once. // Since we want the translation to initial X, Y to be immediate, @@ -714,7 +727,16 @@ export function hideWithoutAnimation() { } owner = null; - (common.getMainWorkspace() as WorkspaceSvg).markFocused(); + const workspace = common.getMainWorkspace() as WorkspaceSvg; + const existingOwnership = + aria.getState(workspace.getFocusableElement(), aria.State.OWNS) ?? ''; + aria.setState( + workspace.getFocusableElement(), + aria.State.OWNS, + existingOwnership.replace(div.id, ''), + ); + + workspace.markFocused(); if (returnEphemeralFocus) { returnEphemeralFocus(); diff --git a/packages/blockly/core/widgetdiv.ts b/packages/blockly/core/widgetdiv.ts index d9d49a29dfc..6d8ad58d655 100644 --- a/packages/blockly/core/widgetdiv.ts +++ b/packages/blockly/core/widgetdiv.ts @@ -10,7 +10,9 @@ import * as browserEvents from './browser_events.js'; import * as common from './common.js'; import {Field} from './field.js'; import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js'; +import * as aria from './utils/aria.js'; import * as dom from './utils/dom.js'; +import * as idGenerator from './utils/idgenerator.js'; import type {Rect} from './utils/rect.js'; import type {Size} from './utils/size.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -85,6 +87,7 @@ export function createDom() { containerDiv = existingContainer as HTMLDivElement; } else { containerDiv = document.createElement('div'); + containerDiv.id = idGenerator.getNextUniqueId(); containerDiv.className = containerClassName; containerDiv.tabIndex = -1; } @@ -126,6 +129,17 @@ export function show( const div = containerDiv; if (!div) return; + ownerWorkspace = workspace ?? (common.getMainWorkspace() as WorkspaceSvg); + const existingOwnership = aria.getState( + ownerWorkspace.getFocusableElement(), + aria.State.OWNS, + ); + aria.setState( + ownerWorkspace.getFocusableElement(), + aria.State.OWNS, + existingOwnership ? [existingOwnership, div.id] : div.id, + ); + const parentDiv = common.getParentContainer(); parentDiv?.appendChild(div); @@ -136,11 +150,8 @@ export function show( // workspace to this function, attempt to derive it from the field. workspace = (newOwner as Field).getSourceBlock()?.workspace as WorkspaceSvg; } - ownerWorkspace = workspace ?? null; - const rendererWorkspace = - workspace ?? (common.getMainWorkspace() as WorkspaceSvg); - rendererClassName = rendererWorkspace.getRenderer().getClassName(); - themeClassName = rendererWorkspace.getTheme().getClassName(); + rendererClassName = ownerWorkspace.getRenderer().getClassName(); + themeClassName = ownerWorkspace.getTheme().getClassName(); if (rendererClassName) { dom.addClass(div, rendererClassName); } @@ -182,12 +193,22 @@ export function hide() { dom.removeClass(div, themeClassName); themeClassName = ''; } - (common.getMainWorkspace() as WorkspaceSvg).markFocused(); + ownerWorkspace?.markFocused(); if (returnEphemeralFocus) { returnEphemeralFocus(); returnEphemeralFocus = null; } + + if (!ownerWorkspace || !containerDiv) return; + + const existingOwnership = + aria.getState(ownerWorkspace.getFocusableElement(), aria.State.OWNS) ?? ''; + aria.setState( + ownerWorkspace.getFocusableElement(), + aria.State.OWNS, + existingOwnership.replace(containerDiv.id, ''), + ); } /** diff --git a/packages/blockly/tests/mocha/dropdowndiv_test.js b/packages/blockly/tests/mocha/dropdowndiv_test.js index ab9026f4ad0..5b9a9e41d0f 100644 --- a/packages/blockly/tests/mocha/dropdowndiv_test.js +++ b/packages/blockly/tests/mocha/dropdowndiv_test.js @@ -194,6 +194,20 @@ suite('DropDownDiv', function () { assert.strictEqual(dropDownDivElem.style.left, '45px'); assert.strictEqual(dropDownDivElem.style.top, '60px'); }); + + test('sets the dropdowndiv as owned by the workspace', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + + Blockly.DropDownDiv.show(field, false, 50, 60, 70, 80, false); + assert.equal( + Blockly.utils.aria.getState( + Blockly.getMainWorkspace().getFocusableElement(), + Blockly.utils.aria.State.OWNS, + ), + Blockly.DropDownDiv.getContentDiv().parentElement.id, + ); + }); }); suite('showPositionedByField()', function () { @@ -388,6 +402,21 @@ suite('DropDownDiv', function () { assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); assert.strictEqual(document.activeElement, blockFocusableElem); }); + + test('clears ownership of the dropdowndiv by the workspace', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + Blockly.DropDownDiv.showPositionedByField(field, null, null, true); + Blockly.DropDownDiv.hideWithoutAnimation(); + + assert.isNull( + Blockly.utils.aria.getState( + Blockly.getMainWorkspace().getFocusableElement(), + Blockly.utils.aria.State.OWNS, + ), + ); + }); }); suite('for div positioned by block', function () { @@ -454,6 +483,28 @@ suite('DropDownDiv', function () { assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); assert.strictEqual(document.activeElement, blockFocusableElem); }); + + test('clears ownership of the dropdowndiv by the workspace', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + Blockly.DropDownDiv.showPositionedByBlock( + field, + block, + null, + null, + true, + ); + + Blockly.DropDownDiv.hideWithoutAnimation(); + + assert.isNull( + Blockly.utils.aria.getState( + Blockly.getMainWorkspace().getFocusableElement(), + Blockly.utils.aria.State.OWNS, + ), + ); + }); }); }); }); diff --git a/packages/blockly/tests/mocha/field_textinput_test.js b/packages/blockly/tests/mocha/field_textinput_test.js index 5a1191435a4..14cc531d6a7 100644 --- a/packages/blockly/tests/mocha/field_textinput_test.js +++ b/packages/blockly/tests/mocha/field_textinput_test.js @@ -211,6 +211,9 @@ suite('Text Input Fields', function () { }, markFocused: function () {}, options: {}, + getFocusableElement: function () { + return document.createElement('div'); + }, }; field.sourceBlock_ = { workspace: workspace, diff --git a/packages/blockly/tests/mocha/widget_div_test.js b/packages/blockly/tests/mocha/widget_div_test.js index d55fa215f0a..56c6f9e91b0 100644 --- a/packages/blockly/tests/mocha/widget_div_test.js +++ b/packages/blockly/tests/mocha/widget_div_test.js @@ -362,6 +362,21 @@ suite('WidgetDiv', function () { assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); assert.strictEqual(document.activeElement, widgetDivElem); }); + + test('makes the widget div owned by the workspace', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + + Blockly.WidgetDiv.show(field, false, () => {}, null, true); + assert.equal( + Blockly.utils.aria.getState( + Blockly.getMainWorkspace().getFocusableElement(), + Blockly.utils.aria.State.OWNS, + ), + Blockly.WidgetDiv.getDiv().id, + ); + }); }); suite('hide()', function () { @@ -424,5 +439,21 @@ suite('WidgetDiv', function () { assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); assert.strictEqual(document.activeElement, blockFocusableElem); }); + + test('clears ownership of the widget div by the workspace', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + Blockly.WidgetDiv.show(field, false, () => {}, null, true); + + Blockly.WidgetDiv.hide(); + + assert.isNull( + Blockly.utils.aria.getState( + Blockly.getMainWorkspace().getFocusableElement(), + Blockly.utils.aria.State.OWNS, + ), + ); + }); }); });