Skip to content

Commit b47a7e3

Browse files
mfreed7chromium-wpt-export-bot
authored andcommitted
Maintain interest invoker state when focus moves into iframe popover
When an interest invoker invokes an `<iframe popover>`, moving focus into the iframe's content document caused the invoker to lose focus (because the iframe content document now has focus). This triggered a loseinterest task that closed the popover. This CL addresses the issue by: 1. Enabling AllSourceInterestInvokersRecursive and Element::HandleInterestForHoverOrFocus to traverse across document boundaries via Document::LocalOwner(). 2. Handling bubbling focusin and focusout events at the child Document::DefaultEventHandler and forwarding them to the local frame owner element. 3. Handling focus changes into and out of cross-origin (remote) frames in FocusController::SetFocusedFrame. Fixed: 534893151 Change-Id: Ib8fb5a7fc11c7397b7c120e1fc45feb267f96a67 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8264990 Auto-Submit: Mason Freed <masonf@chromium.org> Reviewed-by: Joey Arhar <jarhar@chromium.org> Commit-Queue: Mason Freed <masonf@chromium.org> Cr-Commit-Position: refs/heads/main@{#1683588}
1 parent c2160b7 commit b47a7e3

1 file changed

Lines changed: 168 additions & 0 deletions

File tree

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<link rel="author" href="mailto:masonf@chromium.org">
4+
<link rel="help" href="https://open-ui.org/components/interest-invokers.explainer/">
5+
<script src="/resources/testharness.js"></script>
6+
<script src="/resources/testharnessreport.js"></script>
7+
<script src="/resources/testdriver.js"></script>
8+
<script src="/resources/testdriver-actions.js"></script>
9+
<script src="/resources/testdriver-vendor.js"></script>
10+
<script src="resources/invoker-utils.js"></script>
11+
<script src="/html/semantics/popovers/resources/popover-utils.js"></script>
12+
<script src="/common/get-host-info.sub.js"></script>
13+
14+
<button id="button" interestfor="popover">Button</button>
15+
<iframe popover="hint" id="popover" srcdoc="<a href='#' id='link' tabindex='0'>Link</a>"></iframe>
16+
<button id="button_crossorigin" interestfor="popover_crossorigin">Button Cross-Origin</button>
17+
<iframe popover="hint" id="popover_crossorigin"></iframe>
18+
<button id="button_shadow" interestfor="popover_shadow">Button Shadow</button>
19+
<iframe popover="hint" id="popover_shadow"></iframe>
20+
<button id="button_multi" interestfor="popover_multi">Button Multi</button>
21+
<iframe popover="hint" id="popover_multi" srcdoc="<button id='btn1'>Button 1</button><button id='btn2'>Button 2</button>"></iframe>
22+
<button id="other">Other</button>
23+
24+
<style>
25+
[interestfor] {
26+
interest-delay: 0s;
27+
}
28+
</style>
29+
30+
<script>
31+
window.onload = () => {
32+
promise_test(async function (t) {
33+
t.add_cleanup(() => other.focus());
34+
popover.hidePopover();
35+
const link = popover.contentDocument.getElementById('link');
36+
assert_true(!!link, 'link in iframe should exist');
37+
38+
await focusOn(button);
39+
const kTab = '\uE004';
40+
await test_driver.send_keys(document.activeElement, kTab);
41+
await waitForRender();
42+
if (popover.contentDocument.activeElement !== link) {
43+
await test_driver.send_keys(popover.contentDocument.activeElement || popover.contentDocument.body, kTab);
44+
await waitForRender();
45+
}
46+
assert_equals(popover.contentDocument.activeElement, link, 'link in iframe should be focused');
47+
assert_true(popover.matches(':popover-open'), 'popover should remain open when focus moves into iframe');
48+
49+
await focusOn(other);
50+
assert_false(popover.matches(':popover-open'), 'popover should close when focus moves to unrelated element');
51+
}, 'Interest is maintained when focus moves into an iframe popover via Tab');
52+
53+
promise_test(async function (t) {
54+
t.add_cleanup(() => other.focus());
55+
popover.hidePopover();
56+
const link = popover.contentDocument.getElementById('link');
57+
assert_true(!!link, 'link in iframe should exist');
58+
59+
await focusOn(button);
60+
assert_true(popover.matches(':popover-open'), 'popover should open on button focus');
61+
62+
link.focus();
63+
await waitForRender();
64+
assert_equals(popover.contentDocument.activeElement, link, 'link in iframe should be focused');
65+
assert_true(popover.matches(':popover-open'), 'popover should remain open when focus is directly moved into iframe');
66+
67+
await focusOn(other);
68+
assert_false(popover.matches(':popover-open'), 'popover should close when focus moves to unrelated element');
69+
}, 'Interest is maintained when focus moves into an iframe popover directly');
70+
71+
promise_test(async function (t) {
72+
t.add_cleanup(() => other.focus());
73+
popover_crossorigin.hidePopover();
74+
const url = get_host_info().HTTP_REMOTE_ORIGIN + '/common/blank.html';
75+
popover_crossorigin.src = url;
76+
await new Promise(resolve => popover_crossorigin.addEventListener('load', resolve, {once: true}));
77+
78+
await focusOn(button_crossorigin);
79+
assert_true(popover_crossorigin.matches(':popover-open'), 'popover should open on button focus');
80+
81+
popover_crossorigin.focus();
82+
await waitForRender();
83+
assert_true(popover_crossorigin.matches(':popover-open'), 'popover should remain open when focus moves into cross-origin iframe');
84+
85+
await focusOn(other);
86+
assert_false(popover_crossorigin.matches(':popover-open'), 'popover should close when focus moves to unrelated element');
87+
}, 'Interest is maintained when focus moves into a cross-origin iframe popover');
88+
89+
promise_test(async function (t) {
90+
t.add_cleanup(() => other.focus());
91+
popover_shadow.hidePopover();
92+
const doc = popover_shadow.contentDocument;
93+
const host = doc.createElement('div');
94+
doc.body.appendChild(host);
95+
const shadowRoot = host.attachShadow({mode: 'open'});
96+
const shadowBtn = doc.createElement('button');
97+
shadowBtn.textContent = 'Inside Shadow';
98+
shadowRoot.appendChild(shadowBtn);
99+
100+
await focusOn(button_shadow);
101+
assert_true(popover_shadow.matches(':popover-open'), 'popover should open on button focus');
102+
103+
shadowBtn.focus();
104+
await waitForRender();
105+
assert_equals(shadowRoot.activeElement, shadowBtn, 'button in shadow root should be focused');
106+
assert_true(popover_shadow.matches(':popover-open'), 'popover should remain open when focus is moved into shadow DOM inside iframe');
107+
108+
await focusOn(other);
109+
assert_false(popover_shadow.matches(':popover-open'), 'popover should close when focus moves to unrelated element');
110+
}, 'Interest is maintained when focus moves into a shadow root inside an iframe popover');
111+
112+
promise_test(async function (t) {
113+
t.add_cleanup(() => other.focus());
114+
popover_multi.hidePopover();
115+
const btn1 = popover_multi.contentDocument.getElementById('btn1');
116+
const btn2 = popover_multi.contentDocument.getElementById('btn2');
117+
assert_true(!!btn1 && !!btn2, 'buttons in iframe should exist');
118+
119+
await focusOn(button_multi);
120+
assert_true(popover_multi.matches(':popover-open'), 'popover should open on button focus');
121+
122+
btn1.focus();
123+
await waitForRender();
124+
assert_equals(popover_multi.contentDocument.activeElement, btn1, 'btn1 in iframe should be focused');
125+
assert_true(popover_multi.matches(':popover-open'), 'popover should remain open when btn1 is focused');
126+
127+
btn2.focus();
128+
await waitForRender();
129+
assert_equals(popover_multi.contentDocument.activeElement, btn2, 'btn2 in iframe should be focused');
130+
assert_true(popover_multi.matches(':popover-open'), 'popover should remain open when focus moves to btn2 inside same iframe');
131+
132+
await focusOn(other);
133+
assert_false(popover_multi.matches(':popover-open'), 'popover should close when focus moves to unrelated element');
134+
}, 'Moving focus between elements inside the same iframe popover does not close it');
135+
136+
promise_test(async function (t) {
137+
t.add_cleanup(() => other.focus());
138+
popover.hidePopover();
139+
const link = popover.contentDocument.getElementById('link');
140+
assert_true(!!link, 'link in iframe should exist');
141+
142+
assert_false(popover.matches(':popover-open'), 'popover should start closed');
143+
144+
// Dispatch synthetic focusin event on link inside iframe
145+
link.dispatchEvent(new FocusEvent('focusin', {bubbles: true}));
146+
await waitForRender();
147+
assert_false(popover.matches(':popover-open'), 'synthetic focusin in iframe should not open popover');
148+
149+
// Open popover by focusing button
150+
await focusOn(button);
151+
assert_true(popover.matches(':popover-open'), 'popover should open on button focus');
152+
153+
// Focus link inside iframe (popover stays open)
154+
link.focus();
155+
await waitForRender();
156+
assert_true(popover.matches(':popover-open'), 'popover should remain open when link is focused');
157+
158+
// Dispatch synthetic focusout event on link inside iframe
159+
link.dispatchEvent(new FocusEvent('focusout', {bubbles: true}));
160+
await waitForRender();
161+
assert_true(popover.matches(':popover-open'), 'synthetic focusout in iframe should not close popover');
162+
163+
// Real focus to unrelated element closes popover
164+
await focusOn(other);
165+
assert_false(popover.matches(':popover-open'), 'popover should close when focus moves to unrelated element');
166+
}, 'Untrusted synthetic focus events in child document do not trigger interest actions');
167+
};
168+
</script>

0 commit comments

Comments
 (0)