keyboard radiogroup, modal focus trap, nav session hydration
All checks were successful
Build and Push / build (push) Successful in 1m53s

This commit is contained in:
2026-05-29 22:46:03 +02:00
parent bff3aad1ca
commit 7fac3c3aa8
4 changed files with 144 additions and 48 deletions

View File

@@ -3,6 +3,7 @@ import { NextIntlClientProvider } from "next-intl";
import { getMessages, getTranslations } from "next-intl/server";
import { routing } from "@/i18n/routing";
import { notFound } from "next/navigation";
import { auth } from "@/lib/auth";
import { NavShell } from "@/components/layout/nav-shell";
export function generateStaticParams() {
@@ -44,12 +45,13 @@ export default async function LocaleLayout({
}
const messages = await getMessages();
const session = await auth();
return (
<html lang={locale} className="dark">
<body className="min-h-screen bg-surface-0 text-text-primary antialiased">
<NextIntlClientProvider messages={messages}>
<NavShell>{children}</NavShell>
<NavShell session={session}>{children}</NavShell>
</NextIntlClientProvider>
</body>
</html>

View File

@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useRef, forwardRef } from "react";
import { useTranslations } from "next-intl";
import { useRouter, Link } from "@/i18n/navigation";
import { Card } from "@/components/ui/card";
@@ -50,6 +50,30 @@ export default function RegisterPage() {
const [state, setState] = useState<FormState>("idle");
const [error, setError] = useState("");
// Radiogroup keyboard support. `role="radio"` requires roving
// tabindex (one tab stop) + arrow-key navigation between options —
// native buttons don't move focus on arrows. The selected card is
// the tab stop; when nothing is selected yet the first card is
// focusable so keyboard users can enter the group.
const TYPES: AccountType[] = ["personal", "company"];
const cardRefs = useRef<(HTMLButtonElement | null)[]>([]);
const rovingTabIndex = (type: AccountType, index: number) =>
accountType === type || (accountType === null && index === 0) ? 0 : -1;
const handleCardKeyDown = (e: React.KeyboardEvent, index: number) => {
let next: number | null = null;
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
next = (index + 1) % TYPES.length;
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
next = (index - 1 + TYPES.length) % TYPES.length;
}
if (next === null) return;
e.preventDefault();
setAccountType(TYPES[next]);
cardRefs.current[next]?.focus();
};
const isPersonal = accountType === "personal";
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -146,8 +170,13 @@ export default function RegisterPage() {
className="grid grid-cols-2 gap-3 mb-6 animate-in animate-in-delay-1"
>
<AccountTypeCard
ref={(el) => {
cardRefs.current[0] = el;
}}
selected={accountType === "personal"}
onClick={() => setAccountType("personal")}
tabIndex={rovingTabIndex("personal", 0)}
onKeyDown={(e) => handleCardKeyDown(e, 0)}
label={t("personalCardTitle")}
description={t("personalCardDescription")}
icon={
@@ -168,8 +197,13 @@ export default function RegisterPage() {
}
/>
<AccountTypeCard
ref={(el) => {
cardRefs.current[1] = el;
}}
selected={accountType === "company"}
onClick={() => setAccountType("company")}
tabIndex={rovingTabIndex("company", 1)}
onKeyDown={(e) => handleCardKeyDown(e, 1)}
label={t("companyCardTitle")}
description={t("companyCardDescription")}
icon={
@@ -305,41 +339,42 @@ export default function RegisterPage() {
* and text colours intensify when selected to give a clear "this one
* is on" signal beyond just the border colour.
*/
function AccountTypeCard({
selected,
onClick,
label,
description,
icon,
}: {
selected: boolean;
onClick: () => void;
label: string;
description: string;
icon: React.ReactNode;
}) {
const AccountTypeCard = forwardRef<
HTMLButtonElement,
{
selected: boolean;
onClick: () => void;
label: string;
description: string;
icon: React.ReactNode;
tabIndex: number;
onKeyDown: (e: React.KeyboardEvent) => void;
}
>(function AccountTypeCard(
{ selected, onClick, label, description, icon, tabIndex, onKeyDown },
ref
) {
return (
<button
ref={ref}
type="button"
role="radio"
aria-checked={selected}
tabIndex={tabIndex}
onClick={onClick}
onKeyDown={onKeyDown}
className={`text-left rounded-xl border p-4 transition-colors cursor-pointer focus:outline-none focus:ring-2 focus:ring-accent/40 ${
selected
? "border-accent bg-accent/10"
: "border-border bg-surface-2 hover:border-accent/40 hover:bg-surface-3/30"
}`}
>
<div
className={`mb-2 ${
selected ? "text-accent" : "text-text-muted"
}`}
>
<div className={`mb-2 ${selected ? "text-accent" : "text-text-muted"}`}>
{icon}
</div>
<div
className={`text-sm font-semibold mb-0.5 ${
selected ? "text-text-primary" : "text-text-primary"
selected ? "text-text-primary" : "text-text-secondary"
}`}
>
{label}
@@ -347,4 +382,4 @@ function AccountTypeCard({
<div className="text-xs text-text-muted leading-snug">{description}</div>
</button>
);
}
});