From d0b7607c5a28b2b5926d31a3a3bae43e09dde347 Mon Sep 17 00:00:00 2001 From: Tutitoos Date: Fri, 11 Sep 2026 19:00:12 +0200 Subject: [PATCH] fix(web): use auth locale dropdown --- apps/web/src/auth-gate.tsx | 82 +++++++++++++++++++++++++++----- apps/web/tests/bootstrap.spec.ts | 9 +++- 2 files changed, 78 insertions(+), 13 deletions(-) diff --git a/apps/web/src/auth-gate.tsx b/apps/web/src/auth-gate.tsx index 9edacd2..69f41c0 100644 --- a/apps/web/src/auth-gate.tsx +++ b/apps/web/src/auth-gate.tsx @@ -1,5 +1,5 @@ -import { Check, KeyRound, Languages, LoaderCircle, LockKeyhole } from "lucide-react"; -import { type FormEvent, type ReactNode, useCallback, useEffect, useState } from "react"; +import { Check, ChevronDown, KeyRound, Languages, LoaderCircle, LockKeyhole } from "lucide-react"; +import { type FormEvent, type ReactNode, useCallback, useEffect, useRef, useState } from "react"; import { AuthRequestError, createOwner, @@ -18,11 +18,72 @@ import { Brand } from "./pages"; type Phase = "loading" | "setup" | "sign-in" | "recovery" | "app" | "error"; type Translator = (key: TranslationKey) => string; -function LanguageButton({ locale, onChange }: { locale: Locale; onChange: () => void }) { +function LanguageButton({ + locale, + onChange, +}: { + locale: Locale; + onChange: (locale: Locale) => void; +}) { + const [open, setOpen] = useState(false); + const menu = useRef(null); + const t: Translator = (key) => translate(locale, key); + + useEffect(() => { + if (!open) return; + const close = (event: PointerEvent) => { + if (!menu.current?.contains(event.target as Node)) setOpen(false); + }; + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") setOpen(false); + }; + window.addEventListener("pointerdown", close); + window.addEventListener("keydown", handleEscape); + return () => { + window.removeEventListener("pointerdown", close); + window.removeEventListener("keydown", handleEscape); + }; + }, [open]); + return ( - +
+ + {open && ( +
+ {(["en", "es"] as const).map((option) => ( + + ))} +
+ )} +
); } @@ -32,12 +93,12 @@ function AuthFrame({ children, }: { locale: Locale; - onLocaleChange: () => void; + onLocaleChange: (locale: Locale) => void; children: ReactNode; }) { return (
-
+
@@ -380,10 +441,7 @@ export function AuthGate({ renderApp }: { renderApp: (locale: Locale) => ReactNo if (phase === "app") return renderApp(locale); return ( - setLocale((value) => (value === "en" ? "es" : "en"))} - > + {phase === "loading" && (
diff --git a/apps/web/tests/bootstrap.spec.ts b/apps/web/tests/bootstrap.spec.ts index 85ee054..8027926 100644 --- a/apps/web/tests/bootstrap.spec.ts +++ b/apps/web/tests/bootstrap.spec.ts @@ -28,7 +28,14 @@ test("creates the only owner in Spanish without persisting secrets", async ({ pa (violation) => violation.impact === "critical" || violation.impact === "serious", ), ).toEqual([]); - await page.getByRole("button", { name: "EN" }).click(); + await page.getByRole("button", { name: "Language" }).click(); + const languageMenu = page.getByRole("menu", { name: "Language" }); + await expect(languageMenu).toBeVisible(); + await expect(page.getByRole("menuitemradio", { name: "EN English" })).toHaveAttribute( + "aria-checked", + "true", + ); + await page.getByRole("menuitemradio", { name: "ES Spanish" }).click(); await expect( page.getByRole("heading", { name: "Crea el propietario de Mailflow" }), ).toBeVisible();