Skip to content

Commit

Permalink
Do not autofocus inert or hidden elements
Browse files Browse the repository at this point in the history
Closes [#546][]

According to the specification, there is an [algorithm][] for
determining the [focusable area][], that is, the elements with the
`[autofocus]` attribute that can receive focus:

> Elements that meet all the following criteria:
> * the element's [tabindex][] value is non-null, or the element is determined by the user agent to be focusable;
> * the element is either not a shadow host, or has a shadow root whose delegates focus is false;
> * the element is not actually [disabled][];
> * the element is not [inert][];
> * the element is either being rendered or being used as relevant canvas fallback content.

This commit extends the `Snapshot.firstAutofocusableElement` to
programmatically determine the focusable area based on the available
attributes, including checks for `<details>` and `<dialog>` ancestors
that do not declare `[open]`.

[#546]: #546
[tabindex]: https://html.spec.whatwg.org/multipage/interaction.html#tabindex-value
[algorithm]: https://html.spec.whatwg.org/multipage/interaction.html#get-the-focusable-area
[disabled]: https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled
[inert]: https://html.spec.whatwg.org/multipage/interaction.html#inert
[focusable area]: https://html.spec.whatwg.org/multipage/interaction.html#focusable-area
[hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden
  • Loading branch information
keithamus authored and seanpdoyle committed Jul 29, 2022
1 parent 1563ebb commit 0c6bc0e
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 1 deletion.
9 changes: 8 additions & 1 deletion src/core/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ export class Snapshot<E extends Element = Element> {
}

get firstAutofocusableElement() {
return this.element.querySelector("[autofocus]")
const inertDisabledOrHidden = "[inert], :disabled, [hidden], details:not([open]), dialog:not([open])"

for (const element of this.element.querySelectorAll("[autofocus]")) {
if (element.closest(inertDisabledOrHidden) == null) return element
else continue
}

return null
}

get permanentElements() {
Expand Down
28 changes: 28 additions & 0 deletions src/tests/fixtures/autofocus-inert.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Autofocus</title>
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
<script src="/src/tests/fixtures/test.js"></script>
</head>
<body>
<h1>Autofocus With Inert Elements</h1>
<dialog>
<button id="dialog-autofocus-element" autofocus>dialog[autofocus]</button>
</dialog>
<details>
<button id="details-autofocus-element" autofocus>details[autofocus]</button>
</details>
<div hidden>
<button id="hidden-autofocus-element" autofocus>div[hidden][autofocus]</button>
</div>
<div inert>
<button id="inert-autofocus-element" autofocus>div[inert][autofocus]</button>
</div>
<button id="disabled-autofocus-element" disabled autofocus>button[disabled][autofocus]</button>
<form disabled>
<button id="visible-autofocus-element" autofocus>button[autofocus]</button>
</form>
</body>
</html>
1 change: 1 addition & 0 deletions src/tests/fixtures/autofocus.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ <h1>Autofocus</h1>
<button autofocus id="first-autofocus-element" type="button">First [autofocus]</button>
<button autofocus id="second-autofocus-element" type="button">Second [autofocus]</button>

<a id="autofocus-inert-link" href="/src/tests/fixtures/autofocus-inert.html">autofocus-inert.html link</a>
<a id="frame-outer-link" href="/src/tests/fixtures/frames/form.html" data-turbo-frame="frame">Outer #frame link to frames/form.html</a>

<turbo-frame id="frame">
Expand Down
30 changes: 30 additions & 0 deletions src/tests/functional/autofocus_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,36 @@ test("test navigating a frame with a descendant link autofocuses [autofocus]:fir
)
})

test("test autofocus visible [autofocus] element on visit with inert elements", async ({ page }) => {
await page.click("#autofocus-inert-link")
await nextBeat()

assert.notOk(
await hasSelector(page, "#dialog-autofocus-element:focus"),
"autofocus element is ignored in a closed dialog"
)
assert.notOk(
await hasSelector(page, "#details-autofocus-element:focus"),
"autofocus element is ignored in a closed details"
)
assert.notOk(
await hasSelector(page, "#hidden-autofocus-element:focus"),
"autofocus element is ignored in a hidden div"
)
assert.notOk(
await hasSelector(page, "#inert-autofocus-element:focus"),
"autofocus element is ignored in an inert div"
)
assert.notOk(
await hasSelector(page, "#disabled-autofocus-element:focus"),
"autofocus element is ignored when disabled"
)
assert.ok(
await hasSelector(page, "#visible-autofocus-element:focus"),
"focuses the visible [autofocus] element on the page"
)
})

test("test navigating a frame with a link targeting the frame autofocuses [autofocus]:first-of-type", async ({
page,
}) => {
Expand Down

0 comments on commit 0c6bc0e

Please sign in to comment.