mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-04 04:05:04 +03:00
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:
@@ -1,20 +1,8 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState, useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
Alert,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Radio,
|
||||
Select,
|
||||
Switch,
|
||||
Tabs,
|
||||
Tooltip,
|
||||
message,
|
||||
} from 'antd';
|
||||
import { Alert, Form, Input, InputNumber, Modal, Radio, Select, Switch, Tabs, message } from 'antd';
|
||||
import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils';
|
||||
@@ -99,16 +87,6 @@ import SniffingTab from './SniffingTab';
|
||||
import type { DBInbound } from '@/models/dbinbound';
|
||||
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||
|
||||
/* Render a field label with a hover tooltip icon instead of an `extra` help line below. */
|
||||
const labelWithHint = (label: string, hint: string) => (
|
||||
<span>
|
||||
{label}
|
||||
<Tooltip title={hint}>
|
||||
<QuestionCircleOutlined style={{ marginInlineStart: 4, color: 'rgba(128,128,128,0.65)' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
|
||||
const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
|
||||
const SHARE_ADDR_STRATEGIES = ['node', 'listen', 'custom'] as const;
|
||||
const SHARE_ADDR_HOSTNAME_RE =
|
||||
@@ -240,6 +218,7 @@ export default function InboundFormModal({
|
||||
availableNodesFetched = true,
|
||||
}: InboundFormModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const [modal, modalContextHolder] = Modal.useModal();
|
||||
const methods = useForm<InboundFormValues>({ defaultValues: buildAddModeValues() });
|
||||
@@ -668,7 +647,8 @@ export default function InboundFormModal({
|
||||
|
||||
<FormField
|
||||
name="listen"
|
||||
label={labelWithHint(t('pages.inbounds.address'), t('pages.inbounds.form.listenHelp'))}
|
||||
label={t('pages.inbounds.address')}
|
||||
tooltip={t('pages.inbounds.form.listenHelp')}
|
||||
>
|
||||
<Input placeholder={t('pages.inbounds.monitorDesc')} />
|
||||
</FormField>
|
||||
@@ -677,10 +657,8 @@ export default function InboundFormModal({
|
||||
<>
|
||||
<FormField
|
||||
name="shareAddrStrategy"
|
||||
label={labelWithHint(
|
||||
t('pages.inbounds.form.shareAddrStrategy'),
|
||||
t('pages.inbounds.form.shareAddrStrategyHelp'),
|
||||
)}
|
||||
label={t('pages.inbounds.form.shareAddrStrategy')}
|
||||
tooltip={t('pages.inbounds.form.shareAddrStrategyHelp')}
|
||||
>
|
||||
<Select
|
||||
options={SHARE_ADDR_STRATEGIES.filter(
|
||||
@@ -695,10 +673,8 @@ export default function InboundFormModal({
|
||||
{shareAddrStrategy === 'custom' && (
|
||||
<FormField
|
||||
name="shareAddr"
|
||||
label={labelWithHint(
|
||||
t('pages.inbounds.form.shareAddr'),
|
||||
t('pages.inbounds.form.shareAddrHelp'),
|
||||
)}
|
||||
label={t('pages.inbounds.form.shareAddr')}
|
||||
tooltip={t('pages.inbounds.form.shareAddrHelp')}
|
||||
rules={{
|
||||
validate: (value) =>
|
||||
isValidShareAddrInput(String(value ?? '')) ||
|
||||
@@ -713,10 +689,8 @@ export default function InboundFormModal({
|
||||
|
||||
<FormField
|
||||
name="subSortIndex"
|
||||
label={labelWithHint(
|
||||
t('pages.inbounds.form.subSortIndex'),
|
||||
t('pages.inbounds.form.subSortIndexHelp'),
|
||||
)}
|
||||
label={t('pages.inbounds.form.subSortIndex')}
|
||||
tooltip={t('pages.inbounds.form.subSortIndexHelp')}
|
||||
>
|
||||
<InputNumber />
|
||||
</FormField>
|
||||
@@ -724,10 +698,8 @@ export default function InboundFormModal({
|
||||
<FormField
|
||||
name="excludeFromSub"
|
||||
valueProp="checked"
|
||||
label={labelWithHint(
|
||||
t('pages.inbounds.form.excludeFromSub'),
|
||||
t('pages.inbounds.form.excludeFromSubHelp'),
|
||||
)}
|
||||
label={t('pages.inbounds.form.excludeFromSub')}
|
||||
tooltip={t('pages.inbounds.form.excludeFromSubHelp')}
|
||||
>
|
||||
<Switch />
|
||||
</FormField>
|
||||
@@ -736,10 +708,8 @@ export default function InboundFormModal({
|
||||
<FormField
|
||||
name="disableFlow"
|
||||
valueProp="checked"
|
||||
label={labelWithHint(
|
||||
t('pages.inbounds.form.disableFlow'),
|
||||
t('pages.inbounds.form.disableFlowHelp'),
|
||||
)}
|
||||
label={t('pages.inbounds.form.disableFlow')}
|
||||
tooltip={t('pages.inbounds.form.disableFlowHelp')}
|
||||
>
|
||||
<Switch />
|
||||
</FormField>
|
||||
@@ -754,13 +724,13 @@ export default function InboundFormModal({
|
||||
</FormField>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title={t('pages.inbounds.meansNoLimit')}>
|
||||
{t('pages.inbounds.totalFlow')}
|
||||
</Tooltip>
|
||||
}
|
||||
htmlFor={`${uid}-totalFlow`}
|
||||
label={t('pages.inbounds.totalFlow')}
|
||||
tooltip={t('pages.inbounds.meansNoLimit')}
|
||||
>
|
||||
<InputNumber
|
||||
id={`${uid}-totalFlow`}
|
||||
aria-describedby={`${uid}-totalFlow-hint`}
|
||||
value={wTotal ? Math.round((wTotal / SizeFormatter.ONE_GB) * 100) / 100 : 0}
|
||||
min={0}
|
||||
step={1}
|
||||
@@ -769,6 +739,9 @@ export default function InboundFormModal({
|
||||
setV('total', bytes);
|
||||
}}
|
||||
/>
|
||||
<span id={`${uid}-totalFlow-hint`} hidden>
|
||||
{t('pages.inbounds.meansNoLimit')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
|
||||
<FormField name="trafficReset" label={t('pages.inbounds.periodicTrafficResetTitle')}>
|
||||
@@ -791,16 +764,19 @@ export default function InboundFormModal({
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title={t('pages.inbounds.leaveBlankToNeverExpire')}>
|
||||
{t('pages.inbounds.expireDate')}
|
||||
</Tooltip>
|
||||
}
|
||||
htmlFor={`${uid}-expiryTime`}
|
||||
label={t('pages.inbounds.expireDate')}
|
||||
tooltip={t('pages.inbounds.leaveBlankToNeverExpire')}
|
||||
>
|
||||
<DateTimePicker
|
||||
id={`${uid}-expiryTime`}
|
||||
aria-describedby={`${uid}-expiryTime-hint`}
|
||||
value={wExpiry > 0 ? dayjs(wExpiry) : null}
|
||||
onChange={(d) => setV('expiryTime', d ? d.valueOf() : 0)}
|
||||
/>
|
||||
<span id={`${uid}-expiryTime-hint`} hidden>
|
||||
{t('pages.inbounds.leaveBlankToNeverExpire')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
@@ -936,8 +912,9 @@ export default function InboundFormModal({
|
||||
const streamTab = (
|
||||
<>
|
||||
{hasSelectableTransport && (
|
||||
<Form.Item label={t('transmission')}>
|
||||
<Form.Item htmlFor={`${uid}-network`} label={t('transmission')}>
|
||||
<Select
|
||||
id={`${uid}-network`}
|
||||
style={{ width: '75%' }}
|
||||
value={network}
|
||||
onChange={onNetworkChange}
|
||||
|
||||
Reference in New Issue
Block a user