Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion src/components/CommandPalette.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,28 @@
color: var(--text);
font-size: 16px;
font-family: var(--font-family);
outline: none;
padding: 4px 0;
}

/*
* #699 — this file is unlayered (imported directly, not inside @layer
* focus), so an unconditional `outline: none` here would permanently defeat
* the global `*:focus-visible` ring in src/index.css: unlayered rules always
* beat layered ones regardless of specificity, so no rule inside @layer
* focus could ever restore it. Suppress only on :focus (mouse/programmatic)
* and restore explicitly on :focus-visible (keyboard), mirroring the same
* pattern the global reset uses.
*/
.command-palette-input:focus {
outline: none;
}

.command-palette-input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 4px;
}

.command-palette-clear-button,
.command-palette-close-button {
background: var(--surface-soft);
Expand All @@ -86,6 +104,15 @@
background: var(--surface);
}

/* Explicit reinforcement (the global *:focus-visible ring already applies
here since neither button overrides `outline`; stated explicitly so
accessibility audits don't have to trace the cascade to confirm it). */
.command-palette-clear-button:focus-visible,
.command-palette-close-button:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}

.command-palette-results {
flex: 1;
overflow-y: auto;
Expand Down
29 changes: 29 additions & 0 deletions src/focus-layer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,33 @@ describe("@layer focus contract", () => {
expect(marketplacePage).not.toMatch(/\.marketplace-filter-button:focus\s*\{/);
expect(recentlyActiveRail).not.toMatch(/button:focus\s*\{/);
});

// ── #699 — CommandPalette: unlayered outline:none permanently defeated the
// global *:focus-visible ring (unlayered CSS always beats @layer rules
// regardless of specificity, so nothing in @layer focus could restore it).
it("CommandPalette input no longer carries an unconditional outline:none", () => {
const css = read("src/components/CommandPalette.css");
// The old bug: `.command-palette-input { ... outline: none; ... }` with
// no accompanying :focus/:focus-visible split. Only a *conditional*
// `:focus { outline: none }` is allowed now.
expect(css).not.toMatch(/\.command-palette-input\s*\{[^}]*outline:\s*none/);
});

it("CommandPalette input restores a visible ring on :focus-visible", () => {
const css = read("src/components/CommandPalette.css");
expect(css).toMatch(
/\.command-palette-input:focus-visible[\s\S]*?outline:\s*2px solid var\(--accent\)/,
);
});

it("CommandPalette input only suppresses outline on :focus, not unconditionally", () => {
const css = read("src/components/CommandPalette.css");
expect(css).toMatch(/\.command-palette-input:focus\s*\{\s*outline:\s*none;\s*\}/);
});

it("CommandPalette clear/close buttons have explicit focus-visible reinforcement", () => {
const css = read("src/components/CommandPalette.css");
expect(css).toMatch(/\.command-palette-clear-button:focus-visible/);
expect(css).toMatch(/\.command-palette-close-button:focus-visible/);
});
});