fix(player): the card under the big view is out of the Tab order, and the focus leaves it and comes back
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
import { useRef } from "react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { useDialogChrome } from "./dialogFocus";
|
||||
import { focusablesIn, useDialogChrome } from "./dialogFocus";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
@@ -36,6 +36,25 @@ describe("useDialogChrome · the page scroll lock", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("focusablesIn", () => {
|
||||
// A trap that counted them had its "last" control where Tab never lands, and let the focus out.
|
||||
it("leaves out what an inert part holds", () => {
|
||||
const offsetParent = vi
|
||||
.spyOn(HTMLElement.prototype, "offsetParent", "get")
|
||||
.mockReturnValue(document.body);
|
||||
const { container } = render(
|
||||
<div>
|
||||
<button>reachable</button>
|
||||
<div inert>
|
||||
<button>under</button>
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
expect(focusablesIn(container).map((el) => el.textContent)).toEqual(["reachable"]);
|
||||
offsetParent.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("useDialogChrome · initial focus", () => {
|
||||
it("lands on the first control by default", () => {
|
||||
// jsdom has no layout, so the visibility filter (offsetParent) would see nothing focusable.
|
||||
|
||||
@@ -9,12 +9,15 @@ import { useEffect, useEffectEvent, useRef, useState, type RefObject } from "rea
|
||||
|
||||
// Tabbable elements inside the dialog, in DOM order. `:not([tabindex="-1"])` drops programmatically
|
||||
// focusable-only nodes; the visibility filter (offsetParent) drops display:none ones (e.g. a
|
||||
// collapsed disclosure) so the trap never lands focus on something the user can't see.
|
||||
// collapsed disclosure) so the trap never lands focus on something the user can't see, and an inert
|
||||
// part's are not tabbable: counted, the trap's last control is one Tab never reaches.
|
||||
const FOCUSABLE =
|
||||
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
export const focusablesIn = (root: HTMLElement): HTMLElement[] =>
|
||||
[...root.querySelectorAll<HTMLElement>(FOCUSABLE)].filter((el) => el.offsetParent !== null);
|
||||
[...root.querySelectorAll<HTMLElement>(FOCUSABLE)].filter(
|
||||
(el) => el.offsetParent !== null && !el.closest("[inert]"),
|
||||
);
|
||||
|
||||
/** A form's first field — where a dialog opened to fill something in takes the focus: its first
|
||||
* control, unless one is marked `data-initial-focus`. The mark is for a form with a control above
|
||||
|
||||
Reference in New Issue
Block a user