ladybird/Tests/LibWeb/Text/input/HTML/focus-navigable-container.html
Andreas Kling 94302775bf LibWeb: Map document element focus to the viewport
Apply the focusing steps' get-the-focusable-area mapping before
rejecting a non-focusable target. This preserves documentElement.focus()
by mapping the non-focusable document element to the Document viewport.

Also map rendered navigable containers with content navigables to their
active document, while leaving hidden containers unfocused. Preserve
Window focus events for child document viewports reached through iframe
focus, while still suppressing the top-level viewport surrogate events.

Treat rendered object elements as focusable through their default
non-null tabindex, even when they show fallback or image content instead
of a child navigable.

Keep the spec focus-chain common-tail handling intact for viewport
focus. The Document object is only our surrogate for the viewport, so
designate viewport focus from the new focus target without dispatching
Window focus/focusin events for that top-level surrogate.

Pass that viewport surrogate as the fallback target for fragment
scrolling and NavigateEvent focus reset, so unfocusable body or fragment
targets still clear stale element focus.

Cover documentElement.focus() in both the activeElement and focus-chain
tests, including a tabindex document element that remains focused as an
element. Cover object focus with and without a child navigable, hidden
object focus attempts, iframe focus events, hidden iframe focus, and
blurring a focused iframe after it becomes hidden. Also cover viewport
fallback for intercepted navigation focus reset and fragment scrolling
to an unfocusable target.
2026-05-21 08:56:05 +02:00

118 lines
4.8 KiB
HTML

<!DOCTYPE html>
<script src="../include.js"></script>
<input id="before">
<input id="after">
<script>
function activeElementName() {
return document.activeElement.id || document.activeElement.localName;
}
function createIframe(id, style = "") {
return new Promise(resolve => {
const iframe = document.createElement("iframe");
iframe.id = id;
iframe.srcdoc = "<!DOCTYPE html><body>frame</body>";
if (style)
iframe.setAttribute("style", style);
iframe.onload = () => resolve(iframe);
document.body.appendChild(iframe);
});
}
function createObject() {
return new Promise(resolve => {
const object = document.createElement("object");
object.type = "text/html";
object.data = "about:srcdoc";
object.onload = () => resolve(object);
document.body.appendChild(object);
});
}
function createFallbackObject(id, style = "") {
const object = document.createElement("object");
object.id = id;
object.innerHTML = "<span>fallback</span>";
if (style)
object.setAttribute("style", style);
document.body.appendChild(object);
return object;
}
asyncTest(async done => {
const before = document.getElementById("before");
const after = document.getElementById("after");
const object = await createObject();
let blurredByObject = false;
before.addEventListener("blur", () => {
blurredByObject = true;
}, { once: true });
before.focus();
object.focus();
println(`before blurred after object.focus(): ${blurredByObject}`);
const fallbackObject = createFallbackObject("fallback-object");
let changeCommittedByFallbackObject = false;
before.addEventListener("change", () => {
changeCommittedByFallbackObject = true;
}, { once: true });
internals.sendText(before, "typed");
fallbackObject.focus();
println(`fallback object focus: active=${activeElementName()}, changeCommitted=${changeCommittedByFallbackObject}`);
const hiddenFallbackObject = createFallbackObject("hidden-fallback-object", "display: none");
let beforeBlurredByHiddenFallbackObject = false;
before.addEventListener("blur", () => {
beforeBlurredByHiddenFallbackObject = true;
}, { once: true });
before.focus();
hiddenFallbackObject.focus();
println(`hidden fallback object focus attempt: active=${activeElementName()}, beforeBlurred=${beforeBlurredByHiddenFallbackObject}`);
const frame = await createIframe("frame");
let frameFocusCount = 0;
let frameBlurCount = 0;
let frameWindowFocusCount = 0;
let frameWindowBlurCount = 0;
frame.addEventListener("focus", () => ++frameFocusCount);
frame.addEventListener("blur", () => ++frameBlurCount);
frame.contentWindow.addEventListener("focus", () => ++frameWindowFocusCount);
frame.contentWindow.addEventListener("blur", () => ++frameWindowBlurCount);
before.focus();
frame.focus();
after.focus();
println(`iframe focus/blur events: frame=${frameFocusCount}/${frameBlurCount}, window=${frameWindowFocusCount}/${frameWindowBlurCount}`);
const hiddenFrame = await createIframe("hidden-frame", "display: none");
let beforeBlurredByHiddenFrame = false;
let hiddenFrameFocusCount = 0;
let hiddenFrameWindowFocusCount = 0;
before.addEventListener("blur", () => {
beforeBlurredByHiddenFrame = true;
}, { once: true });
hiddenFrame.addEventListener("focus", () => ++hiddenFrameFocusCount);
hiddenFrame.contentWindow.addEventListener("focus", () => ++hiddenFrameWindowFocusCount);
before.focus();
hiddenFrame.focus();
println(`hidden iframe focus attempt: active=${activeElementName()}, beforeBlurred=${beforeBlurredByHiddenFrame}, frameFocus=${hiddenFrameFocusCount}, windowFocus=${hiddenFrameWindowFocusCount}`);
const hideAfterFocusFrame = await createIframe("hide-after-focus-frame");
let hideAfterFocusFrameBlurCount = 0;
let hideAfterFocusFrameWindowBlurCount = 0;
hideAfterFocusFrame.addEventListener("blur", () => ++hideAfterFocusFrameBlurCount);
hideAfterFocusFrame.contentWindow.addEventListener("blur", () => ++hideAfterFocusFrameWindowBlurCount);
hideAfterFocusFrame.focus();
hideAfterFocusFrame.style.display = "none";
hideAfterFocusFrame.blur();
println(`hidden focused iframe blur: active=${activeElementName()}, frameBlur=${hideAfterFocusFrameBlurCount}, windowBlur=${hideAfterFocusFrameWindowBlurCount}`);
done();
});
</script>