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
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useId, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
AutoComplete,
@@ -82,6 +82,7 @@ export default function ClientBulkAddModal({
onSaved,
}: ClientBulkAddModalProps) {
const { t } = useTranslation();
const uid = useId();
const [messageApi, messageContextHolder] = message.useMessage();
const { bulkCreate } = useClients({ list: false });
@@ -254,13 +255,18 @@ export default function ClientBulkAddModal({
>
<FormProvider {...methods}>
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
<Form.Item label={t('pages.clients.attachedInbounds')} required>
<Form.Item
label={t('pages.clients.attachedInbounds')}
required
htmlFor={`${uid}-inbounds`}
>
<SelectAllClearButtons
options={inboundOptions}
value={inboundIds}
onChange={(v) => methods.setValue('inboundIds', v)}
/>
<Select
id={`${uid}-inbounds`}
mode="multiple"
value={inboundIds}
onChange={(v) => methods.setValue('inboundIds', v)}
@@ -323,16 +329,17 @@ export default function ClientBulkAddModal({
</FormField>
)}
<Form.Item label={t('pages.clients.subId')}>
<Form.Item label={t('pages.clients.subId')} htmlFor={`${uid}-sub-id`}>
<Space.Compact style={{ display: 'flex' }}>
<Input
id={`${uid}-sub-id`}
value={subId}
onChange={(e) => methods.setValue('subId', e.target.value)}
style={{ flex: 1 }}
/>
<Button
aria-label={t('regenerate')}
icon={<ReloadOutlined />}
icon={<ReloadOutlined aria-hidden />}
onClick={() => methods.setValue('subId', RandomUtil.randomLowerAndNum(16))}
/>
</Space.Compact>
@@ -376,10 +383,11 @@ export default function ClientBulkAddModal({
</FormField>
)}
<Form.Item label={t('pages.clients.limitIp')}>
<Form.Item label={t('pages.clients.limitIp')} htmlFor={`${uid}-limit-ip`}>
<Tooltip title={limitIpNotice || undefined}>
<span style={{ display: 'inline-flex' }}>
<InputNumber
id={`${uid}-limit-ip`}
value={limitIp}
min={0}
disabled={limitIpDisabled}
@@ -399,8 +407,9 @@ export default function ClientBulkAddModal({
<InputNumber min={0} step={1} />
</FormField>
<Form.Item label={t('pages.clients.delayedStart')}>
<Form.Item label={t('pages.clients.delayedStart')} htmlFor={`${uid}-delayed-start`}>
<Switch
id={`${uid}-delayed-start`}
checked={delayedStart}
onClick={() => {
setDelayedStart(!delayedStart);
@@ -410,16 +419,18 @@ export default function ClientBulkAddModal({
</Form.Item>
{delayedStart ? (
<Form.Item label={t('pages.clients.expireDays')}>
<Form.Item label={t('pages.clients.expireDays')} htmlFor={`${uid}-expire-days`}>
<InputNumber
id={`${uid}-expire-days`}
value={delayedExpireDays}
min={0}
onChange={(v) => methods.setValue('expiryTime', -86400000 * (Number(v) || 0))}
/>
</Form.Item>
) : (
<Form.Item label={t('pages.inbounds.expireDate')}>
<Form.Item label={t('pages.inbounds.expireDate')} htmlFor={`${uid}-expiry`}>
<DateTimePicker
id={`${uid}-expiry`}
value={expiryDate}
onChange={(next) => methods.setValue('expiryTime', next ? next.valueOf() : 0)}
/>