From 731e87be72f26ac94b0663b8f366e6af2bf2ba80 Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Thu, 18 Jun 2026 16:16:45 +0200 Subject: [PATCH] switch lang tag on language switch, lint --- client/ui/frontend/src/components/Badge.tsx | 2 +- .../frontend/src/components/DropdownMenu.tsx | 2 +- .../frontend/src/components/inputs/Input.tsx | 37 ++++++++-- .../components/switches/FancyToggleSwitch.tsx | 1 + .../ui/frontend/src/hooks/useFocusVisible.ts | 6 +- client/ui/frontend/src/lib/i18n.ts | 9 +++ .../frontend/src/modules/main/MainHeader.tsx | 5 +- .../ui/frontend/src/modules/main/MainPage.tsx | 4 +- .../modules/profiles/ProfileCreationModal.tsx | 6 +- .../src/modules/profiles/ProfilesTab.tsx | 8 +- .../src/modules/settings/SettingsPage.tsx | 74 ++++++++++--------- 11 files changed, 94 insertions(+), 60 deletions(-) diff --git a/client/ui/frontend/src/components/Badge.tsx b/client/ui/frontend/src/components/Badge.tsx index 5f0df23e4..c5e2b5f22 100644 --- a/client/ui/frontend/src/components/Badge.tsx +++ b/client/ui/frontend/src/components/Badge.tsx @@ -34,7 +34,7 @@ export const Badge = forwardRef(function Badge( )} {...rest} > - {Icon && } + {Icon && } {children} ); diff --git a/client/ui/frontend/src/components/DropdownMenu.tsx b/client/ui/frontend/src/components/DropdownMenu.tsx index b48379576..d43c37e1b 100644 --- a/client/ui/frontend/src/components/DropdownMenu.tsx +++ b/client/ui/frontend/src/components/DropdownMenu.tsx @@ -41,7 +41,7 @@ const DropdownMenuSubTrigger = React.forwardRef< {...props} > {children} - + )); DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; diff --git a/client/ui/frontend/src/components/inputs/Input.tsx b/client/ui/frontend/src/components/inputs/Input.tsx index 349833cc5..2dad80d7a 100644 --- a/client/ui/frontend/src/components/inputs/Input.tsx +++ b/client/ui/frontend/src/components/inputs/Input.tsx @@ -172,7 +172,7 @@ function NumberStepper({ "flex w-9 flex-1 cursor-default items-center justify-center text-nb-gray-300 transition-colors hover:bg-nb-gray-800" } > - + ); } -function FieldMessage({ error, warning }: Readonly<{ error?: string; warning?: string }>) { +function FieldMessage({ + id, + error, + warning, +}: Readonly<{ id?: string; error?: string; warning?: string }>) { if (!error && !warning) return null; return ( (function Input( const isNumber = type === "number"; const reactId = useId(); - const inputId = id ?? (label ? `input-${reactId}` : undefined); + const fallbackId = `input-${reactId}`; + const inputId = id ?? (label ? fallbackId : undefined); + const messageId = error || warning ? `${inputId ?? fallbackId}-message` : undefined; const copyTimer = useRef | null>(null); useEffect( @@ -268,8 +276,13 @@ export const Input = forwardRef(function Input( onClick={() => setShowPassword((s) => !s)} className={"pointer-events-auto transition-all hover:text-white"} aria-label={t("common.togglePasswordVisibility")} + aria-pressed={showPassword} > - {showPassword ? : } + {showPassword ? ( + + ) : ( + + )} ) : null; @@ -293,7 +306,11 @@ export const Input = forwardRef(function Input( className={"pointer-events-auto transition-all hover:text-white"} aria-label={t("common.copy")} > - {copied ? : } + {copied ? ( + + ) : ( + + )} ) : null; @@ -332,6 +349,12 @@ export const Input = forwardRef(function Input( id={inputId} type={inputType} ref={setRefs} + aria-invalid={error ? true : undefined} + aria-describedby={ + messageId + ? [props["aria-describedby"], messageId].filter(Boolean).join(" ") + : props["aria-describedby"] + } {...props} className={inputClassName} /> @@ -343,7 +366,7 @@ export const Input = forwardRef(function Input( )} - + ); }); diff --git a/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx b/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx index ae6462f90..45e3e333a 100644 --- a/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx +++ b/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx @@ -90,6 +90,7 @@ export default function FancyToggleSwitch({ id={switchId} checked={value} onCheckedChange={onChange} + disabled={disabled} dataCy={dataCy} aria-describedby={helpText ? descriptionId : undefined} /> diff --git a/client/ui/frontend/src/hooks/useFocusVisible.ts b/client/ui/frontend/src/hooks/useFocusVisible.ts index 240739529..6061beb9c 100644 --- a/client/ui/frontend/src/hooks/useFocusVisible.ts +++ b/client/ui/frontend/src/hooks/useFocusVisible.ts @@ -24,15 +24,15 @@ const isKeyboardEvent = (e: KeyboardEvent) => { return e.key === "Tab" || e.key === "Escape" || e.key.startsWith("Arrow"); }; -if (typeof window !== "undefined") { - window.addEventListener( +if (globalThis.window !== undefined) { + globalThis.addEventListener( "keydown", (e) => { if (isKeyboardEvent(e)) setModality("keyboard"); }, true, ); - window.addEventListener("pointerdown", () => setModality("pointer"), true); + globalThis.addEventListener("pointerdown", () => setModality("pointer"), true); } export const useFocusVisible = (): boolean => { diff --git a/client/ui/frontend/src/lib/i18n.ts b/client/ui/frontend/src/lib/i18n.ts index 5b36683b9..dc7fc7902 100644 --- a/client/ui/frontend/src/lib/i18n.ts +++ b/client/ui/frontend/src/lib/i18n.ts @@ -77,6 +77,9 @@ export async function initI18n(): Promise { returnNull: false, }); + syncDocumentLang(); + i18next.on("languageChanged", syncDocumentLang); + Events.On("netbird:preferences:changed", (e) => { const next = e.data?.language; if (next && next !== i18next.language) { @@ -87,6 +90,12 @@ export async function initI18n(): Promise { }); } +function syncDocumentLang() { + if (typeof document !== "undefined") { + document.documentElement.lang = i18next.language; + } +} + export async function loadLanguages() { return I18n.Languages(); } diff --git a/client/ui/frontend/src/modules/main/MainHeader.tsx b/client/ui/frontend/src/modules/main/MainHeader.tsx index afe056ec4..becfde47c 100644 --- a/client/ui/frontend/src/modules/main/MainHeader.tsx +++ b/client/ui/frontend/src/modules/main/MainHeader.tsx @@ -77,6 +77,7 @@ export const MainHeader = () => { className={"select-none"} aria-label={t("header.menu.open")} aria-haspopup={"menu"} + aria-expanded={menuOpen} /> { ); return ( -
{
{settingsSlot}
-
+ ); }; diff --git a/client/ui/frontend/src/modules/main/MainPage.tsx b/client/ui/frontend/src/modules/main/MainPage.tsx index c1330220d..c05b3a025 100644 --- a/client/ui/frontend/src/modules/main/MainPage.tsx +++ b/client/ui/frontend/src/modules/main/MainPage.tsx @@ -44,7 +44,7 @@ const MainBody = () => { const isAdvanced = viewMode === "advanced"; return ( -
+
{/* Windows narrower width compensates for the OS frame Wails counts differently than macOS. See https://github.com/wailsapp/wails/issues/3260 */}
{ )} -
+
); }; diff --git a/client/ui/frontend/src/modules/profiles/ProfileCreationModal.tsx b/client/ui/frontend/src/modules/profiles/ProfileCreationModal.tsx index f113f0c38..19313ccb3 100644 --- a/client/ui/frontend/src/modules/profiles/ProfileCreationModal.tsx +++ b/client/ui/frontend/src/modules/profiles/ProfileCreationModal.tsx @@ -71,11 +71,11 @@ export const ProfileCreationModal = ({ open, onOpenChange, onSubmit, initial }: useEffect(() => { if (!open) return; initialModeRef.current = mode; - const id = window.setTimeout(() => { + const id = globalThis.setTimeout(() => { nameRef.current?.focus(); nameRef.current?.select(); }, 0); - return () => window.clearTimeout(id); + return () => globalThis.clearTimeout(id); // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); @@ -108,7 +108,7 @@ export const ProfileCreationModal = ({ open, onOpenChange, onSubmit, initial }: } const target = normalizeManagementUrl(trimmed); - const unchanged = initial && target === initial.managementUrl; + const unchanged = target === initial?.managementUrl; return { url: target, needsReachCheck: !unchanged }; }; diff --git a/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx b/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx index 4049f0610..a602e94bf 100644 --- a/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx +++ b/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx @@ -504,11 +504,9 @@ const RowActions = ({ }: RowActionsProps) => { const { t } = useTranslation(); const deleteDisabled = isDefault || isActive; - const deleteDisabledReason = isDefault - ? t("profile.delete.disabledDefault") - : isActive - ? t("profile.delete.disabledActive") - : null; + let deleteDisabledReason: string | null = null; + if (isDefault) deleteDisabledReason = t("profile.delete.disabledDefault"); + else if (isActive) deleteDisabledReason = t("profile.delete.disabledActive"); return (
{ ) : (
)} - - - - - - - -
- {visibleTabs.map((tab) => ( - - {TAB_CONTENT[tab]} - - ))} -
-
- + + + + + + - - -
-
-
-
-
+ +
+ {visibleTabs.map((tab) => ( + + {TAB_CONTENT[tab]} + + ))} +
+
+ + + + + + + + + ); };