fix(frontend): make the panel usable with screen readers (#6907)

* fix(frontend): make the panel usable with screen readers

* fix(frontend): keep mobile tab names and the inbound expiry hint

Review of #6907 found two gaps in the screen-reader pass:

- On phone-width screens catTabLabel renders only the icon and names the
  tab by putting aria-label on it, but callers now pass that icon with
  aria-hidden for the desktop label, so every Settings, Xray and Host
  category tab lost its name under TalkBack. catTabLabel now clears
  aria-hidden on the icon it uses as the name.
- DateTimePicker forwarded id and aria-label but not aria-describedby,
  so the inbound expiry hint ("leave blank to never expire") was never
  attached. It now forwards it on both the Gregorian and Jalali input.
This commit is contained in:
Alireza Ahmadi
2026-10-23 18:29:47 +02:00
committed by GitHub
parent 775b2b0b26
commit e2184777a0
156 changed files with 2373 additions and 863 deletions
@@ -0,0 +1,9 @@
import { PlusOutlined, WarningOutlined } from '@ant-design/icons';
import { Button } from 'antd';
declare const rest: Record<string, unknown>;
export const Decorative = () => <Button icon={<PlusOutlined aria-hidden />}>Add</Button>;
export const Named = () => <WarningOutlined aria-label="Disk almost full" />;
export const LabelledBy = () => <WarningOutlined aria-labelledby="warning-text" />;
export const Forwarded = () => <WarningOutlined {...rest} />;
@@ -0,0 +1,8 @@
import { PlusOutlined, WarningOutlined } from '@ant-design/icons';
import { Button } from 'antd';
declare function CustomOutlined(): null;
export const InButton = () => <Button icon={<PlusOutlined />}>Add</Button>;
export const Standalone = () => <WarningOutlined style={{ color: 'red' }} />;
export const NotAnAntdIcon = () => <CustomOutlined />;
@@ -0,0 +1,6 @@
{
"plugins": [],
"categories": {},
"jsPlugins": ["../icon-a11y-guard.mjs"],
"rules": { "antd-icon/explicit-a11y": "error" }
}
@@ -0,0 +1,6 @@
{
"plugins": [],
"categories": {},
"jsPlugins": ["../control-label-guard.mjs"],
"rules": { "control-label/has-name": "error" }
}
@@ -0,0 +1,43 @@
import { Form, Input, Space, Switch } from 'antd';
declare function FormField(props: { label?: string; children?: unknown }): null;
declare function SettingListItem(props: { title?: string; control?: unknown; children?: unknown }): null;
declare const rest: Record<string, unknown>;
export const AriaLabel = () => <Switch aria-label="Enabled" />;
export const LabelledBy = () => <Switch aria-labelledby="heading" />;
export const Forwarded = () => <Input {...rest} />;
export const Wrapped = () => (
// eslint-disable-next-line jsx-a11y/label-has-associated-control -- antd Input is opaque to it
<label>
Email <Input />
</label>
);
export const HtmlForAndId = () => (
<Form.Item label="Email" htmlFor="email">
<Space.Compact>
<Input id="email" />
</Space.Compact>
</Form.Item>
);
export const NamedItemDirect = () => (
<Form.Item label="Email" name="email">
<Input />
</Form.Item>
);
export const ViaFormField = () => (
<FormField label="Email">
<Input />
</FormField>
);
export const SettingChild = () => (
<SettingListItem title="Enabled">
<Switch />
</SettingListItem>
);
export const SettingControlProp = () => <SettingListItem title="Enabled" control={<Switch />} />;
@@ -0,0 +1,39 @@
import { Form, Input, Space, Switch } from 'antd';
declare function SettingListItem(props: { title?: string; children?: unknown }): null;
export const TableCell = () => <Switch checked />;
export const LabelOnlyItem = () => (
<Form.Item label="Email">
<Input />
</Form.Item>
);
export const IdWithoutHtmlFor = () => (
<Form.Item label="Email">
<Input id="email" />
</Form.Item>
);
export const HtmlForWithoutId = () => (
<Form.Item label="Email" htmlFor="email">
<Input />
</Form.Item>
);
export const NamedItemNested = () => (
<Form.Item label="Email" name="email">
<Space.Compact>
<Input />
</Space.Compact>
</Form.Item>
);
export const SettingNested = () => (
<SettingListItem title="Enabled">
<Space>
<Switch />
</Space>
</SettingListItem>
);