Skip to content

Commit

Permalink
Return focus to previously-focused element on popup hide
Browse files Browse the repository at this point in the history
Akin to <dialog>'s behavior, the desired behavior is that focus
is returned to the previously-focused element when a popup is
hidden:

  openui/open-ui#327

Bug: 1307772
Change-Id: Ia6ae1981612a0c0150b8b5f51b485a00a9a90de9
  • Loading branch information
mfreed7 authored and chromium-wpt-export-bot committed May 18, 2022
1 parent 455361a commit dfddbf3
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 39 deletions.
120 changes: 99 additions & 21 deletions html/semantics/popups/popup-focus.tentative.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
<title>Popup focus behaviors</title>
<link rel="author" href="mailto:[email protected]">
<link rel=help href="https://open-ui.org/components/popup.research.explainer">
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="resources/popup-utils.js"></script>

<div popup=popup data-test='default behavior - popup is not focused' data-no-focus>
<p>This is a popup</p>
Expand Down Expand Up @@ -98,47 +103,120 @@
</style>

<script>
function activateAndVerify(popup) {
const testName = popup.getAttribute('data-test');
function addInvoker(t,popup) {
const button = document.createElement('button');
button.innerText = 'Click me';
const popupId = 'popup-id';
assert_equals(document.querySelectorAll('#' + popupId).length,0);
document.body.appendChild(button);
t.add_cleanup(function() {
popup.removeAttribute('id');
button.remove();
});
popup.id = popupId;
button.setAttribute('togglepopup', popupId);
return button;
}
function addPriorFocus(t) {
const priorFocus = document.createElement('button');
priorFocus.id = 'prior-focus';
document.body.appendChild(priorFocus);
let expectedFocusedElement = popup.matches('.should-be-focused') ? popup : popup.querySelector('.should-be-focused');
if (popup.hasAttribute('data-no-focus')) {
expectedFocusedElement = priorFocus;
}
t.add_cleanup(() => priorFocus.remove());
return priorFocus;
}
function activateAndVerify(popup) {
const testName = popup.getAttribute('data-test');
test(t => {
t.add_cleanup(() => priorFocus.remove());
const priorFocus = addPriorFocus(t);
let expectedFocusedElement = popup.matches('.should-be-focused') ? popup : popup.querySelector('.should-be-focused');
if (popup.hasAttribute('data-no-focus')) {
expectedFocusedElement = priorFocus;
}
assert_true(!!expectedFocusedElement);
assert_false(popup.matches(':popup-open'));

// Directly show and hide the popup:
priorFocus.focus();
assert_equals(document.activeElement,priorFocus);

// Directly show the popup:
popup.showPopup();
assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popup.showPopup()`);
popup.hidePopup();
assert_equals(document.activeElement,priorFocus,'prior element should get focus on hide');

// Change the popup type:
priorFocus.focus();
popup.showPopup();
assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popup.showPopup()`);
assert_equals(popup.popup,'popup','All popups in this test should start as popup=popup');
popup.popup = 'hint';
assert_false(popup.matches(':popup-open'),'Changing the popup type should hide the popup');
assert_equals(document.activeElement,priorFocus,'prior element should get focus when the type is changed');
popup.popup = 'popup';

// Remove from the document:
priorFocus.focus();
popup.showPopup();
assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popup.showPopup()`);
popup.remove();
assert_false(popup.matches(':popup-open'),'Removing the popup should hide it');
if (!popup.hasAttribute('data-no-focus')) {
assert_not_equals(document.activeElement,priorFocus,'prior element should *not* get focus when the popup is removed from the document');
}
document.body.appendChild(popup);

// Show a modal dialog:
priorFocus.focus();
popup.showPopup();
assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popup.showPopup()`);
const dialog = document.body.appendChild(document.createElement('dialog'));
dialog.showModal();
assert_false(popup.matches(':popup-open'),'Opening a modal dialog should hide the popup');
assert_not_equals(document.activeElement,priorFocus,'prior element should *not* get focus when a modal dialog is shown');
dialog.close();
dialog.remove();

// Use an activating element:
const button = document.createElement('button');
const popupId = 'popup-id';
assert_equals(document.querySelectorAll('#' + popupId).length,0);
document.body.appendChild(button);
t.add_cleanup(function() {
popup.removeAttribute('id');
button.remove();
});
popup.id = popupId;
button.setAttribute('togglepopup', popupId);
const button = addInvoker(t,popup);
priorFocus.focus();
button.click();
assert_true(popup.matches(':popup-open'));
assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by button.click()`);

// Make sure we can directly focus the (already open) popup:
popup.focus();
assert_equals(document.activeElement, popup.hasAttribute('tabindex') ? popup : expectedFocusedElement, `${testName} directly focus with popup.focus()`);
popup.hidePopup();
button.click(); // Button is set to toggle the popup
assert_false(popup.matches(':popup-open'));
assert_equals(document.activeElement,priorFocus,'prior element should get focus on button-toggled hide');
}, "Popup focus test: " + testName);

promise_test(async t => {
const priorFocus = addPriorFocus(t);
assert_false(popup.matches(':popup-open'),'popup should start out hidden');
const button = addInvoker(t,popup);
assert_equals(button.getAttribute('togglepopup'),popup.id,'This test assumes the button uses `togglepopup`.');
assert_false(popup.contains(button),'Start with a non-contained button');
priorFocus.focus();
assert_equals(document.activeElement,priorFocus);
await clickOn(button);
assert_true(popup.matches(':popup-open'));
await clickOn(button);
assert_false(popup.matches(':popup-open'));
assert_equals(document.activeElement,button,'Button should get focused');

// Same thing, but the button is contained within the popup
button.removeAttribute('togglepopup');
button.setAttribute('hidepopup',popup.id);
popup.appendChild(button);
priorFocus.focus();
popup.showPopup();
assert_true(popup.matches(':popup-open'));
if (!popup.hasAttribute('data-no-focus')) {
assert_not_equals(document.activeElement,priorFocus,'focus should shift for this element');
}
await clickOn(button);
assert_false(popup.matches(':popup-open'),'clicking button should hide the popup');
assert_equals(document.activeElement,priorFocus,'Contained button should return focus to the previously focused element');
}, "Popup button click focus test: " + testName);
}

document.querySelectorAll('body > [popup]').forEach(popup => activateAndVerify(popup));
Expand Down
19 changes: 1 addition & 18 deletions html/semantics/popups/popup-light-dismiss.tentative.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="resources/popup-utils.js"></script>

<button id=b1 togglepopup='p1'>Popup 1</button>
<button id=p1anchor>Popup1 anchor (no action)</button>
Expand All @@ -30,24 +31,6 @@
}
</style>
<script>
function spinEventLoop() {
return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
}
async function clickOn(element) {
const actions = new test_driver.Actions();
await spinEventLoop();
await actions.pointerMove(0, 0, {origin: element})
.pointerDown({button: actions.ButtonType.LEFT})
.pointerUp({button: actions.ButtonType.LEFT})
.send();
await spinEventLoop();
}
async function sendTab() {
await spinEventLoop();
await new test_driver.send_keys(document.body,'\uE004'); // Tab
await spinEventLoop();
}

const popup1 = document.querySelector('#p1');
const button1 = document.querySelector('#b1');
const popup1anchor = document.querySelector('#p1anchor');
Expand Down
17 changes: 17 additions & 0 deletions html/semantics/popups/resources/popup-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
function spinEventLoop() {
return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
}
async function clickOn(element) {
const actions = new test_driver.Actions();
await spinEventLoop();
await actions.pointerMove(0, 0, {origin: element})
.pointerDown({button: actions.ButtonType.LEFT})
.pointerUp({button: actions.ButtonType.LEFT})
.send();
await spinEventLoop();
}
async function sendTab() {
await spinEventLoop();
await new test_driver.send_keys(document.body,'\uE004'); // Tab
await spinEventLoop();
}

0 comments on commit dfddbf3

Please sign in to comment.