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
|
<!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 tabindex="0">Button1</button>
<div popover id=popover1 style="top:100px">
<button id=inside_popover1 tabindex="0">Inside1</button>
<button id=invoker2 popovertarget=popover2 tabindex="0">Nested Invoker 2</button>
<button id=inside_popover2 tabindex="0">Inside2</button>
</div>
<button id=button2 tabindex="0">Button2</button>
<button popovertarget=popover1 id=invoker1 tabindex="0">Invoker1</button>
<button id=button3 tabindex="0">Button3</button>
<div popover id=popover2 style="top:200px">
<button id=inside_popover3 tabindex="0">Inside3</button>
<button id=invoker3 popovertarget=popover3 tabindex="0">Nested Invoker 3</button>
</div>
<div popover id=popover3 style="top:300px">
Non-focusable popover
</div>
<button id=button4 tabindex="0">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 tabindex="0">Invoker</button>
<div id=popover4 popover>
<button id=circular1 autofocus popovertarget=popover4 popovertargetaction=hide tabindex="0"></button>
<button id=circular2 popovertarget=popover4 popovertargetaction=show tabindex="0"></button>
<button id=circular3 popovertarget=popover4 tabindex="0"></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 tabindex="0">Show popover</button>
<div popover id=focus-return1-p>
<button popovertarget=focus-return1-p popovertargetaction=hide autofocus tabindex="0">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 tabindex="0">Toggle popover</button>
<div popover id=focus-return2-p>Popover with <button tabindex="0">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>
|