mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-30 00:09:07 +01:00
add a11y keys
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user