build(eslint): focus-safe-disabled folds into button-primitive

Two rules looked at the same <button>: button-primitive refused a raw one
outside the control primitives, focus-safe-disabled refused a raw
`disabled` one everywhere but FocusSafeButton and AnchoredPopover. They are
one rule now, with the same reach: outside the primitives a raw button is
refused (a disabled one with the focus message); inside a primitive and in
a test only a raw `<button disabled>` is, and the two native-disabled owners
are named in the rule rather than in a config override. One override block,
one rule entry and two register rows go; the snapshot loses a config group.
focusSafe.contract.test.ts pins every case, the primitive and test ones new.
This commit is contained in:
2026-10-05 00:19:39 +02:00
parent 2eb20ca227
commit ca707fd390
4 changed files with 45 additions and 102 deletions
+18 -13
View File
@@ -2,16 +2,19 @@ import { describe, expect, it } from "vitest";
import { eslint } from "./lintProbe.testkit";
// `local/focus-safe-disabled` (eslint.config.js): a raw `<button disabled>` throws a focused keyboard
// user to <body> the moment it turns disabled. ~60 were swept onto ui/FocusSafeButton; this pins that
// the rule keeping a new one out actually fires.
const RULE = "local/focus-safe-disabled";
// `local/button-primitive`'s disabled check (eslint.config.js): a raw `<button disabled>` throws a
// focused keyboard user to <body> the moment it turns disabled. ~60 were swept onto ui/FocusSafeButton;
// this pins that the check keeping a new one out fires — also inside a control primitive and a test,
// where the rest of the rule stands aside.
async function hits(source: string, filePath = "src/components/__probe.tsx"): Promise<number> {
const [result] = await eslint.lintText(source, { filePath });
return (result?.messages ?? []).filter((m) => m.ruleId === RULE).length;
return (result?.messages ?? []).filter(
(m) => m.ruleId === "local/button-primitive" && m.messageId === "disabled",
).length;
}
const DISABLED = "export const D = (p: { d: boolean }) => <button disabled={p.d}>x</button>;\n";
describe("a raw button that can be disabled", () => {
it("is refused, statically or not", async () => {
expect(
@@ -20,13 +23,15 @@ describe("a raw button that can be disabled", () => {
expect(await hits("export const B = () => <button disabled>x</button>;\n")).toBe(1);
});
it("leaves a raw button with no disabled, and the primitive, alone", async () => {
it("is refused inside a control primitive and in a test too", async () => {
expect(await hits(DISABLED, "src/components/ui/Button.tsx")).toBe(1);
// an existing test file: the project service admits only lintProbe.testkit's probe names
expect(await hits(DISABLED, "src/components/Feed.countStale.dom.test.tsx")).toBe(1);
});
it("leaves a raw button with no disabled, and the two native-disabled owners, alone", async () => {
expect(await hits("export const C = () => <button onClick={() => {}}>x</button>;\n")).toBe(0);
expect(
await hits(
"export const D = (p: { d: boolean }) => <button disabled={p.d}>x</button>;\n",
"src/components/ui/FocusSafeButton.tsx",
),
).toBe(0);
expect(await hits(DISABLED, "src/components/ui/FocusSafeButton.tsx")).toBe(0);
expect(await hits(DISABLED, "src/components/ui/AnchoredPopover.tsx")).toBe(0);
});
});