mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-12 00:19:08 +02:00
make settings accessible
Conflicts: client/ui/frontend/src/modules/profiles/ProfilesTab.tsx
This commit is contained in:
@@ -49,6 +49,14 @@ export function LanguagePicker() {
|
|||||||
[languages, i18n.language],
|
[languages, i18n.language],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleTriggerKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||||
|
if (open) return;
|
||||||
|
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const select = async (code: string) => {
|
const select = async (code: string) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
if (busy || code === i18n.language) return;
|
if (busy || code === i18n.language) return;
|
||||||
@@ -76,7 +84,9 @@ export function LanguagePicker() {
|
|||||||
<Popover.Trigger asChild>
|
<Popover.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type={"button"}
|
type={"button"}
|
||||||
|
tabIndex={0}
|
||||||
disabled={busy || languages.length === 0}
|
disabled={busy || languages.length === 0}
|
||||||
|
onKeyDown={handleTriggerKeyDown}
|
||||||
aria-label={t("settings.general.language.label")}
|
aria-label={t("settings.general.language.label")}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
@@ -86,6 +96,7 @@ export function LanguagePicker() {
|
|||||||
"border-neutral-200 dark:border-nb-gray-700",
|
"border-neutral-200 dark:border-nb-gray-700",
|
||||||
"text-xs font-semibold text-nb-gray-100 cursor-default outline-none",
|
"text-xs font-semibold text-nb-gray-100 cursor-default outline-none",
|
||||||
"hover:border-nb-gray-600 data-[state=open]:border-nb-gray-600",
|
"hover:border-nb-gray-600 data-[state=open]:border-nb-gray-600",
|
||||||
|
"focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940",
|
||||||
"disabled:opacity-50",
|
"disabled:opacity-50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -109,7 +120,6 @@ export function LanguagePicker() {
|
|||||||
<Popover.Content
|
<Popover.Content
|
||||||
align={"start"}
|
align={"start"}
|
||||||
sideOffset={6}
|
sideOffset={6}
|
||||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-[var(--radix-popover-trigger-width)]",
|
"w-[var(--radix-popover-trigger-width)]",
|
||||||
"rounded-lg border border-nb-gray-850 bg-nb-gray-920 shadow-lg p-1 z-50",
|
"rounded-lg border border-nb-gray-850 bg-nb-gray-920 shadow-lg p-1 z-50",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const Trigger = forwardRef<HTMLButtonElement, TriggerProps>(function VerticalTab
|
|||||||
"transition-colors duration-150",
|
"transition-colors duration-150",
|
||||||
"data-[state=active]:bg-nb-gray-930",
|
"data-[state=active]:bg-nb-gray-930",
|
||||||
"data-[state=inactive]:hover:bg-nb-gray-935",
|
"data-[state=inactive]:hover:bg-nb-gray-935",
|
||||||
|
"focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -81,7 +82,14 @@ const Content = forwardRef<HTMLDivElement, Tabs.TabsContentProps>(function Verti
|
|||||||
{ className, ...props },
|
{ className, ...props },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
return <Tabs.Content ref={ref} className={cn("outline-none", className)} {...props} />;
|
return (
|
||||||
|
<Tabs.Content
|
||||||
|
ref={ref}
|
||||||
|
tabIndex={-1}
|
||||||
|
className={cn("outline-none", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const VerticalTabs = Object.assign(Root, { List, Trigger, Content });
|
export const VerticalTabs = Object.assign(Root, { List, Trigger, Content });
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type={type}
|
type={type}
|
||||||
|
tabIndex={0}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
aria-busy={loading || undefined}
|
aria-busy={loading || undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export default function FancyToggleSwitch({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
{...(disabled ? { inert: "" } : {})}
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-default transition-all duration-300 relative z-[1]",
|
"cursor-default transition-all duration-300 relative z-[1]",
|
||||||
"inline-block text-left w-full",
|
"inline-block text-left w-full",
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ const NetworksList = ({ data, onToggle, scrollParent }: NetworksListProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRowKeyDown = (e: KeyboardEvent<HTMLButtonElement>, index: number) => {
|
const handleRowKeyDown = (e: KeyboardEvent<HTMLDivElement>, index: number) => {
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -297,6 +297,7 @@ const NetworksList = ({ data, onToggle, scrollParent }: NetworksListProps) => {
|
|||||||
itemContent={(index, n) => (
|
itemContent={(index, n) => (
|
||||||
<div
|
<div
|
||||||
role="listitem"
|
role="listitem"
|
||||||
|
onKeyDown={(e) => handleRowKeyDown(e, index)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group relative flex items-start gap-2.5 pl-6 pr-9 py-3 min-w-0",
|
"group relative flex items-start gap-2.5 pl-6 pr-9 py-3 min-w-0",
|
||||||
"hover:bg-nb-gray-900/40 transition-colors",
|
"hover:bg-nb-gray-900/40 transition-colors",
|
||||||
@@ -313,7 +314,6 @@ const NetworksList = ({ data, onToggle, scrollParent }: NetworksListProps) => {
|
|||||||
aria-label={t("networks.row.toggle", { name: n.id })}
|
aria-label={t("networks.row.toggle", { name: n.id })}
|
||||||
aria-pressed={n.selected}
|
aria-pressed={n.selected}
|
||||||
onClick={() => onToggle(n.id, n.selected)}
|
onClick={() => onToggle(n.id, n.selected)}
|
||||||
onKeyDown={(e) => handleRowKeyDown(e, index)}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-0 cursor-pointer outline-none",
|
"absolute inset-0 cursor-pointer outline-none",
|
||||||
"focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/60",
|
"focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/60",
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ const PeersList = ({ data, scrollParent }: PeersListProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRowKeyDown = (e: KeyboardEvent<HTMLButtonElement>, index: number) => {
|
const handleRowKeyDown = (e: KeyboardEvent<HTMLDivElement>, index: number) => {
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -231,6 +231,7 @@ const PeersList = ({ data, scrollParent }: PeersListProps) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="listitem"
|
role="listitem"
|
||||||
|
onKeyDown={(e) => handleRowKeyDown(e, index)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group relative flex items-start gap-2.5 pl-6 pr-4 py-3 min-w-0",
|
"group relative flex items-start gap-2.5 pl-6 pr-4 py-3 min-w-0",
|
||||||
"hover:bg-nb-gray-900/40 transition-colors",
|
"hover:bg-nb-gray-900/40 transition-colors",
|
||||||
@@ -249,7 +250,6 @@ const PeersList = ({ data, scrollParent }: PeersListProps) => {
|
|||||||
status: statusLabel,
|
status: statusLabel,
|
||||||
})}
|
})}
|
||||||
onClick={() => setSelected(peer)}
|
onClick={() => setSelected(peer)}
|
||||||
onKeyDown={(e) => handleRowKeyDown(e, index)}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-0 cursor-default outline-none",
|
"absolute inset-0 cursor-default outline-none",
|
||||||
"focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/60",
|
"focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/60",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { KeyboardEvent, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { CircleMinus, LogIn, PlusCircle, Trash2, UserCircle } from "lucide-react";
|
import { CircleMinus, LogIn, PlusCircle, Trash2, UserCircle } from "lucide-react";
|
||||||
import type { Profile } from "@bindings/services/models.js";
|
import type { Profile } from "@bindings/services/models.js";
|
||||||
@@ -128,23 +128,13 @@ export function ProfilesTab() {
|
|||||||
"bg-nb-gray-930/60 border border-nb-gray-900 rounded-xl overflow-hidden",
|
"bg-nb-gray-930/60 border border-nb-gray-900 rounded-xl overflow-hidden",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<table
|
<ProfilesTable
|
||||||
className={"w-full text-sm"}
|
ordered={ordered}
|
||||||
aria-label={t("settings.profiles.section.profiles")}
|
activeProfileId={activeProfileId}
|
||||||
>
|
onSwitch={handleSwitch}
|
||||||
<tbody>
|
onDeregister={handleDeregister}
|
||||||
{ordered.map((profile) => (
|
onDelete={handleDelete}
|
||||||
<ProfileRow
|
/>
|
||||||
key={profile.id}
|
|
||||||
profile={profile}
|
|
||||||
isActive={profile.id === activeProfileId}
|
|
||||||
onSwitch={() => handleSwitch(profile.id, profile.name)}
|
|
||||||
onDeregister={() => handleDeregister(profile.id, profile.name)}
|
|
||||||
onDelete={() => handleDelete(profile.id, profile.name)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{loaded && ordered.length === 0 && (
|
{loaded && ordered.length === 0 && (
|
||||||
<div
|
<div
|
||||||
@@ -184,64 +174,169 @@ export function ProfilesTab() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProfilesTableProps = {
|
||||||
|
ordered: Profile[];
|
||||||
|
activeProfileId: string | undefined;
|
||||||
|
onSwitch: (id: string, name: string) => void;
|
||||||
|
onDeregister: (id: string, name: string) => void;
|
||||||
|
onDelete: (id: string, name: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProfilesTable = ({
|
||||||
|
ordered,
|
||||||
|
activeProfileId,
|
||||||
|
onSwitch,
|
||||||
|
onDeregister,
|
||||||
|
onDelete,
|
||||||
|
}: ProfilesTableProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [focusedIndex, setFocusedIndex] = useState(0);
|
||||||
|
const rowRefs = useRef<Map<string, HTMLLIElement>>(new Map());
|
||||||
|
|
||||||
|
const focusRow = (index: number) => {
|
||||||
|
if (index < 0 || index >= ordered.length) return;
|
||||||
|
setFocusedIndex(index);
|
||||||
|
const el = rowRefs.current.get(ordered[index].id);
|
||||||
|
el?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowKeyDown = (e: KeyboardEvent<HTMLLIElement>, index: number) => {
|
||||||
|
switch (e.key) {
|
||||||
|
case "ArrowDown":
|
||||||
|
e.preventDefault();
|
||||||
|
focusRow(Math.min(index + 1, ordered.length - 1));
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
e.preventDefault();
|
||||||
|
focusRow(Math.max(index - 1, 0));
|
||||||
|
break;
|
||||||
|
case "Home":
|
||||||
|
e.preventDefault();
|
||||||
|
focusRow(0);
|
||||||
|
break;
|
||||||
|
case "End":
|
||||||
|
e.preventDefault();
|
||||||
|
focusRow(ordered.length - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeFocusedIndex = Math.min(focusedIndex, Math.max(0, ordered.length - 1));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
role={"list"}
|
||||||
|
className={"w-full text-sm flex flex-col"}
|
||||||
|
aria-label={t("settings.profiles.section.profiles")}
|
||||||
|
>
|
||||||
|
{ordered.map((profile, index) => (
|
||||||
|
<ProfileRow
|
||||||
|
key={profile.id}
|
||||||
|
profile={profile}
|
||||||
|
isActive={profile.id === activeProfileId}
|
||||||
|
isFocused={index === safeFocusedIndex}
|
||||||
|
isFirst={index === 0}
|
||||||
|
isLast={index === ordered.length - 1}
|
||||||
|
rowRef={(el) => {
|
||||||
|
if (el) rowRefs.current.set(profile.id, el);
|
||||||
|
else rowRefs.current.delete(profile.id);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => handleRowKeyDown(e, index)}
|
||||||
|
onFocus={() => setFocusedIndex(index)}
|
||||||
|
onSwitch={() => onSwitch(profile.id, profile.name)}
|
||||||
|
onDeregister={() => onDeregister(profile.id, profile.name)}
|
||||||
|
onDelete={() => onDelete(profile.id, profile.name)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
type ProfileRowProps = {
|
type ProfileRowProps = {
|
||||||
profile: Profile;
|
profile: Profile;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
isFocused: boolean;
|
||||||
|
isFirst: boolean;
|
||||||
|
isLast: boolean;
|
||||||
|
rowRef: (el: HTMLLIElement | null) => void;
|
||||||
|
onKeyDown: (e: KeyboardEvent<HTMLLIElement>) => void;
|
||||||
|
onFocus: () => void;
|
||||||
onSwitch: () => void;
|
onSwitch: () => void;
|
||||||
onDeregister: () => void;
|
onDeregister: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProfileRow = ({ profile, isActive, onSwitch, onDeregister, onDelete }: ProfileRowProps) => {
|
const ProfileRow = ({
|
||||||
|
profile,
|
||||||
|
isActive,
|
||||||
|
isFocused,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
rowRef,
|
||||||
|
onKeyDown,
|
||||||
|
onFocus,
|
||||||
|
onSwitch,
|
||||||
|
onDeregister,
|
||||||
|
onDelete,
|
||||||
|
}: ProfileRowProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const Icon = pickProfileIcon(profile.name) ?? UserCircle;
|
const Icon = pickProfileIcon(profile.name) ?? UserCircle;
|
||||||
const showEmail = !!profile.email;
|
const showEmail = !!profile.email;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className={"border-b border-nb-gray-910 last:border-b-0"}>
|
<li
|
||||||
<td className={"px-4 py-2.5 align-middle"}>
|
ref={rowRef}
|
||||||
<div
|
tabIndex={isFocused ? 0 : -1}
|
||||||
className={cn(
|
onKeyDown={onKeyDown}
|
||||||
"flex gap-2 min-w-0 leading-tight",
|
onFocus={onFocus}
|
||||||
showEmail ? "items-start" : "items-center",
|
aria-label={profile.name}
|
||||||
)}
|
className={cn(
|
||||||
>
|
"flex items-center gap-4 px-4 py-2.5",
|
||||||
<Icon
|
"border-b border-nb-gray-910 last:border-b-0",
|
||||||
size={15}
|
"outline-none",
|
||||||
aria-hidden="true"
|
isFirst && "rounded-t-xl",
|
||||||
className={cn(
|
isLast && "rounded-b-xl",
|
||||||
"text-nb-gray-200 shrink-0",
|
"focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/60",
|
||||||
|
)}
|
||||||
showEmail ? "mt-0.5" : "",
|
>
|
||||||
)}
|
<div
|
||||||
/>
|
className={cn(
|
||||||
<div className={"flex flex-col min-w-0 flex-1 leading-tight"}>
|
"flex gap-2 min-w-0 leading-tight flex-1",
|
||||||
<div className={"flex items-center gap-2 min-w-0"}>
|
showEmail ? "items-start" : "items-center",
|
||||||
<span
|
)}
|
||||||
className={
|
>
|
||||||
"truncate font-medium text-nb-gray-100 select-text cursor-text"
|
<Icon
|
||||||
}
|
size={15}
|
||||||
>
|
aria-hidden="true"
|
||||||
{profile.name}
|
className={cn("text-nb-gray-200 shrink-0", showEmail ? "mt-0.5" : "")}
|
||||||
</span>
|
/>
|
||||||
{isActive && <Badge>{t("settings.profiles.active")}</Badge>}
|
<div className={"flex flex-col min-w-0 flex-1 leading-tight"}>
|
||||||
</div>
|
<div className={"flex items-center gap-2 min-w-0"}>
|
||||||
{showEmail && <TruncatedEmail email={profile.email} />}
|
<span
|
||||||
|
className={
|
||||||
|
"truncate font-medium text-nb-gray-100 select-text cursor-text"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{profile.name}
|
||||||
|
</span>
|
||||||
|
{isActive && <Badge>{t("settings.profiles.active")}</Badge>}
|
||||||
</div>
|
</div>
|
||||||
|
{showEmail && <TruncatedEmail email={profile.email} />}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
<td className={"px-4 py-2.5 text-right align-middle"}>
|
<div className={"shrink-0 text-right"}>
|
||||||
<RowActions
|
<RowActions
|
||||||
canSwitch={!isActive}
|
canSwitch={!isActive}
|
||||||
canDeregister={!!profile.email}
|
canDeregister={!!profile.email}
|
||||||
isDefault={profile.name === DEFAULT_PROFILE}
|
isDefault={profile.name === DEFAULT_PROFILE}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
|
rowFocused={isFocused}
|
||||||
onSwitch={onSwitch}
|
onSwitch={onSwitch}
|
||||||
onDeregister={onDeregister}
|
onDeregister={onDeregister}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -272,6 +367,7 @@ type RowActionsProps = {
|
|||||||
canDeregister: boolean;
|
canDeregister: boolean;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
rowFocused: boolean;
|
||||||
onSwitch: () => void;
|
onSwitch: () => void;
|
||||||
onDeregister: () => void;
|
onDeregister: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
@@ -282,6 +378,7 @@ const RowActions = ({
|
|||||||
canDeregister,
|
canDeregister,
|
||||||
isDefault,
|
isDefault,
|
||||||
isActive,
|
isActive,
|
||||||
|
rowFocused,
|
||||||
onSwitch,
|
onSwitch,
|
||||||
onDeregister,
|
onDeregister,
|
||||||
onDelete,
|
onDelete,
|
||||||
@@ -299,6 +396,7 @@ const RowActions = ({
|
|||||||
icon={CircleMinus}
|
icon={CircleMinus}
|
||||||
onClick={onDeregister}
|
onClick={onDeregister}
|
||||||
hidden={!canDeregister}
|
hidden={!canDeregister}
|
||||||
|
tabbable={rowFocused}
|
||||||
/>
|
/>
|
||||||
<ActionIconButton
|
<ActionIconButton
|
||||||
label={deleteLabel}
|
label={deleteLabel}
|
||||||
@@ -306,12 +404,14 @@ const RowActions = ({
|
|||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
variant={"danger"}
|
variant={"danger"}
|
||||||
disabled={deleteDisabled}
|
disabled={deleteDisabled}
|
||||||
|
tabbable={rowFocused}
|
||||||
/>
|
/>
|
||||||
<ActionIconButton
|
<ActionIconButton
|
||||||
label={t("profile.selector.switchTo")}
|
label={t("profile.selector.switchTo")}
|
||||||
icon={LogIn}
|
icon={LogIn}
|
||||||
onClick={onSwitch}
|
onClick={onSwitch}
|
||||||
hidden={!canSwitch}
|
hidden={!canSwitch}
|
||||||
|
tabbable={rowFocused}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -325,6 +425,7 @@ type ActionIconButtonProps = {
|
|||||||
/** Occupies space but invisible and non-interactive (preserves row layout). */
|
/** Occupies space but invisible and non-interactive (preserves row layout). */
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
tabbable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ActionIconButton = ({
|
const ActionIconButton = ({
|
||||||
@@ -334,6 +435,7 @@ const ActionIconButton = ({
|
|||||||
variant = "default",
|
variant = "default",
|
||||||
hidden = false,
|
hidden = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
tabbable = true,
|
||||||
}: ActionIconButtonProps) => {
|
}: ActionIconButtonProps) => {
|
||||||
const button = (
|
const button = (
|
||||||
<button
|
<button
|
||||||
@@ -342,10 +444,11 @@ const ActionIconButton = ({
|
|||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-hidden={hidden || undefined}
|
aria-hidden={hidden || undefined}
|
||||||
aria-disabled={disabled || undefined}
|
aria-disabled={disabled || undefined}
|
||||||
tabIndex={hidden ? -1 : undefined}
|
tabIndex={hidden || !tabbable ? -1 : 0}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-9 w-9 inline-flex items-center justify-center rounded-md cursor-default outline-none",
|
"h-9 w-9 inline-flex items-center justify-center rounded-md cursor-default outline-none",
|
||||||
"transition-colors duration-150",
|
"transition-colors duration-150",
|
||||||
|
"focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940",
|
||||||
variant === "danger"
|
variant === "danger"
|
||||||
? "text-nb-gray-400 hover:text-red-500 hover:bg-red-500/10"
|
? "text-nb-gray-400 hover:text-red-500 hover:bg-red-500/10"
|
||||||
: "text-nb-gray-400 hover:text-nb-gray-100 hover:bg-nb-gray-900",
|
: "text-nb-gray-400 hover:text-nb-gray-100 hover:bg-nb-gray-900",
|
||||||
|
|||||||
@@ -133,9 +133,10 @@ export function SettingsAbout() {
|
|||||||
<button
|
<button
|
||||||
key={url}
|
key={url}
|
||||||
type={"button"}
|
type={"button"}
|
||||||
|
tabIndex={0}
|
||||||
onClick={() => openUrl(url)}
|
onClick={() => openUrl(url)}
|
||||||
className={
|
className={
|
||||||
"inline-flex items-center gap-1.5 decoration-[0.5px] underline-offset-4 hover:text-nb-gray-100 hover:underline transition"
|
"inline-flex items-center gap-1.5 rounded-sm outline-none decoration-[0.5px] underline-offset-4 hover:text-nb-gray-100 hover:underline transition focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon aria-hidden="true" className={iconClassName ?? "h-3.5 w-3.5"} />
|
<Icon aria-hidden="true" className={iconClassName ?? "h-3.5 w-3.5"} />
|
||||||
@@ -150,9 +151,10 @@ export function SettingsAbout() {
|
|||||||
<button
|
<button
|
||||||
key={link.url}
|
key={link.url}
|
||||||
type={"button"}
|
type={"button"}
|
||||||
|
tabIndex={0}
|
||||||
onClick={() => openUrl(link.url)}
|
onClick={() => openUrl(link.url)}
|
||||||
className={
|
className={
|
||||||
"decoration-[0.5px] underline-offset-4 hover:text-nb-gray-100 hover:underline transition"
|
"rounded-sm outline-none decoration-[0.5px] underline-offset-4 hover:text-nb-gray-100 hover:underline transition focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ export const SectionGroup = ({
|
|||||||
<section
|
<section
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
aria-disabled={disabled || undefined}
|
aria-disabled={disabled || undefined}
|
||||||
className={cn("mb-8 last:mb-1 px-1", disabled && "opacity-30 pointer-events-none")}
|
tabIndex={disabled ? -1 : 0}
|
||||||
|
{...(disabled ? { inert: "" } : {})}
|
||||||
|
className={cn(
|
||||||
|
"mb-8 last:mb-1 px-1 rounded-md outline-none",
|
||||||
|
"focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940",
|
||||||
|
disabled && "opacity-30 pointer-events-none",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<h2 className={"text-xs uppercase tracking-wider text-nb-gray-400 mb-4 font-semibold"}>
|
<h2 className={"text-xs uppercase tracking-wider text-nb-gray-400 mb-4 font-semibold"}>
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -93,7 +93,10 @@ export function SettingsTroubleshooting() {
|
|||||||
helpText={t("settings.troubleshooting.packets.help")}
|
helpText={t("settings.troubleshooting.packets.help")}
|
||||||
disabled={!capture}
|
disabled={!capture}
|
||||||
/>
|
/>
|
||||||
<div className={"flex items-center gap-6 justify-between"}>
|
<div
|
||||||
|
className={"flex items-center gap-6 justify-between"}
|
||||||
|
{...(!capture ? { inert: "" } : {})}
|
||||||
|
>
|
||||||
<div className={"flex-1 max-w-md"}>
|
<div className={"flex-1 max-w-md"}>
|
||||||
<Label htmlFor={durationId} disabled={!capture}>
|
<Label htmlFor={durationId} disabled={!capture}>
|
||||||
{t("settings.troubleshooting.duration.label")}
|
{t("settings.troubleshooting.duration.label")}
|
||||||
|
|||||||
Reference in New Issue
Block a user