Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions core/dropdowndiv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,8 @@ function showPositionedByRect(
secondaryX,
secondaryY,
manageEphemeralFocus,
autoCloseOnLostFocus,
opt_onHide,
autoCloseOnLostFocus,
);
}

Expand All @@ -366,9 +366,9 @@ function showPositionedByRect(
* @param primaryY Desired origin point y, in absolute px.
* @param secondaryX Secondary/alternative origin point x, in absolute px.
* @param secondaryY Secondary/alternative origin point y, in absolute px.
* @param opt_onHide Optional callback for when the drop-down is hidden.
* @param manageEphemeralFocus Whether ephemeral focus should be managed
* according to the widget div's lifetime.
* @param opt_onHide Optional callback for when the drop-down is hidden.
* @param autoCloseOnLostFocus Whether the drop-down should automatically hide
* if it loses DOM focus for any reason.
* @returns True if the menu rendered at the primary origin point.
Expand All @@ -382,8 +382,8 @@ export function show<T>(
secondaryX: number,
secondaryY: number,
manageEphemeralFocus: boolean,
autoCloseOnLostFocus: boolean,
opt_onHide?: () => void,
autoCloseOnLostFocus?: boolean,
): boolean {
owner = newOwner as Field;
onHide = opt_onHide || null;
Expand Down
2 changes: 1 addition & 1 deletion core/focus_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,10 @@ export class FocusManager {
element instanceof Node &&
ephemeralFocusElem.contains(element);
if (hadFocus !== hasFocus) {
this.ephemerallyFocusedElementCurrentlyHasFocus = hasFocus;
if (this.ephemeralDomFocusChangedCallback) {
this.ephemeralDomFocusChangedCallback(hasFocus);
}
this.ephemerallyFocusedElementCurrentlyHasFocus = hasFocus;
}
}
};
Expand Down
16 changes: 15 additions & 1 deletion core/widgetdiv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,16 @@ export function createDom() {
* passed in here then callers should manage ephemeral focus directly
* otherwise focus may not properly restore when the widget closes. Defaults
* to true.
* @param autoCloseOnLostFocus Whether the widget should automatically hide if
* it loses DOM focus for any reason.
*/
export function show(
newOwner: unknown,
rtl: boolean,
newDispose: () => void,
workspace?: WorkspaceSvg | null,
manageEphemeralFocus: boolean = true,
autoCloseOnLostFocus: boolean = true,
) {
hide();
owner = newOwner;
Expand All @@ -131,7 +134,18 @@ export function show(
dom.addClass(div, themeClassName);
}
if (manageEphemeralFocus) {
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);
const autoCloseCallback = autoCloseOnLostFocus
? (hasFocus: boolean) => {
// If focus is ever lost, close the widget.
if (!hasFocus) {
hide();
}
}
: null;
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(
div,
autoCloseCallback,
);
}
}

Expand Down
4 changes: 2 additions & 2 deletions tests/mocha/dropdowndiv_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ suite('DropDownDiv', function () {
});
test('Escape dismisses DropDownDiv', function () {
let hidden = false;
Blockly.DropDownDiv.show(this, false, 0, 0, 0, 0, false, false, () => {
Blockly.DropDownDiv.show(this, false, 0, 0, 0, 0, false, () => {
hidden = true;
});
assert.isFalse(hidden);
Expand Down Expand Up @@ -276,7 +276,7 @@ suite('DropDownDiv', function () {
// Focus an element outside of the drop-down.
document.getElementById('nonTreeElementForEphemeralFocus').focus();

// the drop-down should now be hidden since it lost focus.
// The drop-down should now be hidden since it lost focus.
const dropDownDivElem = document.querySelector('.blocklyDropDownDiv');
assert.strictEqual(dropDownDivElem.style.opacity, '0');
});
Expand Down
39 changes: 14 additions & 25 deletions tests/mocha/focus_manager_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5624,21 +5624,6 @@ suite('FocusManager', function () {
/* Ephemeral focus tests. */

suite('takeEphemeralFocus()', function () {
setup(function () {
// Ensure ephemeral-specific elements are focusable.
document.getElementById('nonTreeElementForEphemeralFocus').tabIndex = -1;
document.getElementById('nonTreeGroupForEphemeralFocus').tabIndex = -1;
});
teardown(function () {
// Ensure ephemeral-specific elements have their tab indexes reset for a clean state.
document
.getElementById('nonTreeElementForEphemeralFocus')
.removeAttribute('tabindex');
document
.getElementById('nonTreeGroupForEphemeralFocus')
.removeAttribute('tabindex');
});

test('with no focused node does not change states', function () {
this.focusManager.registerTree(this.testFocusableTree2);
this.focusManager.registerTree(this.testFocusableGroup2);
Expand Down Expand Up @@ -6070,7 +6055,7 @@ suite('FocusManager', function () {
assert.isTrue(callback.thirdCall.calledWithExactly(true));
});

test('with focus change callback set focus to non-ephemeral element with auto return finishes ephemeral', function () {
test('with focus change callback set focus to non-ephemeral element with auto return finishes ephemeral does not restore to focused node', function () {
this.focusManager.registerTree(this.testFocusableTree2);
this.focusManager.registerTree(this.testFocusableGroup2);
this.focusManager.focusNode(this.testFocusableTree2Node1);
Expand All @@ -6090,21 +6075,24 @@ suite('FocusManager', function () {
// Force focus away, triggering the callback's automatic returning logic.
ephemeralElement2.focus();

// The original focused node should be restored.
const nodeElem = this.testFocusableTree2Node1.getFocusableElement();
// The original node should not be focused since the ephemeral element
// lost its own DOM focus while ephemeral focus was active. Instead, the
// newly active element should still hold focus.
const activeElems = Array.from(
document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR),
);
assert.strictEqual(
this.focusManager.getFocusedNode(),
this.testFocusableTree2Node1,
const passiveElems = Array.from(
document.querySelectorAll(PASSIVE_FOCUS_NODE_CSS_SELECTOR),
);
assert.strictEqual(activeElems.length, 1);
assert.isEmpty(activeElems);
assert.strictEqual(passiveElems.length, 1);
assert.includesClass(
nodeElem.classList,
FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME,
this.testFocusableTree2Node1.getFocusableElement().classList,
FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME,
);
assert.strictEqual(document.activeElement, nodeElem);
assert.isNull(this.focusManager.getFocusedNode());
assert.strictEqual(document.activeElement, ephemeralElement2);
assert.isFalse(this.focusManager.ephemeralFocusTaken());
});

test('with focus on non-ephemeral element ephemeral ended does not restore to focused node', function () {
Expand Down Expand Up @@ -6139,6 +6127,7 @@ suite('FocusManager', function () {
this.testFocusableTree2Node1.getFocusableElement().classList,
FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME,
);
assert.isNull(this.focusManager.getFocusedNode());
assert.strictEqual(document.activeElement, ephemeralElement2);
assert.isFalse(this.focusManager.ephemeralFocusTaken());
});
Expand Down
2 changes: 1 addition & 1 deletion tests/mocha/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@
</text>
</g>
</g>
<g id="nonTreeGroupForEphemeralFocus"></g>
<g id="nonTreeGroupForEphemeralFocus" tabindex="-1"></g>
</svg>
<!-- Load mocha et al. before Blockly and the test modules so that
we can safely import the test modules that make calls
Expand Down
66 changes: 66 additions & 0 deletions tests/mocha/widget_div_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,5 +444,71 @@ suite('WidgetDiv', function () {
assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block);
assert.strictEqual(document.activeElement, blockFocusableElem);
});

test('without auto close on lost focus lost focus does not hide widget div', function () {
const block = this.setUpBlockWithField();
const field = Array.from(block.getFields())[0];
Blockly.getFocusManager().focusNode(block);
Blockly.WidgetDiv.show(field, false, () => {}, null, true, false);

// Focus an element outside of the widget.
document.getElementById('nonTreeElementForEphemeralFocus').focus();

// Even though the widget lost focus, it should still be visible.
const widgetDivElem = document.querySelector('.blocklyWidgetDiv');
assert.strictEqual(widgetDivElem.style.display, 'block');
});

test('with auto close on lost focus lost focus hides widget div', function () {
const block = this.setUpBlockWithField();
const field = Array.from(block.getFields())[0];
Blockly.getFocusManager().focusNode(block);
Blockly.WidgetDiv.show(field, false, () => {}, null, true, true);

// Focus an element outside of the widget.
document.getElementById('nonTreeElementForEphemeralFocus').focus();

// The widget should now be hidden since it lost focus.
const widgetDivElem = document.querySelector('.blocklyWidgetDiv');
assert.strictEqual(widgetDivElem.style.display, 'none');
});

test('with auto close on lost focus lost focus with nested div hides widget div', function () {
const block = this.setUpBlockWithField();
const field = Array.from(block.getFields())[0];
Blockly.getFocusManager().focusNode(block);
const nestedDiv = document.createElement('div');
nestedDiv.tabIndex = -1;
Blockly.WidgetDiv.getDiv().appendChild(nestedDiv);
Blockly.WidgetDiv.show(field, false, () => {}, null, true, true);
nestedDiv.focus(); // It's valid to focus this during ephemeral focus.

// Focus an element outside of the widget.
document.getElementById('nonTreeElementForEphemeralFocus').focus();

// The widget should now be hidden since it lost focus.
const widgetDivElem = document.querySelector('.blocklyWidgetDiv');
assert.strictEqual(widgetDivElem.style.display, 'none');
});

test('with auto close on lost focus lost focus with nested div does not restore DOM focus', function () {
const block = this.setUpBlockWithField();
const field = Array.from(block.getFields())[0];
Blockly.getFocusManager().focusNode(block);
const nestedDiv = document.createElement('div');
nestedDiv.tabIndex = -1;
Blockly.WidgetDiv.getDiv().appendChild(nestedDiv);
Blockly.WidgetDiv.show(field, false, () => {}, null, true, true);
nestedDiv.focus(); // It's valid to focus this during ephemeral focus.

// Focus an element outside of the widget.
const elem = document.getElementById('nonTreeElementForEphemeralFocus');
elem.focus();

// Auto hiding should not restore focus back to the block since ephemeral
// focus was lost before it was returned.
assert.isNull(Blockly.getFocusManager().getFocusedNode());
assert.strictEqual(document.activeElement, elem);
});
});
});