-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Return focus to previously-focused element on popup hide
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
1 parent
9370bce
commit b4a448b
Showing
3 changed files
with
155 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
|
@@ -98,47 +103,153 @@ | |
</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')); | ||
priorFocus.focus(); | ||
assert_equals(document.activeElement,priorFocus); | ||
|
||
// Directly show the popup: | ||
// Directly show and hide the popup: | ||
priorFocus.focus(); | ||
assert_equals(document.activeElement, priorFocus); | ||
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); | ||
|
||
promise_test(async t => { | ||
if (popup.hasAttribute('data-no-focus')) { | ||
// This test only applies if the popup changes focus | ||
return; | ||
} | ||
const priorFocus = addPriorFocus(t); | ||
assert_false(popup.matches(':popup-open'), 'popup should start out hidden'); | ||
|
||
// Move the prior focus out of the document | ||
priorFocus.focus(); | ||
popup.showPopup(); | ||
assert_true(popup.matches(':popup-open')); | ||
const newFocus = document.activeElement; | ||
assert_not_equals(newFocus, priorFocus, 'focus should shift for this element'); | ||
priorFocus.remove(); | ||
assert_equals(document.activeElement, newFocus, 'focus should not change when prior focus is removed'); | ||
popup.hidePopup(); | ||
assert_not_equals(document.activeElement, priorFocus, 'focused element has been removed'); | ||
document.body.appendChild(priorFocus); // Put it back | ||
|
||
// Move the prior focus inside the (already open) popup | ||
priorFocus.focus(); | ||
popup.showPopup(); | ||
assert_true(popup.matches(':popup-open')); | ||
assert_false(popup.contains(priorFocus), 'Start with a non-contained prior focus'); | ||
popup.appendChild(priorFocus); // Move inside the popup | ||
assert_true(popup.contains(priorFocus)); | ||
assert_true(popup.matches(':popup-open'), 'popup should stay open'); | ||
popup.hidePopup(); | ||
assert_not_equals(document.activeElement, priorFocus, 'focused element is display:none inside the popup'); | ||
document.body.appendChild(priorFocus); // Put it back | ||
}, "Popup corner cases test: " + testName); | ||
} | ||
|
||
document.querySelectorAll('body > [popup]').forEach(popup => activateAndVerify(popup)); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
function waitForRender() { | ||
return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); | ||
} | ||
async function clickOn(element) { | ||
const actions = new test_driver.Actions(); | ||
await waitForRender(); | ||
await actions.pointerMove(0, 0, {origin: element}) | ||
.pointerDown({button: actions.ButtonType.LEFT}) | ||
.pointerUp({button: actions.ButtonType.LEFT}) | ||
.send(); | ||
await waitForRender(); | ||
} | ||
async function sendTab() { | ||
await waitForRender(); | ||
await new test_driver.send_keys(document.body,'\uE004'); // Tab | ||
await waitForRender(); | ||
} |