blob: 5a7f5da8f7b52d47c7e09e984cb835465591a67d [file] [log] [blame]
<!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">
<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 id=fixup>
<button id=button1>Button1</button>
<div popover id=popover1 style="top:100px">
<button id=inside_popover1>Inside1</button>
<button id=invoker2 popovertarget=popover2>Nested Invoker 2</button>
<button id=inside_popover2>Inside2</button>
</div>
<button id=button2>Button2</button>
<button popovertarget=popover1 id=invoker1>Invoker1</button>
<button id=button3>Button3</button>
<div popover id=popover2 style="top:200px">
<button id=inside_popover3>Inside3</button>
<button id=invoker3 popovertarget=popover3>Nested Invoker 3</button>
</div>
<div popover id=popover3 style="top:300px">
Non-focusable popover
</div>
<button id=button4>Button4</button>
</div>
<style>
#fixup [popover] {
bottom:auto;
}
</style>
<script>
async function verifyFocusOrder(order) {
order[0].focus();
for(let i=0;i<order.length;++i) {
const control = order[i];
assert_equals(document.activeElement,control,`Step ${i+1}`);
await sendTab();
}
// Shift-tab not supported, crbug.com/893480.
// for(let i=order.length-1;i>=0;--i) {
// const control = order[i];
// await sendShiftTab();
// assert_equals(document.activeElement,control,`Step ${i+1} (backwards)`);
// }
}
promise_test(async t => {
button1.focus();
assert_equals(document.activeElement,button1);
await sendTab();
assert_equals(document.activeElement,button2,'Hidden popover should be skipped');
// Shift-tab not supported, crbug.com/893480.
// await sendShiftTab();
// assert_equals(document.activeElement,button1,'Hidden popover should be skipped backwards');
//await sendTab();
await sendTab();
assert_equals(document.activeElement,invoker1);
await sendEnter(); // Activate the invoker
assert_true(popover1.matches(':popover-open'), 'popover1 should be invoked by invoker1');
assert_equals(document.activeElement,invoker1,'Focus should not move when popover is shown');
await sendTab();
assert_equals(document.activeElement,inside_popover1,'Focus should move from invoker into the open popover');
await sendTab();
assert_equals(document.activeElement,invoker2,'Focus should move within popover');
await verifyFocusOrder([button1, button2, invoker1, inside_popover1, invoker2, inside_popover2, button3, button4]);
invoker2.focus();
await sendEnter(); // Activate the nested invoker
assert_true(popover2.matches(':popover-open'), 'popover2 should be invoked by nested invoker');
assert_equals(document.activeElement,invoker2,'Focus should stay on the invoker');
await sendTab();
assert_equals(document.activeElement,inside_popover3,'Focus should move into nested popover');
await sendTab();
assert_equals(document.activeElement,invoker3);
await sendEnter(); // Activate the (empty) nested invoker
assert_true(popover3.matches(':popover-open'), 'popover3 should be invoked by nested invoker');
assert_equals(document.activeElement,invoker3,'Focus should stay on the invoker');
await sendTab();
assert_equals(document.activeElement,inside_popover2,'Focus should skip popover without focusable content, going back to higher scope');
await sendTab();
assert_equals(document.activeElement,button3,'Focus should exit popovers');
await sendTab();
assert_equals(document.activeElement,button4,'Focus should skip popovers');
button1.focus();
await verifyFocusOrder([button1, button2, invoker1, inside_popover1, invoker2, inside_popover3, invoker3, inside_popover2, button3, button4]);
}, "Popover focus navigation");
</script>
<button id=circular0 popovertarget=popover4>Invoker</button>
<div id=popover4 popover>
<button id=circular1 autofocus popovertarget=popover4 popovertargetaction=hide></button>
<button id=circular2 popovertarget=popover4 popovertargetaction=show></button>
<button id=circular3 popovertarget=popover4></button>
</div>
<button id=circular4>after</button>
<script>
promise_test(async t => {
circular0.focus();
await sendEnter(); // Activate the invoker
await verifyFocusOrder([circular0, circular1, circular2, circular3, circular4]);
popover4.hidePopover();
}, "Circular reference tab navigation");
</script>
<div id=focus-return1>
<button popovertarget=focus-return1-p popovertargetaction=show>Show popover</button>
<div popover id=focus-return1-p>
<button popovertarget=focus-return1-p popovertargetaction=hide autofocus>Hide popover</button>
</div>
</div>
<script>
promise_test(async t => {
const invoker = document.querySelector('#focus-return1>button');
const popover = document.querySelector('#focus-return1>[popover]');
const hideButton = popover.querySelector('[popovertargetaction=hide]');
invoker.focus(); // Make sure button is focused.
assert_equals(document.activeElement,invoker);
await sendEnter(); // Activate the invoker
assert_true(popover.matches(':popover-open'), 'popover should be invoked by invoker');
assert_equals(document.activeElement,hideButton,'Hide button should be focused due to autofocus attribute');
await sendEnter(); // Activate the hide invoker
assert_false(popover.matches(':popover-open'), 'popover should be hidden by invoker');
assert_equals(document.activeElement,invoker,'Focus should be returned to the invoker');
}, "Popover focus returns when popover is hidden by invoker");
</script>
<div id=focus-return2>
<button popovertarget=focus-return2-p>Toggle popover</button>
<div popover id=focus-return2-p>Popover with <button>focusable element</button></div>
<span tabindex=0>Other focusable element</span>
</div>
<script>
promise_test(async t => {
const invoker = document.querySelector('#focus-return2>button');
const popover = document.querySelector('#focus-return2>[popover]');
const otherElement = document.querySelector('#focus-return2>span');
invoker.focus(); // Make sure button is focused.
assert_equals(document.activeElement,invoker);
invoker.click(); // Activate the invoker
assert_true(popover.matches(':popover-open'), 'popover should be invoked by invoker');
assert_equals(document.activeElement,invoker,'invoker should still be focused');
await sendTab();
assert_equals(document.activeElement,popover.querySelector('button'),'next up is the popover');
await sendTab();
assert_equals(document.activeElement,otherElement,'next focus stop is outside the popover');
await sendEscape(); // Close the popover via ESC
assert_false(popover.matches(':popover-open'), 'popover should be hidden');
assert_equals(document.activeElement,otherElement,'focus does not move because it was not inside the popover');
}, "Popover focus only returns to invoker when focus is within the popover");
</script>