mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
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.
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ function DialogContent({
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-muted data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
||||
<DialogPrimitive.Close className="ring-offset-background focus-visible:ring-ring data-[state=open]:bg-muted data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -70,7 +70,7 @@ function SheetContent({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-muted absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<SheetPrimitive.Close className="ring-offset-background focus-visible:ring-ring data-[state=open]:bg-muted absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
|
||||
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-0 focus-visible:ring-offset-0 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user