1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
|
<!DOCTYPE html>
<meta charset="utf-8" />
<title>Popover focus behaviors</title>
<link rel="author" href="mailto:masonf@chromium.org">
<link rel=help href="https://open-ui.org/components/popover.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/popover-utils.js"></script>
<div popover data-test='default behavior - popover is not focused' data-no-focus>
<p>This is a popover</p>
<button tabindex="0">first button</button>
</div>
<div popover data-test='autofocus popover' autofocus tabindex=-1 class=should-be-focused>
<p>This is a popover</p>
</div>
<div popover data-test='autofocus empty popover' autofocus tabindex=-1 class=should-be-focused></div>
<div popover data-test='autofocus popover with button' autofocus tabindex=-1 class=should-be-focused>
<p>This is a popover</p>
<button tabindex="0">button</button>
</div>
<div popover data-test='autofocus child'>
<p>This is a popover</p>
<button autofocus class=should-be-focused tabindex="0">autofocus button</button>
</div>
<div popover data-test='autofocus on tabindex=0 element'>
<p autofocus tabindex=0 class=should-be-focused>This is a popover with autofocus on a tabindex=0 element</p>
<button tabindex="0">button</button>
</div>
<div popover data-test='autofocus multiple children'>
<p>This is a popover</p>
<button autofocus class=should-be-focused tabindex="0">autofocus button</button>
<button autofocus tabindex="0">second autofocus button</button>
</div>
<div popover autofocus tabindex=-1 data-test='autofocus popover and multiple autofocus children' class=should-be-focused>
<p>This is a popover</p>
<button autofocus tabindex="0">autofocus button</button>
<button autofocus tabindex="0">second autofocus button</button>
</div>
<dialog popover=auto data-test='Opening dialogs as popovers should use dialog initial focus algorithm.'>
<button class=should-be-focused tabindex="0">button</button>
</dialog>
<dialog popover=auto autofocus class=should-be-focused data-test='Opening dialogs as popovers which have autofocus should focus the dialog.'>
<button tabindex="0">button</button>
</dialog>
<style>
[popover] {
border: 2px solid black;
top:150px;
left:150px;
}
:focus-within { border: 5px dashed red; }
:focus { border: 5px solid lime; }
</style>
<script>
function addInvoker(t, popover) {
const button = document.createElement('button');
button.innerText = 'Click me';
const popoverId = 'popover-id';
assert_equals(document.querySelectorAll('#' + popoverId).length, 0);
document.body.appendChild(button);
t.add_cleanup(function() {
popover.removeAttribute('id');
button.remove();
});
popover.id = popoverId;
button.setAttribute('tabindex', '0');
button.setAttribute('popovertarget', popoverId);
return button;
}
function addPriorFocus(t) {
const priorFocus = document.createElement('button');
priorFocus.setAttribute("tabindex", "0");
priorFocus.id = 'priorFocus';
document.body.appendChild(priorFocus);
t.add_cleanup(() => priorFocus.remove());
return priorFocus;
}
function activateAndVerify(popover) {
const testName = popover.getAttribute('data-test');
promise_test(async t => {
const priorFocus = addPriorFocus(t);
let expectedFocusedElement = popover.matches('.should-be-focused') ? popover : popover.querySelector('.should-be-focused');
const changesFocus = !popover.hasAttribute('data-no-focus');
if (!changesFocus) {
expectedFocusedElement = priorFocus;
}
assert_true(!!expectedFocusedElement);
assert_false(popover.matches(':popover-open'));
// Directly show and hide the popover:
priorFocus.focus();
assert_equals(document.activeElement, priorFocus);
popover.showPopover();
assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
popover.hidePopover();
assert_equals(document.activeElement, priorFocus, 'prior element should get focus on hide, or if focus didn\'t shift on show, focus should stay where it was');
assert_false(isElementVisible(popover));
// Manual popover does not restore focus
popover.popover = 'manual';
priorFocus.focus();
assert_equals(document.activeElement, priorFocus);
popover.showPopover();
assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
popover.hidePopover();
if (!popover.hasAttribute('data-no-focus')) {
assert_not_equals(document.activeElement, priorFocus, 'prior element should *not* get focus when the popover is manual');
}
assert_false(isElementVisible(popover));
popover.popover = 'auto';
// Hit Escape:
priorFocus.focus();
assert_equals(document.activeElement, priorFocus);
popover.showPopover();
assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
await sendEscape();
assert_equals(document.activeElement, priorFocus, 'prior element should get focus after Escape');
assert_false(isElementVisible(popover));
// Move focus into the popover, then hit Escape:
let containedButton = popover.querySelector('button');
if (containedButton) {
priorFocus.focus();
assert_equals(document.activeElement, priorFocus);
popover.showPopover();
containedButton.focus();
assert_equals(document.activeElement, containedButton);
await sendEscape();
assert_equals(document.activeElement, priorFocus, 'prior element should get focus after Escape');
assert_false(isElementVisible(popover));
}
// Change the popover type:
priorFocus.focus();
popover.showPopover();
assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
assert_equals(popover.popover, 'auto', 'All popovers in this test should start as popover=auto');
popover.popover = 'manual';
assert_false(popover.matches(':popover-open'), 'Changing the popover type should hide the popover');
assert_equals(document.activeElement, priorFocus, 'prior element should get focus when the type is changed');
assert_false(isElementVisible(popover));
popover.popover = 'auto';
// Remove from the document:
priorFocus.focus();
popover.showPopover();
assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
popover.remove();
assert_false(isElementVisible(popover), 'Removing the popover should hide it immediately');
if (!popover.hasAttribute('data-no-focus')) {
assert_not_equals(document.activeElement, priorFocus, 'prior element should *not* get focus when the popover is removed from the document');
}
document.body.appendChild(popover);
// Show a modal dialog:
priorFocus.focus();
popover.showPopover();
assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
const dialog = document.body.appendChild(document.createElement('dialog'));
dialog.showModal();
assert_false(popover.matches(':popover-open'), 'Opening a modal dialog should hide the popover');
assert_not_equals(document.activeElement, priorFocus, 'prior element should *not* get focus when a modal dialog is shown');
assert_false(isElementVisible(popover));
dialog.close();
dialog.remove();
// Use an activating element:
const button = addInvoker(t, popover);
priorFocus.focus();
button.click();
assert_true(popover.matches(':popover-open'));
assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by button.click()`);
// Make sure Escape works in the invoker case:
await sendEscape();
assert_equals(document.activeElement, priorFocus, 'prior element should get focus after Escape (via invoker)');
assert_false(isElementVisible(popover));
// Make sure we can directly focus the (already open) popover:
priorFocus.focus();
button.click();
assert_true(popover.matches(':popover-open'));
assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by button.click()`);
popover.focus();
assert_equals(document.activeElement, popover.hasAttribute('tabindex') || popover.tagName === 'DIALOG' ? popover : expectedFocusedElement, `${testName} directly focus with popover.focus()`);
button.click(); // Button is set to toggle the popover
assert_false(popover.matches(':popover-open'));
assert_equals(document.activeElement, priorFocus, 'prior element should get focus on button-toggled hide');
assert_false(isElementVisible(popover));
}, "Popover focus test: " + testName);
promise_test(async t => {
const priorFocus = addPriorFocus(t);
assert_false(popover.matches(':popover-open'), 'popover should start out hidden');
let button = addInvoker(t, popover);
assert_equals(button.getAttribute('popovertarget'), popover.id, 'This test assumes the button uses `popovertarget`.');
assert_not_equals(button, priorFocus, 'Stranger things have happened');
assert_false(popover.contains(button), 'Start with a non-contained button');
priorFocus.focus();
assert_equals(document.activeElement, priorFocus);
popover.showPopover();
assert_true(popover.matches(':popover-open'));
await clickOn(button); // This will *not* light dismiss, but will "toggle" the popover.
assert_false(popover.matches(':popover-open'));
assert_equals(document.activeElement, button, 'focus should move to the button when clicked, and should stay there when the popover closes');
assert_false(isElementVisible(popover));
// Same thing, but the button is contained within the popover
button.setAttribute('popovertarget', popover.id);
button.setAttribute('popovertargetaction', 'hide');
popover.appendChild(button);
t.add_cleanup(() => button.remove());
priorFocus.focus();
popover.showPopover();
assert_true(popover.matches(':popover-open'));
const changesFocus = !popover.hasAttribute('data-no-focus');
if (changesFocus) {
assert_not_equals(document.activeElement, priorFocus, 'focus should shift for this element');
}
await clickOn(button);
assert_false(popover.matches(':popover-open'), 'clicking button should hide the popover');
assert_equals(document.activeElement, priorFocus, 'Contained button should return focus to the previously focused element');
assert_false(isElementVisible(popover));
// Same thing, but the button is unrelated (no popovertarget)
button = document.createElement('button');
button.setAttribute("tabindex", "0");
document.body.appendChild(button);
priorFocus.focus();
popover.showPopover();
assert_true(popover.matches(':popover-open'));
await clickOn(button); // This will light dismiss the popover, focus the prior focus, then focus this button.
assert_false(popover.matches(':popover-open'), 'clicking button should hide the popover (via light dismiss)');
assert_equals(document.activeElement, button, 'Focus should go to unrelated button on light dismiss');
assert_false(isElementVisible(popover));
}, "Popover button click focus test: " + testName);
promise_test(async t => {
if (popover.hasAttribute('data-no-focus')) {
// This test only applies if the popover changes focus
return;
}
const priorFocus = addPriorFocus(t);
assert_false(popover.matches(':popover-open'), 'popover should start out hidden');
// Move the prior focus out of the document
priorFocus.focus();
popover.showPopover();
assert_true(popover.matches(':popover-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');
popover.hidePopover();
assert_not_equals(document.activeElement, priorFocus, 'focused element has been removed');
assert_false(isElementVisible(popover));
document.body.appendChild(priorFocus); // Put it back
// Move the prior focus inside the (already open) popover
priorFocus.focus();
popover.showPopover();
assert_true(popover.matches(':popover-open'));
assert_false(popover.contains(priorFocus), 'Start with a non-contained prior focus');
popover.appendChild(priorFocus); // Move inside the popover
assert_true(popover.contains(priorFocus));
assert_true(popover.matches(':popover-open'), 'popover should stay open');
popover.hidePopover();
assert_false(isElementVisible(popover));
assert_not_equals(document.activeElement, priorFocus, 'focused element is display:none inside the popover');
document.body.appendChild(priorFocus); // Put it back
}, "Popover corner cases test: " + testName);
}
document.querySelectorAll('body > [popover]').forEach(popover => activateAndVerify(popover));
</script>
|