mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-06 21:25: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:
@@ -111,6 +111,7 @@ export default function BulkAttachInboundsModal({
|
||||
onChange={setTargetIds}
|
||||
options={targetOptions}
|
||||
placeholder={t('pages.clients.attachToInboundsTargets')}
|
||||
aria-label={t('pages.clients.attachToInboundsTargets')}
|
||||
showSearch={{ optionFilterProp: 'label' }}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@@ -111,6 +111,7 @@ export default function BulkDetachInboundsModal({
|
||||
onChange={setTargetIds}
|
||||
options={targetOptions}
|
||||
placeholder={t('pages.clients.detachFromInboundsTargets')}
|
||||
aria-label={t('pages.clients.detachFromInboundsTargets')}
|
||||
showSearch={{ optionFilterProp: 'label' }}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useId, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AutoComplete,
|
||||
@@ -256,6 +256,7 @@ export default function ClientFormModal({
|
||||
onOpenChange,
|
||||
}: ClientFormModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const isEdit = mode === 'edit';
|
||||
|
||||
@@ -820,7 +821,7 @@ export default function ClientFormModal({
|
||||
<Button
|
||||
color="danger"
|
||||
variant="filled"
|
||||
icon={<RetweetOutlined />}
|
||||
icon={<RetweetOutlined aria-hidden />}
|
||||
loading={resetting}
|
||||
>
|
||||
{t('pages.inbounds.resetTraffic')}
|
||||
@@ -848,9 +849,14 @@ export default function ClientFormModal({
|
||||
<>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item label={t('pages.clients.email')} required>
|
||||
<Form.Item
|
||||
label={t('pages.clients.email')}
|
||||
required
|
||||
htmlFor={`${uid}-email`}
|
||||
>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
<Input
|
||||
id={`${uid}-email`}
|
||||
value={email}
|
||||
placeholder={t('pages.clients.email')}
|
||||
style={{ flex: 1 }}
|
||||
@@ -859,7 +865,7 @@ export default function ClientFormModal({
|
||||
{!isEdit && (
|
||||
<Button
|
||||
aria-label={t('regenerate')}
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
onClick={() =>
|
||||
methods.setValue('email', RandomUtil.randomLowerAndNum(12))
|
||||
}
|
||||
@@ -880,6 +886,7 @@ export default function ClientFormModal({
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-limit-ip`}
|
||||
label={t('pages.clients.limitIp')}
|
||||
tooltip={t('pages.clients.limitIpDesc')}
|
||||
>
|
||||
@@ -887,6 +894,8 @@ export default function ClientFormModal({
|
||||
<span style={{ display: 'flex', width: '100%' }}>
|
||||
<Space.Compact style={{ display: 'flex', flex: 1 }}>
|
||||
<InputNumber
|
||||
id={`${uid}-limit-ip`}
|
||||
aria-describedby={`${uid}-limit-ip-hint`}
|
||||
value={limitIp}
|
||||
min={0}
|
||||
disabled={limitIpDisabled}
|
||||
@@ -900,7 +909,7 @@ export default function ClientFormModal({
|
||||
<Tooltip title={t('pages.clients.ipLog')}>
|
||||
<Button
|
||||
aria-label={t('pages.clients.ipLog')}
|
||||
icon={<EyeOutlined />}
|
||||
icon={<EyeOutlined aria-hidden />}
|
||||
loading={ipsLoading}
|
||||
onClick={openIpsModal}
|
||||
>
|
||||
@@ -911,15 +920,21 @@ export default function ClientFormModal({
|
||||
</Space.Compact>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<span id={`${uid}-limit-ip-hint`} hidden>
|
||||
{t('pages.clients.limitIpDesc')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-limit-hwid`}
|
||||
label={t('pages.clients.limitHwid')}
|
||||
tooltip={t('pages.clients.limitHwidDesc')}
|
||||
>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
<InputNumber
|
||||
id={`${uid}-limit-hwid`}
|
||||
aria-describedby={`${uid}-limit-hwid-hint`}
|
||||
value={limitHwid}
|
||||
min={0}
|
||||
style={{ flex: 1 }}
|
||||
@@ -929,7 +944,7 @@ export default function ClientFormModal({
|
||||
<Tooltip title={t('pages.clients.hwidLog')}>
|
||||
<Button
|
||||
aria-label={t('pages.clients.hwidLog')}
|
||||
icon={<EyeOutlined />}
|
||||
icon={<EyeOutlined aria-hidden />}
|
||||
loading={hwidsLoading}
|
||||
onClick={openHwidsModal}
|
||||
>
|
||||
@@ -938,6 +953,9 @@ export default function ClientFormModal({
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space.Compact>
|
||||
<span id={`${uid}-limit-hwid-hint`} hidden>
|
||||
{t('pages.clients.limitHwidDesc')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -953,8 +971,12 @@ export default function ClientFormModal({
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</FormField>
|
||||
) : (
|
||||
<Form.Item label={t('pages.clients.expiryTime')}>
|
||||
<Form.Item
|
||||
label={t('pages.clients.expiryTime')}
|
||||
htmlFor={`${uid}-expiry`}
|
||||
>
|
||||
<DateTimePicker
|
||||
id={`${uid}-expiry`}
|
||||
value={expiryDayjs}
|
||||
onChange={(d) =>
|
||||
methods.setValue('expiryDate', d ? d.valueOf() : 0)
|
||||
@@ -964,8 +986,12 @@ export default function ClientFormModal({
|
||||
)}
|
||||
</Col>
|
||||
<Col xs={12} md={6}>
|
||||
<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}
|
||||
onChange={(v) => {
|
||||
methods.setValue('delayedStart', v);
|
||||
@@ -1062,13 +1088,18 @@ export default function ClientFormModal({
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Form.Item label={t('pages.clients.attachedInbounds')} required={!isEdit}>
|
||||
<Form.Item
|
||||
label={t('pages.clients.attachedInbounds')}
|
||||
required={!isEdit}
|
||||
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)}
|
||||
@@ -1102,77 +1133,96 @@ export default function ClientFormModal({
|
||||
label: t('pages.clients.tabCredentials'),
|
||||
children: (
|
||||
<>
|
||||
<Form.Item label={t('pages.clients.uuid')}>
|
||||
<Form.Item label={t('pages.clients.uuid')} htmlFor={`${uid}-uuid`}>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
<Input
|
||||
id={`${uid}-uuid`}
|
||||
value={uuid}
|
||||
style={{ flex: 1 }}
|
||||
onChange={(e) => methods.setValue('uuid', e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
aria-label={t('regenerate')}
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
onClick={() => methods.setValue('uuid', RandomUtil.randomUUID())}
|
||||
/>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-password`}
|
||||
label={t('pages.clients.password')}
|
||||
tooltip={t('pages.clients.passwordDesc')}
|
||||
>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
<Input
|
||||
id={`${uid}-password`}
|
||||
aria-describedby={`${uid}-password-hint`}
|
||||
value={password}
|
||||
style={{ flex: 1 }}
|
||||
onChange={(e) => methods.setValue('password', e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
aria-label={t('regenerate')}
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
onClick={regeneratePassword}
|
||||
/>
|
||||
</Space.Compact>
|
||||
<span id={`${uid}-password-hint`} hidden>
|
||||
{t('pages.clients.passwordDesc')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-sub-id`}
|
||||
label={t('pages.clients.subId')}
|
||||
tooltip={t('pages.clients.subIdDesc')}
|
||||
>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
<Input
|
||||
id={`${uid}-sub-id`}
|
||||
aria-describedby={`${uid}-sub-id-hint`}
|
||||
value={subId}
|
||||
style={{ flex: 1 }}
|
||||
onChange={(e) => methods.setValue('subId', e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
aria-label={t('regenerate')}
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
onClick={() =>
|
||||
methods.setValue('subId', RandomUtil.randomLowerAndNum(16))
|
||||
}
|
||||
/>
|
||||
</Space.Compact>
|
||||
<span id={`${uid}-sub-id-hint`} hidden>
|
||||
{t('pages.clients.subIdDesc')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-auth`}
|
||||
label={t('pages.clients.hysteriaAuth')}
|
||||
tooltip={t('pages.clients.hysteriaAuthDesc')}
|
||||
>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
<Input
|
||||
id={`${uid}-auth`}
|
||||
aria-describedby={`${uid}-auth-hint`}
|
||||
value={auth}
|
||||
style={{ flex: 1 }}
|
||||
onChange={(e) => methods.setValue('auth', e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
aria-label={t('regenerate')}
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
onClick={() =>
|
||||
methods.setValue('auth', RandomUtil.randomLowerAndNum(16))
|
||||
}
|
||||
/>
|
||||
</Space.Compact>
|
||||
<span id={`${uid}-auth-hint`} hidden>
|
||||
{t('pages.clients.hysteriaAuthDesc')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
|
||||
{showFlow && (
|
||||
@@ -1195,6 +1245,7 @@ export default function ClientFormModal({
|
||||
{(showWireguard || showAmneziawg) && (
|
||||
<>
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-wg-private-key`}
|
||||
label={t(
|
||||
showAmneziawg
|
||||
? 'pages.clients.amneziaWgPrivateKey'
|
||||
@@ -1203,6 +1254,7 @@ export default function ClientFormModal({
|
||||
>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
<Input
|
||||
id={`${uid}-wg-private-key`}
|
||||
value={wgPrivateKey}
|
||||
style={{ flex: 1 }}
|
||||
onChange={(e) => {
|
||||
@@ -1216,7 +1268,7 @@ export default function ClientFormModal({
|
||||
/>
|
||||
<Button
|
||||
aria-label={t('regenerate')}
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
onClick={regenerateWireguardKeys}
|
||||
/>
|
||||
</Space.Compact>
|
||||
@@ -1232,6 +1284,7 @@ export default function ClientFormModal({
|
||||
<Input disabled />
|
||||
</FormField>
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-wg-psk`}
|
||||
label={t(
|
||||
showAmneziawg
|
||||
? 'pages.clients.amneziaWgPreSharedKey'
|
||||
@@ -1240,11 +1293,11 @@ export default function ClientFormModal({
|
||||
>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
<FormField name="wgPreSharedKey" noStyle>
|
||||
<Input style={{ flex: 1 }} />
|
||||
<Input id={`${uid}-wg-psk`} style={{ flex: 1 }} />
|
||||
</FormField>
|
||||
<Button
|
||||
aria-label={t('regenerate')}
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
onClick={regenerateWireguardPresharedKey}
|
||||
/>
|
||||
</Space.Compact>
|
||||
@@ -1305,18 +1358,20 @@ export default function ClientFormModal({
|
||||
{showMtproto && (
|
||||
<>
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-mtproto-secret`}
|
||||
label={t('pages.clients.mtprotoSecret')}
|
||||
extra={t('pages.clients.mtprotoSecretHint')}
|
||||
>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
<Input
|
||||
id={`${uid}-mtproto-secret`}
|
||||
value={secret}
|
||||
style={{ flex: 1 }}
|
||||
onChange={(e) => methods.setValue('secret', e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
aria-label={t('regenerate')}
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
onClick={regenerateMtprotoSecret}
|
||||
/>
|
||||
</Space.Compact>
|
||||
@@ -1344,7 +1399,7 @@ export default function ClientFormModal({
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
onClick={() => addExternalLinkRow('link')}
|
||||
>
|
||||
{t('pages.clients.addExternalLink')}
|
||||
@@ -1364,7 +1419,7 @@ export default function ClientFormModal({
|
||||
valueProp="checked"
|
||||
noStyle
|
||||
>
|
||||
<Switch size="small" />
|
||||
<Switch size="small" aria-label={t('enable')} />
|
||||
</FormField>
|
||||
<span>{t('enable')}</span>
|
||||
</div>
|
||||
@@ -1378,7 +1433,7 @@ export default function ClientFormModal({
|
||||
<Button
|
||||
aria-label={t('delete')}
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
icon={<DeleteOutlined aria-hidden />}
|
||||
onClick={() => removeExternalLink(index)}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -1398,6 +1453,7 @@ export default function ClientFormModal({
|
||||
const hasSpecificExpiry = Number(expiryField.value) > 0;
|
||||
return (
|
||||
<DateTimePicker
|
||||
aria-label={t('pages.clients.expiryTime')}
|
||||
value={displayedExpiry > 0 ? dayjs(displayedExpiry) : null}
|
||||
onChange={(v) => expiryField.onChange(v ? v.valueOf() : 0)}
|
||||
placeholder={t('pages.inbounds.leaveBlankToNeverExpire')}
|
||||
@@ -1415,7 +1471,7 @@ export default function ClientFormModal({
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
onClick={() => addExternalLinkRow('subscription')}
|
||||
>
|
||||
{t('pages.clients.addExternalSubscription')}
|
||||
@@ -1435,7 +1491,7 @@ export default function ClientFormModal({
|
||||
valueProp="checked"
|
||||
noStyle
|
||||
>
|
||||
<Switch size="small" />
|
||||
<Switch size="small" aria-label={t('enable')} />
|
||||
</FormField>
|
||||
<span>{t('enable')}</span>
|
||||
</div>
|
||||
@@ -1449,7 +1505,7 @@ export default function ClientFormModal({
|
||||
<Button
|
||||
aria-label={t('delete')}
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
icon={<DeleteOutlined aria-hidden />}
|
||||
onClick={() => removeExternalLink(index)}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -1475,6 +1531,7 @@ export default function ClientFormModal({
|
||||
const hasSpecificExpiry = Number(expiryField.value) > 0;
|
||||
return (
|
||||
<DateTimePicker
|
||||
aria-label={t('pages.clients.expiryTime')}
|
||||
value={displayedExpiry > 0 ? dayjs(displayedExpiry) : null}
|
||||
onChange={(v) => expiryField.onChange(v ? v.valueOf() : 0)}
|
||||
placeholder={t('pages.inbounds.leaveBlankToNeverExpire')}
|
||||
@@ -1515,7 +1572,12 @@ export default function ClientFormModal({
|
||||
zIndex={CLIENT_IP_LOG_MODAL_Z_INDEX}
|
||||
onCancel={() => setIpsModalOpen(false)}
|
||||
footer={[
|
||||
<Button key="refresh" icon={<ReloadOutlined />} loading={ipsLoading} onClick={loadIps}>
|
||||
<Button
|
||||
key="refresh"
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
loading={ipsLoading}
|
||||
onClick={loadIps}
|
||||
>
|
||||
{t('refresh')}
|
||||
</Button>,
|
||||
<Button
|
||||
|
||||
@@ -350,7 +350,7 @@ export default function ClientInfoModal({
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyValue(client.subId!)}
|
||||
/>
|
||||
@@ -365,7 +365,7 @@ export default function ClientInfoModal({
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyValue(client.uuid!)}
|
||||
/>
|
||||
@@ -380,7 +380,7 @@ export default function ClientInfoModal({
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyValue(client.password!)}
|
||||
/>
|
||||
@@ -395,7 +395,7 @@ export default function ClientInfoModal({
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyValue(client.auth!)}
|
||||
/>
|
||||
@@ -457,7 +457,7 @@ export default function ClientInfoModal({
|
||||
<td>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
icon={<EyeOutlined aria-hidden />}
|
||||
aria-label={t('pages.clients.ipLog')}
|
||||
loading={ipsLoading}
|
||||
onClick={openIpsModal}
|
||||
@@ -489,7 +489,7 @@ export default function ClientInfoModal({
|
||||
<td>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
icon={<EyeOutlined aria-hidden />}
|
||||
aria-label={t('pages.clients.hwidLog')}
|
||||
loading={hwidsLoading}
|
||||
onClick={openHwidsModal}
|
||||
@@ -591,7 +591,7 @@ export default function ClientInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyValue(subLink)}
|
||||
/>
|
||||
@@ -599,7 +599,7 @@ export default function ClientInfoModal({
|
||||
<Tooltip title={t('download')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
icon={<DownloadOutlined aria-hidden />}
|
||||
aria-label={t('download')}
|
||||
loading={downloadingFormat === 'standard'}
|
||||
disabled={downloadingFormat !== null}
|
||||
@@ -621,7 +621,7 @@ export default function ClientInfoModal({
|
||||
<Tooltip title={t('pages.clients.qrCode')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<QrcodeOutlined />}
|
||||
icon={<QrcodeOutlined aria-hidden />}
|
||||
aria-label={t('pages.clients.qrCode')}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -646,7 +646,7 @@ export default function ClientInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyValue(subJsonLink)}
|
||||
/>
|
||||
@@ -654,7 +654,7 @@ export default function ClientInfoModal({
|
||||
<Tooltip title={t('download')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
icon={<DownloadOutlined aria-hidden />}
|
||||
aria-label={t('download')}
|
||||
loading={downloadingFormat === 'json'}
|
||||
disabled={downloadingFormat !== null}
|
||||
@@ -676,7 +676,7 @@ export default function ClientInfoModal({
|
||||
<Tooltip title={t('pages.clients.qrCode')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<QrcodeOutlined />}
|
||||
icon={<QrcodeOutlined aria-hidden />}
|
||||
aria-label={t('pages.clients.qrCode')}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -704,7 +704,7 @@ export default function ClientInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyValue(subClashLink)}
|
||||
/>
|
||||
@@ -712,7 +712,7 @@ export default function ClientInfoModal({
|
||||
<Tooltip title={t('download')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
icon={<DownloadOutlined aria-hidden />}
|
||||
aria-label={t('download')}
|
||||
loading={downloadingFormat === 'clash'}
|
||||
disabled={downloadingFormat !== null}
|
||||
@@ -734,7 +734,7 @@ export default function ClientInfoModal({
|
||||
<Tooltip title={t('pages.clients.qrCode')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<QrcodeOutlined />}
|
||||
icon={<QrcodeOutlined aria-hidden />}
|
||||
aria-label={t('pages.clients.qrCode')}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -768,7 +768,7 @@ export default function ClientInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyValue(link)}
|
||||
/>
|
||||
@@ -783,7 +783,7 @@ export default function ClientInfoModal({
|
||||
<Tooltip title={t('pages.clients.qrCode')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<QrcodeOutlined />}
|
||||
icon={<QrcodeOutlined aria-hidden />}
|
||||
aria-label={t('pages.clients.qrCode')}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -853,7 +853,12 @@ export default function ClientInfoModal({
|
||||
width={440}
|
||||
onCancel={() => setIpsModalOpen(false)}
|
||||
footer={[
|
||||
<Button key="refresh" icon={<ReloadOutlined />} loading={ipsLoading} onClick={loadIps}>
|
||||
<Button
|
||||
key="refresh"
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
loading={ipsLoading}
|
||||
onClick={loadIps}
|
||||
>
|
||||
{t('refresh')}
|
||||
</Button>,
|
||||
<Button
|
||||
|
||||
@@ -113,7 +113,7 @@ function UngroupIcon() {
|
||||
height: '1em',
|
||||
}}
|
||||
>
|
||||
<TagsOutlined />
|
||||
<TagsOutlined aria-hidden />
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
@@ -1081,6 +1081,7 @@ export default function ClientsPage() {
|
||||
checked={!!record.enable}
|
||||
size="small"
|
||||
loading={togglingEmail === record.email}
|
||||
aria-label={t('pages.clients.enabled')}
|
||||
onChange={(next) => onToggleEnable(record, next)}
|
||||
/>
|
||||
),
|
||||
@@ -1140,16 +1141,20 @@ export default function ClientsPage() {
|
||||
render: (_v, record) => {
|
||||
if (!record.group) return <Typography.Text type="secondary">—</Typography.Text>;
|
||||
const isActive = filters.groups.includes(record.group);
|
||||
const filterByGroup = () => {
|
||||
if (!isActive) setFilters({ ...filters, groups: [...filters.groups, record.group!] });
|
||||
};
|
||||
return (
|
||||
<Tag
|
||||
color="geekblue"
|
||||
style={{ margin: 0, cursor: 'pointer', opacity: isActive ? 0.6 : 1 }}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!isActive) {
|
||||
setFilters({ ...filters, groups: [...filters.groups, record.group!] });
|
||||
}
|
||||
filterByGroup();
|
||||
}}
|
||||
onKeyDown={activateOnKey(filterByGroup)}
|
||||
>
|
||||
{record.group}
|
||||
</Tag>
|
||||
@@ -1327,7 +1332,7 @@ export default function ClientsPage() {
|
||||
<SummaryStat
|
||||
title={t('clients')}
|
||||
value={summary.total}
|
||||
prefix={<TeamOutlined />}
|
||||
prefix={<TeamOutlined aria-hidden />}
|
||||
onSelect={() => selectBucket(null)}
|
||||
/>
|
||||
</Col>
|
||||
@@ -1393,7 +1398,7 @@ export default function ClientsPage() {
|
||||
{selectedRowKeys.length === 0 ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
onClick={onAdd}
|
||||
aria-label={t('pages.clients.addClients')}
|
||||
>
|
||||
@@ -1418,20 +1423,20 @@ export default function ClientsPage() {
|
||||
? [
|
||||
{
|
||||
key: 'attach',
|
||||
icon: <UsergroupAddOutlined />,
|
||||
icon: <UsergroupAddOutlined aria-hidden />,
|
||||
label: t('pages.clients.attach'),
|
||||
onClick: () => setBulkAttachOpen(true),
|
||||
},
|
||||
{
|
||||
key: 'detach',
|
||||
icon: <UsergroupDeleteOutlined />,
|
||||
icon: <UsergroupDeleteOutlined aria-hidden />,
|
||||
label: t('pages.clients.detach'),
|
||||
danger: true,
|
||||
onClick: () => setBulkDetachOpen(true),
|
||||
},
|
||||
{
|
||||
key: 'addToGroup',
|
||||
icon: <TagsOutlined />,
|
||||
icon: <TagsOutlined aria-hidden />,
|
||||
label: t('pages.clients.addToGroup'),
|
||||
onClick: () => setBulkGroupOpen(true),
|
||||
},
|
||||
@@ -1445,26 +1450,26 @@ export default function ClientsPage() {
|
||||
{ type: 'divider' as const },
|
||||
{
|
||||
key: 'enable',
|
||||
icon: <CheckCircleOutlined />,
|
||||
icon: <CheckCircleOutlined aria-hidden />,
|
||||
label: t('pages.clients.enable'),
|
||||
onClick: () => onBulkSetEnable(true),
|
||||
},
|
||||
{
|
||||
key: 'disable',
|
||||
icon: <StopOutlined />,
|
||||
icon: <StopOutlined aria-hidden />,
|
||||
label: t('pages.clients.disable'),
|
||||
danger: true,
|
||||
onClick: () => onBulkSetEnable(false),
|
||||
},
|
||||
{
|
||||
key: 'adjust',
|
||||
icon: <ClockCircleOutlined />,
|
||||
icon: <ClockCircleOutlined aria-hidden />,
|
||||
label: t('pages.clients.adjust'),
|
||||
onClick: () => setBulkAdjustOpen(true),
|
||||
},
|
||||
{
|
||||
key: 'subLinks',
|
||||
icon: <LinkOutlined />,
|
||||
icon: <LinkOutlined aria-hidden />,
|
||||
label: t('pages.clients.subLinks'),
|
||||
onClick: () => setSubLinksOpen(true),
|
||||
},
|
||||
@@ -1472,39 +1477,39 @@ export default function ClientsPage() {
|
||||
: [
|
||||
{
|
||||
key: 'bulk',
|
||||
icon: <UsergroupAddOutlined />,
|
||||
icon: <UsergroupAddOutlined aria-hidden />,
|
||||
label: t('pages.clients.bulk'),
|
||||
onClick: () => setBulkAddOpen(true),
|
||||
},
|
||||
{
|
||||
key: 'export',
|
||||
icon: <DownloadOutlined />,
|
||||
icon: <DownloadOutlined aria-hidden />,
|
||||
label: t('pages.clients.exportClients'),
|
||||
onClick: onExportClients,
|
||||
},
|
||||
{
|
||||
key: 'import',
|
||||
icon: <UploadOutlined />,
|
||||
icon: <UploadOutlined aria-hidden />,
|
||||
label: t('pages.clients.importClients'),
|
||||
onClick: onImportClients,
|
||||
},
|
||||
{
|
||||
key: 'resetAll',
|
||||
icon: <RetweetOutlined />,
|
||||
icon: <RetweetOutlined aria-hidden />,
|
||||
label: t('pages.clients.resetAllTraffics'),
|
||||
onClick: onResetAllTraffics,
|
||||
},
|
||||
{ type: 'divider' as const },
|
||||
{
|
||||
key: 'delDepleted',
|
||||
icon: <RestOutlined />,
|
||||
icon: <RestOutlined aria-hidden />,
|
||||
label: t('pages.clients.delDepleted'),
|
||||
danger: true,
|
||||
onClick: onDelDepleted,
|
||||
},
|
||||
{
|
||||
key: 'delOrphans',
|
||||
icon: <DisconnectOutlined />,
|
||||
icon: <DisconnectOutlined aria-hidden />,
|
||||
label: t('pages.clients.delOrphans'),
|
||||
danger: true,
|
||||
onClick: onDeleteOrphans,
|
||||
@@ -1512,14 +1517,14 @@ export default function ClientsPage() {
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Button icon={<MoreOutlined />} aria-label={t('more')}>
|
||||
<Button icon={<MoreOutlined aria-hidden />} aria-label={t('more')}>
|
||||
{!isMobile && t('more')}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
{selectedRowKeys.length > 0 && (
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
icon={<DeleteOutlined aria-hidden />}
|
||||
onClick={onBulkDelete}
|
||||
style={{ marginInlineStart: 'auto' }}
|
||||
aria-label={t('delete')}
|
||||
@@ -1536,14 +1541,14 @@ export default function ClientsPage() {
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
placeholder={t('pages.clients.searchPlaceholder')}
|
||||
allowClear
|
||||
prefix={<SearchOutlined />}
|
||||
prefix={<SearchOutlined aria-hidden />}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
style={{ maxWidth: 320 }}
|
||||
aria-label={t('search')}
|
||||
/>
|
||||
<Badge count={activeCount} size="small" offset={[-4, 4]}>
|
||||
<Button
|
||||
icon={<FilterOutlined />}
|
||||
icon={<FilterOutlined aria-hidden />}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
onClick={() => setFilterDrawerOpen(true)}
|
||||
type={activeCount > 0 ? 'primary' : 'default'}
|
||||
@@ -1556,7 +1561,7 @@ export default function ClientsPage() {
|
||||
value={sortValueFor(sortColumn, sortOrder)}
|
||||
aria-label={t('sort')}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
suffix={<SortAscendingOutlined />}
|
||||
suffix={<SortAscendingOutlined aria-hidden />}
|
||||
style={{ minWidth: isMobile ? 130 : 200 }}
|
||||
onChange={(value) => {
|
||||
const opt = SORT_OPTIONS.find((o) => o.value === value);
|
||||
@@ -1709,7 +1714,10 @@ export default function ClientsPage() {
|
||||
locale={{
|
||||
emptyText: (
|
||||
<div className="clients-empty">
|
||||
<TeamOutlined style={{ fontSize: 32, marginBottom: 8 }} />
|
||||
<TeamOutlined
|
||||
aria-hidden
|
||||
style={{ fontSize: 32, marginBottom: 8 }}
|
||||
/>
|
||||
<div>{t('noData')}</div>
|
||||
</div>
|
||||
),
|
||||
@@ -1734,7 +1742,7 @@ export default function ClientsPage() {
|
||||
)}
|
||||
{filteredClients.length === 0 && (
|
||||
<div className="card-empty">
|
||||
<TeamOutlined style={{ fontSize: 28, opacity: 0.5 }} />
|
||||
<TeamOutlined aria-hidden style={{ fontSize: 28, opacity: 0.5 }} />
|
||||
<div>{t('noData')}</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1799,6 +1807,7 @@ export default function ClientsPage() {
|
||||
checked={!!row.enable}
|
||||
size="small"
|
||||
loading={togglingEmail === row.email}
|
||||
aria-label={t('pages.clients.enabled')}
|
||||
onChange={(next) => onToggleEnable(row, next)}
|
||||
/>
|
||||
<Dropdown
|
||||
@@ -1810,7 +1819,8 @@ export default function ClientsPage() {
|
||||
key: 'qr',
|
||||
label: (
|
||||
<>
|
||||
<QrcodeOutlined /> {t('pages.clients.qrCode')}
|
||||
<QrcodeOutlined aria-hidden />{' '}
|
||||
{t('pages.clients.qrCode')}
|
||||
</>
|
||||
),
|
||||
onClick: () => onShowQr(row.email),
|
||||
@@ -1819,7 +1829,7 @@ export default function ClientsPage() {
|
||||
key: 'reset',
|
||||
label: (
|
||||
<>
|
||||
<RetweetOutlined />{' '}
|
||||
<RetweetOutlined aria-hidden />{' '}
|
||||
{t('pages.inbounds.resetTraffic')}
|
||||
</>
|
||||
),
|
||||
@@ -1829,7 +1839,7 @@ export default function ClientsPage() {
|
||||
key: 'edit',
|
||||
label: (
|
||||
<>
|
||||
<EditOutlined /> {t('edit')}
|
||||
<EditOutlined aria-hidden /> {t('edit')}
|
||||
</>
|
||||
),
|
||||
onClick: () => onEdit(row.email),
|
||||
@@ -1839,7 +1849,7 @@ export default function ClientsPage() {
|
||||
danger: true,
|
||||
label: (
|
||||
<>
|
||||
<DeleteOutlined /> {t('delete')}
|
||||
<DeleteOutlined aria-hidden /> {t('delete')}
|
||||
</>
|
||||
),
|
||||
onClick: () => onDelete(row.email),
|
||||
@@ -1851,7 +1861,7 @@ export default function ClientsPage() {
|
||||
type="text"
|
||||
size="small"
|
||||
className="row-action-trigger"
|
||||
icon={<MoreOutlined />}
|
||||
icon={<MoreOutlined aria-hidden />}
|
||||
aria-label={t('more')}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useId, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
@@ -46,6 +46,7 @@ export default function FilterDrawer({
|
||||
nodes,
|
||||
}: FilterDrawerProps) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
|
||||
function patch<K extends keyof ClientFilters>(key: K, value: ClientFilters[K]) {
|
||||
onChange({ ...filters, [key]: value });
|
||||
@@ -113,8 +114,9 @@ export default function FilterDrawer({
|
||||
</Checkbox.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('pages.inbounds.protocol')}>
|
||||
<Form.Item label={t('pages.inbounds.protocol')} htmlFor={`${uid}-protocols`}>
|
||||
<Select
|
||||
id={`${uid}-protocols`}
|
||||
mode="multiple"
|
||||
value={filters.protocols}
|
||||
onChange={(v) => patch('protocols', v as string[])}
|
||||
@@ -125,8 +127,9 @@ export default function FilterDrawer({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('inbounds')}>
|
||||
<Form.Item label={t('inbounds')} htmlFor={`${uid}-inbounds`}>
|
||||
<Select
|
||||
id={`${uid}-inbounds`}
|
||||
mode="multiple"
|
||||
value={filters.inboundIds}
|
||||
onChange={(v) => patch('inboundIds', v as number[])}
|
||||
@@ -140,8 +143,9 @@ export default function FilterDrawer({
|
||||
</Form.Item>
|
||||
|
||||
{nodes.length > 0 && (
|
||||
<Form.Item label={t('pages.clients.filters.nodes')}>
|
||||
<Form.Item label={t('pages.clients.filters.nodes')} htmlFor={`${uid}-nodes`}>
|
||||
<Select
|
||||
id={`${uid}-nodes`}
|
||||
mode="multiple"
|
||||
value={filters.nodeIds}
|
||||
onChange={(v) => patch('nodeIds', v as number[])}
|
||||
@@ -155,8 +159,9 @@ export default function FilterDrawer({
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item label={t('pages.clients.group')}>
|
||||
<Form.Item label={t('pages.clients.group')} htmlFor={`${uid}-groups`}>
|
||||
<Select
|
||||
id={`${uid}-groups`}
|
||||
mode="multiple"
|
||||
value={filters.groups}
|
||||
onChange={(v) => patch('groups', v as string[])}
|
||||
@@ -169,8 +174,9 @@ export default function FilterDrawer({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('pages.clients.expiryTime')}>
|
||||
<Form.Item label={t('pages.clients.expiryTime')} htmlFor={`${uid}-expiry`}>
|
||||
<DatePicker.RangePicker
|
||||
id={`${uid}-expiry`}
|
||||
value={dateRange}
|
||||
onChange={(range) => {
|
||||
const from = range?.[0]?.startOf('day').valueOf();
|
||||
@@ -182,10 +188,11 @@ export default function FilterDrawer({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={`${t('pages.clients.traffic')} (GB)`}>
|
||||
<Form.Item label={`${t('pages.clients.traffic')} (GB)`} htmlFor={`${uid}-usage-from`}>
|
||||
<Row gutter={8}>
|
||||
<Col span={12}>
|
||||
<InputNumber
|
||||
id={`${uid}-usage-from`}
|
||||
value={filters.usageFromGB}
|
||||
min={0}
|
||||
step={1}
|
||||
@@ -196,6 +203,7 @@ export default function FilterDrawer({
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<InputNumber
|
||||
aria-label={`${t('pages.clients.traffic')} (GB) ${t('to')}`}
|
||||
value={filters.usageToGB}
|
||||
min={0}
|
||||
step={1}
|
||||
|
||||
@@ -44,7 +44,7 @@ export const ClientRowActions = memo(function ClientRowActions({
|
||||
size="small"
|
||||
type="text"
|
||||
style={ICON_BUTTON_STYLE}
|
||||
icon={<QrcodeOutlined />}
|
||||
icon={<QrcodeOutlined aria-hidden />}
|
||||
aria-label={t('pages.clients.qrCode')}
|
||||
onClick={() => onShowQr(email)}
|
||||
/>
|
||||
@@ -54,7 +54,7 @@ export const ClientRowActions = memo(function ClientRowActions({
|
||||
size="small"
|
||||
type="text"
|
||||
style={ICON_BUTTON_STYLE}
|
||||
icon={<InfoCircleOutlined />}
|
||||
icon={<InfoCircleOutlined aria-hidden />}
|
||||
aria-label={t('pages.clients.clientInfo')}
|
||||
onClick={() => onShowInfo(email)}
|
||||
/>
|
||||
@@ -64,7 +64,7 @@ export const ClientRowActions = memo(function ClientRowActions({
|
||||
size="small"
|
||||
type="text"
|
||||
style={ICON_BUTTON_STYLE}
|
||||
icon={<RetweetOutlined />}
|
||||
icon={<RetweetOutlined aria-hidden />}
|
||||
aria-label={t('pages.inbounds.resetTraffic')}
|
||||
onClick={() => onResetTraffic(email)}
|
||||
/>
|
||||
@@ -74,7 +74,7 @@ export const ClientRowActions = memo(function ClientRowActions({
|
||||
size="small"
|
||||
type="text"
|
||||
style={ICON_BUTTON_STYLE}
|
||||
icon={<EditOutlined />}
|
||||
icon={<EditOutlined aria-hidden />}
|
||||
aria-label={t('edit')}
|
||||
onClick={() => onEdit(email)}
|
||||
/>
|
||||
@@ -85,7 +85,7 @@ export const ClientRowActions = memo(function ClientRowActions({
|
||||
type="text"
|
||||
danger
|
||||
style={ICON_BUTTON_STYLE}
|
||||
icon={<DeleteOutlined />}
|
||||
icon={<DeleteOutlined aria-hidden />}
|
||||
aria-label={t('delete')}
|
||||
onClick={() => onDelete(email)}
|
||||
/>
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function SubLinksModal({
|
||||
size="small"
|
||||
type="text"
|
||||
aria-label={t('copy')}
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
onClick={() => copy(row.link, t('copied'))}
|
||||
/>
|
||||
),
|
||||
@@ -157,7 +157,7 @@ export default function SubLinksModal({
|
||||
<Button onClick={() => onOpenChange(false)}>{t('close')}</Button>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
disabled={rows.length === 0}
|
||||
onClick={() =>
|
||||
copy(allText, t('pages.clients.subLinksCopiedAll', { count: rows.length }))
|
||||
@@ -167,7 +167,7 @@ export default function SubLinksModal({
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
icon={<DownloadOutlined aria-hidden />}
|
||||
disabled={rows.length === 0}
|
||||
onClick={download}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user