From 84df964b05235529d4366b7b34f64dce8a2fb0e7 Mon Sep 17 00:00:00 2001 From: morgmart <98432065+morgmart@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:03:54 -0700 Subject: [PATCH] simplify focus-visible behavior and remove keyboard nav detection (#127) * refactor: simplify focus-visible behavior and remove keyboard nav detection Remove the useKeyboardNavDetection hook that gated focus rings behind a data-keyboard-nav attribute. Focus-visible rings are now always available via the browser's native :focus-visible heuristic, which already distinguishes keyboard from pointer interaction. - Delete useKeyboardNavDetection hook and its App.tsx usage - Make global focus-visible ring unconditional in globals.css - Switch Dialog/Sheet close buttons from focus: to focus-visible: - Opt Input/Textarea out of global ring (rely on border change) - Remove input-group focus ring state (handled by global rule) * style: enhance focus-visible styles for input components - Added focus-visible border styles to InputGroup, Input, and Textarea components to improve accessibility and visual feedback. - Updated class names to ensure consistent focus behavior across all input-related components. --- ui/goose2/src/app/App.tsx | 2 - .../shared/hooks/useKeyboardNavDetection.ts | 53 ------------------- ui/goose2/src/shared/styles/globals.css | 6 +-- ui/goose2/src/shared/ui/dialog.tsx | 2 +- ui/goose2/src/shared/ui/input-group.tsx | 3 +- ui/goose2/src/shared/ui/input.tsx | 2 +- ui/goose2/src/shared/ui/sheet.tsx | 2 +- ui/goose2/src/shared/ui/textarea.tsx | 2 +- 8 files changed, 8 insertions(+), 64 deletions(-) delete mode 100644 ui/goose2/src/shared/hooks/useKeyboardNavDetection.ts diff --git a/ui/goose2/src/app/App.tsx b/ui/goose2/src/app/App.tsx index 4f99406138..5a3889741d 100644 --- a/ui/goose2/src/app/App.tsx +++ b/ui/goose2/src/app/App.tsx @@ -1,12 +1,10 @@ import { useEffect } from "react"; import { AppShell } from "@/app/AppShell"; -import { useKeyboardNavDetection } from "@/shared/hooks/useKeyboardNavDetection"; import { useScrollFade } from "@/shared/hooks/useScrollFade"; import { useZoom } from "@/shared/hooks/useZoom"; export function App() { - useKeyboardNavDetection(); useScrollFade(); useZoom(); useEffect(() => { diff --git a/ui/goose2/src/shared/hooks/useKeyboardNavDetection.ts b/ui/goose2/src/shared/hooks/useKeyboardNavDetection.ts deleted file mode 100644 index e150652d0e..0000000000 --- a/ui/goose2/src/shared/hooks/useKeyboardNavDetection.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect } from "react"; - -const ATTR = "data-keyboard-nav"; -const NAVIGATION_KEYS = new Set([ - "Tab", - "ArrowUp", - "ArrowDown", - "ArrowLeft", - "ArrowRight", - "Enter", - " ", - "Escape", - "Home", - "End", - "PageUp", - "PageDown", -]); - -function isKeyboardNavigationEvent(event: KeyboardEvent) { - if (event.key === "Shift" || event.key === "Meta") { - return false; - } - - if (event.ctrlKey || event.metaKey) { - return true; - } - - return NAVIGATION_KEYS.has(event.key); -} - -export function useKeyboardNavDetection() { - useEffect(() => { - const root = document.documentElement; - - function onKeyDown(e: KeyboardEvent) { - if (isKeyboardNavigationEvent(e)) { - root.setAttribute(ATTR, ""); - } - } - - function onPointerDown() { - root.removeAttribute(ATTR); - } - - window.addEventListener("keydown", onKeyDown, true); - window.addEventListener("pointerdown", onPointerDown, true); - - return () => { - window.removeEventListener("keydown", onKeyDown, true); - window.removeEventListener("pointerdown", onPointerDown, true); - }; - }, []); -} diff --git a/ui/goose2/src/shared/styles/globals.css b/ui/goose2/src/shared/styles/globals.css index b03c9d6af2..58b57ad7a2 100644 --- a/ui/goose2/src/shared/styles/globals.css +++ b/ui/goose2/src/shared/styles/globals.css @@ -746,9 +746,9 @@ body, outline: none !important; } - html[data-keyboard-nav] *:not(body):not(.focus-override) { + *:not(body):not(.focus-override) { &:focus-visible { - @apply focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:outline-hidden; + @apply ring-ring ring-offset-background ring-2 ring-offset-1 outline-hidden; } } @@ -756,7 +756,7 @@ body, @apply hover:text-foreground/80 inline-flex items-center gap-1 rounded-xs font-medium underline underline-offset-4; } - html[data-keyboard-nav] *:not(body):not(.focus-override).link { + *:not(body):not(.focus-override).link { &:focus-visible { @apply ring-ring ring-offset-background ring-2 ring-offset-2 outline-hidden; } diff --git a/ui/goose2/src/shared/ui/dialog.tsx b/ui/goose2/src/shared/ui/dialog.tsx index d5e6cc7ee8..db9613b442 100644 --- a/ui/goose2/src/shared/ui/dialog.tsx +++ b/ui/goose2/src/shared/ui/dialog.tsx @@ -65,7 +65,7 @@ function DialogContent({ > {children} {showCloseButton && ( - + Close diff --git a/ui/goose2/src/shared/ui/input-group.tsx b/ui/goose2/src/shared/ui/input-group.tsx index 2e32bd523c..6955a79f26 100644 --- a/ui/goose2/src/shared/ui/input-group.tsx +++ b/ui/goose2/src/shared/ui/input-group.tsx @@ -21,8 +21,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) { "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3", "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3", - // Focus state. - "has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1", + "has-[[data-slot=input-group-control]:focus-visible]:border-ring", // Error state. "has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40", diff --git a/ui/goose2/src/shared/ui/input.tsx b/ui/goose2/src/shared/ui/input.tsx index 5a3280c57f..5c360f65b5 100644 --- a/ui/goose2/src/shared/ui/input.tsx +++ b/ui/goose2/src/shared/ui/input.tsx @@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { type={type} data-slot="input" className={cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input hover:border-border-input-hover flex h-9 w-full min-w-0 rounded-input border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input hover:border-border-input-hover focus-visible:border-ring focus-visible:ring-0 focus-visible:ring-offset-0 flex h-9 w-full min-w-0 rounded-input border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className, diff --git a/ui/goose2/src/shared/ui/sheet.tsx b/ui/goose2/src/shared/ui/sheet.tsx index 3b852d976f..b9bbdb4cb9 100644 --- a/ui/goose2/src/shared/ui/sheet.tsx +++ b/ui/goose2/src/shared/ui/sheet.tsx @@ -70,7 +70,7 @@ function SheetContent({ {...props} > {children} - + Close diff --git a/ui/goose2/src/shared/ui/textarea.tsx b/ui/goose2/src/shared/ui/textarea.tsx index 081e49febf..e28762be9f 100644 --- a/ui/goose2/src/shared/ui/textarea.tsx +++ b/ui/goose2/src/shared/ui/textarea.tsx @@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {