diff --git a/app/app.css b/app/app.css index 9bc1f29..531d075 100644 --- a/app/app.css +++ b/app/app.css @@ -69,7 +69,7 @@ html { --secondary: #1c1f26; --secondary-foreground: #ffffff; --muted: #1c1f26; - --muted-foreground: rgb(255 255 255 / 62%); + --muted-foreground: rgb(255 255 255 / 55%); --accent: #2ce1c1; --accent-foreground: #14171e; --destructive: oklch(0.65 0.22 25); diff --git a/app/components/AutodraftSettings.tsx b/app/components/AutodraftSettings.tsx index 1587573..dbc64c4 100644 --- a/app/components/AutodraftSettings.tsx +++ b/app/components/AutodraftSettings.tsx @@ -200,8 +200,26 @@ function AutodraftOptions({ const isDisabled = isMyTurn; + function handleGroupKeyDown(e: React.KeyboardEvent) { + if (isDisabled) return; + if (!["ArrowDown", "ArrowRight", "ArrowUp", "ArrowLeft"].includes(e.key)) return; + e.preventDefault(); + const idx = OPTION_ORDER.indexOf(localState); + const next = e.key === "ArrowDown" || e.key === "ArrowRight" + ? (idx + 1) % OPTION_ORDER.length + : (idx - 1 + OPTION_ORDER.length) % OPTION_ORDER.length; + const nextState = OPTION_ORDER[next]; + handleStateChange(nextState); + e.currentTarget.querySelector(`[data-radio-value="${nextState}"]`)?.focus(); + } + return ( -
+
{OPTION_ORDER.map((state) => { const { label } = OPTIONS[state]; const isActive = localState === state; @@ -211,6 +229,8 @@ function AutodraftOptions({ type="button" role="radio" aria-checked={isActive} + tabIndex={isActive ? 0 : -1} + data-radio-value={state} disabled={isDisabled} onClick={() => handleStateChange(state)} className={`w-full py-2.5 px-3 text-left transition-colors border-b last:border-b-0 flex items-center justify-between gap-2 ${getButtonClassName(state, isActive)} ${ @@ -387,6 +407,19 @@ export function AutodraftSettings({ const isDisabled = isMyTurn; + function handleGroupKeyDown(e: React.KeyboardEvent) { + if (isDisabled) return; + if (!["ArrowDown", "ArrowRight", "ArrowUp", "ArrowLeft"].includes(e.key)) return; + e.preventDefault(); + const idx = OPTION_ORDER.indexOf(localState); + const next = e.key === "ArrowDown" || e.key === "ArrowRight" + ? (idx + 1) % OPTION_ORDER.length + : (idx - 1 + OPTION_ORDER.length) % OPTION_ORDER.length; + const nextState = OPTION_ORDER[next]; + handleStateChange(nextState); + e.currentTarget.querySelector(`[data-radio-value="${nextState}"]`)?.focus(); + } + return (
{/* Header with info icon inline */} @@ -419,7 +452,7 @@ export function AutodraftSettings({
-
+
{OPTION_ORDER.map((state) => { const { label } = OPTIONS[state]; const isActive = localState === state; @@ -429,6 +462,8 @@ export function AutodraftSettings({ type="button" role="radio" aria-checked={isActive} + tabIndex={isActive ? 0 : -1} + data-radio-value={state} disabled={isDisabled} onClick={() => handleStateChange(state)} className={`w-full py-2.5 px-3 text-left transition-colors border-b last:border-b-0 flex items-center justify-between gap-2 ${getButtonClassName(state, isActive)} ${ diff --git a/app/components/draft/AuthRecoveryOverlay.tsx b/app/components/draft/AuthRecoveryOverlay.tsx index 900939b..58b0a24 100644 --- a/app/components/draft/AuthRecoveryOverlay.tsx +++ b/app/components/draft/AuthRecoveryOverlay.tsx @@ -1,46 +1,18 @@ -import { useEffect, useRef } from "react"; +import { useRef } from "react"; import { Button } from "~/components/ui/button"; import { Card } from "~/components/ui/card"; +import { useFocusTrap } from "~/hooks/useFocusTrap"; interface AuthRecoveryOverlayProps { isAuthDegraded: boolean; } -export function AuthRecoveryOverlay({ - isAuthDegraded, -}: AuthRecoveryOverlayProps) { +export function AuthRecoveryOverlay({ isAuthDegraded }: AuthRecoveryOverlayProps) { const dialogRef = useRef(null); - useEffect(() => { - if (!isAuthDegraded) return; - const el = dialogRef.current; - if (!el) return; - const focusable = el.querySelector( - 'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])' - ); - focusable?.focus(); + useFocusTrap(dialogRef, isAuthDegraded); - function trapFocus(e: KeyboardEvent) { - if (e.key !== "Tab") return; - const focusables = el?.querySelectorAll( - 'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])' - ); - if (!focusables || focusables.length === 0) return; - const first = focusables[0]; - const last = focusables[focusables.length - 1]; - if (e.shiftKey) { - if (document.activeElement === first) { e.preventDefault(); last.focus(); } - } else { - if (document.activeElement === last) { e.preventDefault(); first.focus(); } - } - } - document.addEventListener("keydown", trapFocus); - return () => document.removeEventListener("keydown", trapFocus); - }, [isAuthDegraded]); - - if (!isAuthDegraded) { - return null; - } + if (!isAuthDegraded) return null; return (

Session Expired

- Your session has expired. Reload the page to reconnect to the - draft. + Your session has expired. Reload the page to reconnect to the draft.

-
diff --git a/app/components/draft/AvailableParticipantsSection.tsx b/app/components/draft/AvailableParticipantsSection.tsx index 0c15a03..f08618c 100644 --- a/app/components/draft/AvailableParticipantsSection.tsx +++ b/app/components/draft/AvailableParticipantsSection.tsx @@ -474,10 +474,10 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
-