fix(ui): simplify login and suppress pointer focus ring on locale select
This commit is contained in:
@@ -162,6 +162,40 @@ for (const width of [390, 1280]) {
|
||||
});
|
||||
}
|
||||
|
||||
for (const locale of ["en", "it"]) {
|
||||
test(`login keeps only the main title in ${locale}`, async ({ page }, testInfo) => {
|
||||
await fixtures(page);
|
||||
await page.route("**/config.js", route => route.fulfill({ contentType: "application/javascript",
|
||||
body: `window.__THOTHII_CONFIG__ = ${JSON.stringify({ backendBaseUrl: "/api", shell: { mode: "full", defaultLocale: locale } })};` }));
|
||||
await page.route("**/api/auth/config", route => route.fulfill({ json: { mode: "local", localLogin: true, oidcLogin: false } }));
|
||||
await page.route("**/api/me", route => route.fulfill({ status: 401, json: { code: "unauthenticated" } }));
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("heading", { name: locale === "it" ? "Accedi a ThothII" : "Sign in to ThothII" })).toBeVisible();
|
||||
await expect(page.getByText(locale === "it" ? "Accesso a ThothII" : "ThothII access", { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByText(locale === "it" ? "Usa l’account della tua installazione per continuare." : "Use your installation account to continue.", { exact: true })).toHaveCount(0);
|
||||
await page.screenshot({ path: testInfo.outputPath(`login-${locale}.png`), animations: "disabled" });
|
||||
});
|
||||
}
|
||||
|
||||
test("language selector has no pointer ring and retains keyboard focus in both themes", async ({ page }, testInfo) => {
|
||||
await fixtures(page);
|
||||
await page.goto("/");
|
||||
const language = page.getByRole("combobox", { name: "Interface language" });
|
||||
for (const theme of ["light", "dark"]) {
|
||||
if (theme === "dark") await page.getByRole("button", { name: "Use dark theme", exact: true }).click();
|
||||
await language.click();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(language).toBeFocused();
|
||||
await expect(language).toHaveCSS("outline-style", "none");
|
||||
await expect(language).toHaveCSS("box-shadow", "none");
|
||||
await page.screenshot({ path: testInfo.outputPath(`language-pointer-${theme}.png`), animations: "disabled" });
|
||||
await page.keyboard.press("Tab");
|
||||
await page.keyboard.press("Shift+Tab");
|
||||
await expect(language).toBeFocused();
|
||||
await expect(language).toHaveCSS("outline-style", "solid");
|
||||
}
|
||||
});
|
||||
|
||||
for (const route of ["/", "/?thoth_route=administration%2Fdatabase"]) {
|
||||
test(`Core prompt remains readable after loading ${route}`, async ({ page }, testInfo) => {
|
||||
const writes = await fixtures(page);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { LoginPage } from "./LoginPage";
|
||||
import { clearAuthState, setAuthState } from "./authState";
|
||||
import * as authApi from "../api/auth";
|
||||
import { server } from "../test/msw";
|
||||
import { setLocale } from "../i18n";
|
||||
|
||||
const localConfig = { mode: "local", localLogin: true, oidcLogin: false } as const;
|
||||
const oidcConfig = { mode: "oidc", localLogin: false, oidcLogin: true } as const;
|
||||
@@ -26,9 +27,16 @@ const authenticated = {
|
||||
};
|
||||
|
||||
beforeEach(() => clearAuthState());
|
||||
afterEach(() => clearAuthState());
|
||||
afterEach(() => { clearAuthState(); setLocale("en"); });
|
||||
|
||||
describe("LoginPage", () => {
|
||||
test.each(["en", "it"])("keeps only the main sign-in title in %s", locale => {
|
||||
setLocale(locale);
|
||||
render(<LoginPage config={localConfig} onAuthenticated={vi.fn()} />);
|
||||
expect(screen.getByRole("heading", { name: locale === "it" ? "Accedi a ThothII" : "Sign in to ThothII" })).toBeInTheDocument();
|
||||
expect(screen.queryByText(locale === "it" ? "Accesso a ThothII" : "ThothII access")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(locale === "it" ? "Usa l’account della tua installazione per continuare." : "Use your installation account to continue.")).not.toBeInTheDocument();
|
||||
});
|
||||
test("shows an unchecked local Remember me control and clears the password after failure", async () => {
|
||||
let submittedPassword = "";
|
||||
server.use(http.post("/api/auth/local/login", async ({ request }) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useI18n } from "../i18n";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { FormEvent } from "react";
|
||||
import { AlertTriangle, ArrowRight, Eye, EyeOff, LockKeyhole } from "lucide-react";
|
||||
import { AlertTriangle, ArrowRight, Eye, EyeOff } from "lucide-react";
|
||||
import { ApiError } from "../api/client";
|
||||
import { beginOidcLogin, loginLocal } from "../api/auth";
|
||||
import type { AuthenticatedUser, AuthPublicConfig } from "../api/types";
|
||||
@@ -85,12 +85,7 @@ export function LoginPage({ config, onAuthenticated, onRetry }: LoginPageProps)
|
||||
|
||||
<section className="mx-auto w-full max-w-md rounded-2xl border border-border/80 bg-card p-6 shadow-md sm:p-8">
|
||||
<div className="mb-7">
|
||||
<div className="flex items-center gap-2 text-primary" aria-hidden="true">
|
||||
<LockKeyhole className="size-4" />
|
||||
<span className="thot-label text-primary">{t("ThothII access")}</span>
|
||||
</div>
|
||||
<h2 className="mt-3 font-heading text-3xl font-semibold tracking-tight">{t("Sign in to ThothII")}</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">{t("Use your installation account to continue.")}</p>
|
||||
<h2 className="font-heading text-3xl font-semibold tracking-tight">{t("Sign in to ThothII")}</h2>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
|
||||
@@ -267,6 +267,12 @@ body.thot-full-document { margin: 0; }
|
||||
color: oklch(var(--foreground));
|
||||
background: oklch(var(--card));
|
||||
}
|
||||
/* Native selects can match :focus-visible after a pointer click as well.
|
||||
Suppress only that outer ring; keyboard focus retains its visible outline. */
|
||||
.thot-full-header .thot-shell-select[data-pointer-focus="true"]:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.thot-shell-select {
|
||||
min-height: 2rem;
|
||||
max-width: 9rem;
|
||||
|
||||
@@ -12,6 +12,7 @@ export function FullHeader({ onLogout }: { onLogout?: () => Promise<void> }) {
|
||||
const user = useAuthUser();
|
||||
const [fullscreenPending, setFullscreenPending] = useState(false);
|
||||
const [logoutPending, setLogoutPending] = useState(false);
|
||||
const [languagePointerFocus, setLanguagePointerFocus] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
if (shell?.mode !== "full") return null;
|
||||
const fullscreenLabel = shell.fullscreen ? t("Exit fullscreen") : t("Enter fullscreen");
|
||||
@@ -41,7 +42,12 @@ export function FullHeader({ onLogout }: { onLogout?: () => Promise<void> }) {
|
||||
<span className="thot-full-header__brand">Thoth<span>II</span></span>
|
||||
<div className="thot-full-header__controls">
|
||||
<label className="sr-only" htmlFor="thot-ui-language">{t("Interface language")}</label>
|
||||
<select id="thot-ui-language" className="thot-shell-select" value={shell.locale} onChange={event => shell.changeLocale(event.target.value)}>
|
||||
<select id="thot-ui-language" className="thot-shell-select" value={shell.locale}
|
||||
data-pointer-focus={languagePointerFocus || undefined}
|
||||
onPointerDown={() => setLanguagePointerFocus(true)}
|
||||
onKeyDown={event => { if (event.key !== "Escape") setLanguagePointerFocus(false); }}
|
||||
onBlur={() => setLanguagePointerFocus(false)}
|
||||
onChange={event => shell.changeLocale(event.target.value)}>
|
||||
{availableLocales.map(({ code, name }) => <option key={code} value={code} lang={code}>{name}</option>)}
|
||||
</select>
|
||||
<Button variant="ghost" size="icon" aria-label={shell.theme === "light" ? t("Use dark theme") : t("Use light theme")}
|
||||
|
||||
Reference in New Issue
Block a user