-
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
455361a
commit dfddbf3
Showing
3 changed files
with
117 additions
and
39 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,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)); | ||
|
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 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(); | ||
} |