Skip to content

Commit eb47c89

Browse files
author
RealDiligent
committed
fix(ui-kit): move focus ring to focus-visible on Select/Dialog/Sheet/NavigationMenu
SelectTrigger, DialogPrimitive.Close, SheetPrimitive.Close, and NavigationMenuTrigger applied their focus ring/highlight on plain focus:, which also fires on mouse-click focus and leaves a lingering ring/highlight after every click — unlike button/input/switch/checkbox/toggle/slider and every other interactive primitive in the kit, which ring on focus-visible:. Moves only the visible ring/highlight classes to focus-visible: (keeping focus:outline-none, which correctly clears the native outline on any focus), with a regression test asserting the corrected class lists. Closes #8304
1 parent 78aeb1c commit eb47c89

5 files changed

Lines changed: 43 additions & 4 deletions

File tree

packages/loopover-ui-kit/src/components/dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const DialogContent = React.forwardRef<
4444
{...props}
4545
>
4646
{children}
47-
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background cursor-pointer transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
47+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background cursor-pointer transition-opacity hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
4848
<X className="h-4 w-4" />
4949
<span className="sr-only">Close</span>
5050
</DialogPrimitive.Close>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { render } from "@testing-library/react";
2+
import { describe, expect, it } from "vitest";
3+
4+
import { navigationMenuTriggerStyle } from "./navigation-menu";
5+
import { Select, SelectTrigger, SelectValue } from "./select";
6+
7+
// Regression for #8304: SelectTrigger, the Dialog/Sheet close buttons, and NavigationMenuTrigger must
8+
// apply their focus ring/highlight via `focus-visible:` (keyboard/programmatic focus only), matching
9+
// every other interactive primitive in @loopover/ui-kit — never on plain `focus:`, which also fires on
10+
// mouse-click focus and leaves a lingering ring/highlight. `focus:outline-none` is intentionally kept
11+
// (clearing the native outline on any focus is correct and shared by every primitive).
12+
describe("focus-visible convention (#8304)", () => {
13+
it("navigationMenuTriggerStyle highlights on focus-visible, never a bare focus:bg-accent", () => {
14+
const classes = navigationMenuTriggerStyle();
15+
expect(classes).toContain("focus-visible:bg-accent");
16+
expect(classes).toContain("focus-visible:text-accent-foreground");
17+
// No bare focus:bg-accent / focus:text-accent-foreground (the data-[state=open]:focus:bg-accent
18+
// compound is a separate, intentional open-state rule and is allowed).
19+
expect(classes).not.toMatch(/(?<!:)\bfocus:bg-accent\b/);
20+
expect(classes).not.toMatch(/(?<!:)\bfocus:text-accent-foreground\b/);
21+
// The native-outline clear stays on plain focus:.
22+
expect(classes).toContain("focus:outline-none");
23+
});
24+
25+
it("SelectTrigger rings on focus-visible, never a bare focus:ring", () => {
26+
const { getByRole } = render(
27+
<Select>
28+
<SelectTrigger aria-label="pick">
29+
<SelectValue placeholder="pick" />
30+
</SelectTrigger>
31+
</Select>,
32+
);
33+
const trigger = getByRole("combobox");
34+
expect(trigger.className).toContain("focus-visible:ring-1");
35+
expect(trigger.className).toContain("focus-visible:ring-ring");
36+
expect(trigger.className).not.toMatch(/(?<!-)\bfocus:ring/);
37+
expect(trigger.className).toContain("focus:outline-none");
38+
});
39+
});

packages/loopover-ui-kit/src/components/navigation-menu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
4141
const NavigationMenuItem = NavigationMenuPrimitive.Item;
4242

4343
const navigationMenuTriggerStyle = cva(
44-
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent",
44+
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent",
4545
);
4646

4747
const NavigationMenuTrigger = React.forwardRef<

packages/loopover-ui-kit/src/components/select.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
1919
<SelectPrimitive.Trigger
2020
ref={ref}
2121
className={cn(
22-
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background cursor-pointer data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
22+
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background cursor-pointer data-[placeholder]:text-muted-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
2323
className,
2424
)}
2525
{...props}

packages/loopover-ui-kit/src/components/sheet.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ const SheetContent = React.forwardRef<
6969
className={cn(sheetVariants({ side }), className)}
7070
{...props}
7171
>
72-
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background cursor-pointer transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
72+
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background cursor-pointer transition-opacity hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
7373
<X className="h-4 w-4" />
7474
<span className="sr-only">Close</span>
7575
</SheetPrimitive.Close>

0 commit comments

Comments
 (0)