fix(a11y): silence live stats and make Select tags removable for screen readers

Ant Design's Spin wraps its content in aria-live="polite", so every
WebSocket tick on the dashboard, inbounds, clients and nodes pages was
announced, and only the changed number was read, with no label.
Pass aria-live="off" on every Spin that wraps content; aria-busy still
signals loading. Each vital tile is now a group named by its metric,
so "47.2 %" is read as CPU.

rc-select renders each tag's remove "x" as an aria-hidden span with no
tabIndex, so screen-reader users could only Backspace the last value
away, never remove one SNI or inbound from the middle.
selectTagRender swaps in a real button named "Remove <value>" and
moves focus to the next tag after removal. It is applied to every
tags/multiple Select. maxTagCount="responsive" is dropped because it
hid selected inbounds behind "+N", where no screen reader could reach
them.

Two oxlint rules (antd-a11y/spin-content-not-live and
antd-a11y/select-tag-remove) keep new code from regressing either fix.
This commit is contained in:
Alireza Ahmadi
2026-11-01 16:49:19 +01:00
parent cbf596d889
commit 1bdb517b88
73 changed files with 672 additions and 48 deletions
@@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Select, Switch } from 'antd';
import { selectTagRender } from '@/components/form';
import { SettingListItem } from '@/components/ui';
import type { XraySettingsValue, SetTemplate } from '@/hooks/useXraySetting';
import {
@@ -105,6 +106,7 @@ export default function RoutingBasic({ templateSettings, setTemplateSettings }:
control={
<Select
mode="tags"
tagRender={selectTagRender}
value={blockedIPs}
style={{ width: '100%' }}
options={IPS_OPTIONS}
@@ -119,6 +121,7 @@ export default function RoutingBasic({ templateSettings, setTemplateSettings }:
control={
<Select
mode="tags"
tagRender={selectTagRender}
value={blockedDomains}
style={{ width: '100%' }}
options={BLOCK_DOMAINS_OPTIONS}
@@ -140,6 +143,7 @@ export default function RoutingBasic({ templateSettings, setTemplateSettings }:
control={
<Select
mode="tags"
tagRender={selectTagRender}
value={directIPs}
style={{ width: '100%' }}
options={IPS_OPTIONS}
@@ -159,6 +163,7 @@ export default function RoutingBasic({ templateSettings, setTemplateSettings }:
control={
<Select
mode="tags"
tagRender={selectTagRender}
value={directDomains}
style={{ width: '100%' }}
options={DOMAINS_OPTIONS}
@@ -179,6 +184,7 @@ export default function RoutingBasic({ templateSettings, setTemplateSettings }:
control={
<Select
mode="tags"
tagRender={selectTagRender}
value={ipv4Domains}
style={{ width: '100%' }}
options={SERVICES_OPTIONS}