fix(focus): a removed control's focus skips a neighbour the CSS has hidden
A display:none button refuses focus(), so handing the focus to a hidden column's header button dropped it to <body>.
This commit is contained in:
@@ -3,7 +3,7 @@ import { afterEach, describe, expect, it } from "vitest";
|
||||
import { act, cleanup, render } from "@testing-library/react";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useFocusKeeper } from "./useFocusKeeper";
|
||||
import { focusNeighbour, useFocusKeeper } from "./useFocusKeeper";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
@@ -77,3 +77,30 @@ describe("useFocusKeeper", () => {
|
||||
expect(document.activeElement).toBe(document.body);
|
||||
});
|
||||
});
|
||||
|
||||
describe("focusNeighbour", () => {
|
||||
// E28 F2: the neighbour of the hidden-filter cue's chip is the table's first header button — the
|
||||
// hidden column's own, `display: none`, which refuses focus(): the focus fell to <body>. jsdom paints
|
||||
// nothing, so `checkVisibility` is stood in for, answering for the one hidden button.
|
||||
it("passes over a focusable the CSS has taken off screen", () => {
|
||||
render(
|
||||
<div>
|
||||
<div>
|
||||
<button>chip</button>
|
||||
</div>
|
||||
<button data-off="">hidden header</button>
|
||||
<button>shown header</button>
|
||||
</div>,
|
||||
);
|
||||
Object.defineProperty(HTMLElement.prototype, "checkVisibility", {
|
||||
configurable: true,
|
||||
value(this: HTMLElement) {
|
||||
return !this.hasAttribute("data-off");
|
||||
},
|
||||
});
|
||||
btn("chip").focus();
|
||||
expect(focusNeighbour(btn("chip"))).toBe(true);
|
||||
expect(document.activeElement).toBe(btn("shown header"));
|
||||
delete (HTMLElement.prototype as Partial<HTMLElement>).checkVisibility;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,12 +15,17 @@ import { useEffect, type RefObject } from "react";
|
||||
const FOCUSABLE =
|
||||
'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
// A focusable element the CSS has taken off screen (a `tabletHidden` column's header button) refuses
|
||||
// `focus()`, and the focus it was meant to catch fell to <body> (E28 F2, measured in Chrome). An engine
|
||||
// without `checkVisibility` (jsdom) paints nothing, so there everything counts as shown.
|
||||
const shown = (el: HTMLElement) => !el.checkVisibility || el.checkVisibility();
|
||||
|
||||
function firstFocusable(node: Node | null, fromEnd = false): HTMLElement | null {
|
||||
let n: Node | null = node;
|
||||
while (n) {
|
||||
if (n instanceof HTMLElement) {
|
||||
if (n.matches(FOCUSABLE)) return n;
|
||||
const all = n.querySelectorAll<HTMLElement>(FOCUSABLE);
|
||||
if (n.matches(FOCUSABLE) && shown(n)) return n;
|
||||
const all = [...n.querySelectorAll<HTMLElement>(FOCUSABLE)].filter(shown);
|
||||
const hit = fromEnd ? all[all.length - 1] : all[0];
|
||||
if (hit) return hit;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user