Files
3x-ui/frontend/src/pages/xray/outbounds/TestResultPopover.tsx
T
Alireza Ahmadi e2184777a0 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.
2026-10-23 18:29:47 +02:00

89 lines
2.9 KiB
TypeScript

import type { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { Popover } from 'antd';
import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';
import type { OutboundTestResult } from '@/hooks/useXraySetting';
import { testModeLabel } from './outbounds-tab-helpers';
interface TestResultPopoverProps {
result: OutboundTestResult;
// Custom trigger element; defaults to the ok/fail latency pill.
children?: ReactNode;
}
// Latency pill + detail popover for an outbound test result: per-endpoint
// dial outcomes for TCP probes, HTTP status and the timing breakdown for
// HTTP probes.
export default function TestResultPopover({ result: r, children }: TestResultPopoverProps) {
const { t } = useTranslation();
const breakdown: Array<{ key: string; label: string; value: string }> = [];
if (typeof r.httpStatus === 'number') {
breakdown.push({
key: 'status',
label: t('pages.xray.outbound.httpStatus'),
value: String(r.httpStatus),
});
}
if (typeof r.connectMs === 'number') {
breakdown.push({
key: 'connect',
label: t('pages.xray.outbound.breakdownConnect'),
value: `${r.connectMs} ms`,
});
}
if (typeof r.tlsMs === 'number') {
breakdown.push({
key: 'tls',
label: t('pages.xray.outbound.breakdownTls'),
value: `${r.tlsMs} ms`,
});
}
if (typeof r.ttfbMs === 'number') {
breakdown.push({
key: 'ttfb',
label: t('pages.xray.outbound.breakdownTtfb'),
value: `${r.ttfbMs} ms`,
});
}
return (
<Popover
placement="topLeft"
rootClassName="outbound-test-popover"
content={
<div className="timing-breakdown">
<div className={`td-head ${r.success ? 'ok' : 'fail'}`}>
{r.success ? <span>{r.delay} ms</span> : <span>{r.error || 'failed'}</span>}
{r.mode && <span className="mode-badge">{testModeLabel(String(r.mode), t)}</span>}
</div>
{(r.endpoints || []).map((ep) => (
<div key={ep.address} className="endpoint-row">
<span className={ep.success ? 'dot-ok' : 'dot-fail'}>●</span>
<span className="ep-addr">{ep.address}</span>
<span className="ep-meta">
{ep.success ? `${ep.delay} ms` : ep.error || 'failed'}
</span>
</div>
))}
{breakdown.map((row) => (
<div key={row.key} className="breakdown-row">
<span className="bd-label">{row.label}</span>
<span className="bd-value">{row.value}</span>
</div>
))}
</div>
}
>
{children ?? (
<span className={r.success ? 'pill-ok' : 'pill-fail'}>
{r.success ? <CheckCircleFilled aria-hidden /> : <CloseCircleFilled aria-hidden />}
{r.success ? <span>{r.delay}&nbsp;ms</span> : <span>failed</span>}
</span>
)}
</Popover>
);
}