mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-12 16:15:02 +03:00
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:
@@ -178,7 +178,7 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
|
||||
<div>
|
||||
{modalContextHolder}
|
||||
{messageContextHolder}
|
||||
<Spin spinning={loading}>
|
||||
<Spin aria-live="off" spinning={loading}>
|
||||
<Alert type="info" className="mb-12" title={t('pages.index.geodataHint')} showIcon />
|
||||
<Form layout="vertical">
|
||||
<Form.Item
|
||||
|
||||
@@ -174,6 +174,7 @@ export default function IndexPage() {
|
||||
<Layout className="content-shell">
|
||||
<Layout.Content className="content-area">
|
||||
<Spin
|
||||
aria-live="off"
|
||||
spinning={loading || !fetched}
|
||||
delay={200}
|
||||
description={loading ? loadingTip : t('loading')}
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function VersionModal({ open, status, onClose, onBusy }: VersionM
|
||||
return (
|
||||
<Modal open={open} title={t('pages.index.xrayUpdates')} footer={null} onCancel={onClose}>
|
||||
{modalContextHolder}
|
||||
<Spin spinning={loading}>
|
||||
<Spin aria-live="off" spinning={loading}>
|
||||
<Collapse
|
||||
accordion
|
||||
activeKey={activeKey}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useId, useMemo } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Card, theme } from 'antd';
|
||||
|
||||
@@ -31,6 +31,7 @@ export default function VitalTile({
|
||||
isMobile,
|
||||
}: VitalTileProps) {
|
||||
const { token } = theme.useToken();
|
||||
const labelId = useId();
|
||||
const meanColor = token.colorTextTertiary;
|
||||
|
||||
const referenceLines = useMemo(
|
||||
@@ -39,10 +40,18 @@ export default function VitalTile({
|
||||
);
|
||||
|
||||
return (
|
||||
<Card hoverable className="ov-tile" styles={{ body: { padding: 0 } }}>
|
||||
<Card
|
||||
hoverable
|
||||
className="ov-tile"
|
||||
styles={{ body: { padding: 0 } }}
|
||||
role="group"
|
||||
aria-labelledby={labelId}
|
||||
>
|
||||
<div className="ov-tile-head">
|
||||
<span className="ov-tile-icon">{icon}</span>
|
||||
<span className="ov-kicker">{label}</span>
|
||||
<span className="ov-kicker" id={labelId}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="ov-tile-value">
|
||||
|
||||
Reference in New Issue
Block a user