add a11y keys

This commit is contained in:
Eduard Gert
2026-06-18 13:56:41 +02:00
parent f6e50995b6
commit 9cdc6e3013
51 changed files with 761 additions and 153 deletions
@@ -1,4 +1,4 @@
import { FormEvent, useEffect, useRef, useState } from "react";
import { FormEvent, useEffect, useId, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import * as Dialog from "@/components/dialog/Dialog";
import { Input } from "@/components/inputs/Input";
@@ -33,6 +33,8 @@ export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) =>
const { t } = useTranslation();
const { mdm } = useRestrictions();
const managedManagementUrl = mdm.managementURL;
const nameId = useId();
const urlId = useId();
const [name, setName] = useState("");
const [nameError, setNameError] = useState<string | null>(null);
const nameRef = useRef<HTMLInputElement>(null);
@@ -126,13 +128,15 @@ export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) =>
maxWidthClass="max-w-md"
showClose={false}
className="py-7"
srTitle={t("profile.dialog.title")}
srDescription={t("profile.dialog.description")}
onOpenAutoFocus={(e) => e.preventDefault()}
>
<form onSubmit={handleSubmit}>
<div className="flex flex-col gap-6 px-7">
<div className="flex flex-col gap-2">
<div className={"pl-1"}>
<Label as={"div"} className={"mb-0.5"}>
<Label htmlFor={nameId} className={"mb-0.5"}>
{t("profile.dialog.nameLabel")}
</Label>
<HelpText margin={false}>
@@ -140,6 +144,7 @@ export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) =>
</HelpText>
</div>
<Input
id={nameId}
ref={nameRef}
autoFocus
placeholder={t("profile.dialog.placeholder")}
@@ -171,8 +176,10 @@ export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) =>
/>
{mode === ManagementMode.SelfHosted && (
<Input
id={urlId}
ref={urlRef}
autoFocus
aria-label={t("settings.general.management.label")}
placeholder={t(
"settings.general.management.urlPlaceholder",
)}
@@ -124,7 +124,7 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
"data-[disabled=true]:opacity-50 data-[disabled=true]:pointer-events-none",
)}
>
<Settings2 size={14} className="shrink-0" />
<Settings2 size={14} aria-hidden="true" className="shrink-0" />
<span className="truncate flex-1">
{t("profile.dropdown.manageProfiles")}
</span>
@@ -138,9 +138,17 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
};
const ProfileTriggerSkeleton = () => (
<div className="h-10 flex items-center gap-2 px-3 rounded-lg select-none wails-no-draggable">
<div className="size-4 rounded-full bg-nb-gray-900 animate-pulse shrink-0" />
<div className="h-4 w-24 rounded bg-nb-gray-900 animate-pulse" />
<div
role="status"
aria-busy="true"
aria-live="polite"
className="h-10 flex items-center gap-2 px-3 rounded-lg select-none wails-no-draggable"
>
<div
aria-hidden="true"
className="size-4 rounded-full bg-nb-gray-900 animate-pulse shrink-0"
/>
<div aria-hidden="true" className="h-4 w-24 rounded bg-nb-gray-900 animate-pulse" />
</div>
);
@@ -150,11 +158,14 @@ type ProfileTriggerButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
const ProfileTriggerButton = forwardRef<HTMLButtonElement, ProfileTriggerButtonProps>(
function ProfileTriggerButton({ name, className, ...props }, ref) {
const { t } = useTranslation();
const Icon = pickProfileIcon(name) ?? UserCircle;
return (
<button
ref={ref}
type="button"
aria-label={t("header.profile.switch")}
aria-haspopup="listbox"
className={cn(
"h-10 flex items-center gap-2 px-3 rounded-lg outline-none cursor-default select-none wails-no-draggable",
"text-nb-gray-200 hover:bg-nb-gray-900",
@@ -165,11 +176,19 @@ const ProfileTriggerButton = forwardRef<HTMLButtonElement, ProfileTriggerButtonP
)}
{...props}
>
<Icon size={16} className={"text-nb-gray-200 shrink-0 wails-no-draggable"} />
<Icon
size={16}
aria-hidden="true"
className={"text-nb-gray-200 shrink-0 wails-no-draggable"}
/>
<span className={"text-sm font-medium truncate max-w-[140px] wails-no-draggable"}>
{name}
</span>
<ChevronDown size={14} className={"text-nb-gray-200 shrink-0 wails-no-draggable"} />
<ChevronDown
size={14}
aria-hidden="true"
className={"text-nb-gray-200 shrink-0 wails-no-draggable"}
/>
</button>
);
},
@@ -199,7 +218,11 @@ const ProfileRow = ({ profile, isActive, onSelect }: ProfileRowProps) => {
{showEmail && <TruncatedEmail email={profile.email} />}
</div>
{isActive && (
<Check size={16} className={cn("shrink-0 text-netbird", showEmail && "mt-0.5")} />
<Check
size={16}
aria-hidden="true"
className={cn("shrink-0 text-netbird", showEmail && "mt-0.5")}
/>
)}
</Command.Item>
);
@@ -128,7 +128,10 @@ export function ProfilesTab() {
"bg-nb-gray-930/60 border border-nb-gray-900 rounded-xl overflow-hidden",
)}
>
<table className={"w-full text-sm"}>
<table
className={"w-full text-sm"}
aria-label={t("settings.profiles.section.profiles")}
>
<tbody>
{ordered.map((profile) => (
<ProfileRow
@@ -149,7 +152,11 @@ export function ProfilesTab() {
"flex flex-col items-center justify-center py-10 text-center"
}
>
<UserCircle size={28} className={"text-nb-gray-500 mb-2"} />
<UserCircle
size={28}
aria-hidden="true"
className={"text-nb-gray-500 mb-2"}
/>
<p className={"text-sm font-semibold text-nb-gray-200"}>
{t("settings.profiles.emptyTitle")}
</p>
@@ -162,7 +169,7 @@ export function ProfilesTab() {
<SettingsBottomBar>
<Button variant={"primary"} size={"md"} onClick={() => setNewOpen(true)}>
<PlusCircle size={14} />
<PlusCircle size={14} aria-hidden="true" />
{t("settings.profiles.addProfile")}
</Button>
</SettingsBottomBar>
@@ -201,6 +208,7 @@ const ProfileRow = ({ profile, isActive, onSwitch, onDeregister, onDelete }: Pro
>
<Icon
size={15}
aria-hidden="true"
className={cn(
"text-nb-gray-200 shrink-0",
@@ -346,7 +354,7 @@ const ActionIconButton = ({
"opacity-40 cursor-not-allowed hover:!text-nb-gray-400 hover:!bg-transparent",
)}
>
<Icon size={16} />
<Icon size={16} aria-hidden="true" />
</button>
);
if (hidden) return button;