功能(充值弹层): 会员周期切换支持键盘方向键
- 月付/年付 radiogroup 原先无 roving tabindex,方向键无法移动选项 - 复用 resolveRovingIndex 与 ref,方向键同步移动选中与焦点;锁定时不响应方向键 - 补一条周期切换键盘导航测试
This commit is contained in:
@@ -197,6 +197,52 @@ describe('PlatformProfileRechargeModal', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('周期切换按钮支持方向键与 roving tabindex', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<PlatformProfileRechargeModal
|
||||
center={{
|
||||
mudPointBalance: buildMudPointBalance(29),
|
||||
membership: buildNormalMembership(),
|
||||
pointProducts: buildPointProducts(),
|
||||
membershipPlans: [STARTER_PLAN, PLUS_PLAN],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
}}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
submittingProductId={null}
|
||||
nativePayment={null}
|
||||
membershipQuote={null}
|
||||
isLoadingMembershipQuote={false}
|
||||
membershipQuoteError={null}
|
||||
onClose={vi.fn()}
|
||||
onRetry={vi.fn()}
|
||||
onBuy={vi.fn()}
|
||||
onRequestMembershipQuote={vi.fn()}
|
||||
onSubmitMembershipCheckout={vi.fn()}
|
||||
onConfirmNativePayment={vi.fn()}
|
||||
onCloseNativePayment={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const group = screen.getByRole('radiogroup', { name: '付费周期' });
|
||||
const monthly = within(group).getByRole('radio', { name: '月付' });
|
||||
const yearly = within(group).getByRole('radio', { name: '年付' });
|
||||
expect(monthly.getAttribute('tabindex')).toBe('0');
|
||||
expect(yearly.getAttribute('tabindex')).toBe('-1');
|
||||
|
||||
monthly.focus();
|
||||
await user.keyboard('{ArrowRight}');
|
||||
|
||||
expect(yearly.getAttribute('aria-checked')).toBe('true');
|
||||
expect(monthly.getAttribute('aria-checked')).toBe('false');
|
||||
expect(yearly.getAttribute('tabindex')).toBe('0');
|
||||
expect(monthly.getAttribute('tabindex')).toBe('-1');
|
||||
expect(document.activeElement).toBe(yearly);
|
||||
});
|
||||
|
||||
test('requests the server quote and shows the upgrade amount', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRequestMembershipQuote = vi.fn();
|
||||
|
||||
@@ -641,6 +641,8 @@ function PlatformMembershipCycleSwitch({
|
||||
{ id: 'monthly', label: '月付' },
|
||||
{ id: 'yearly', label: '年付' },
|
||||
];
|
||||
// 与泥点档位一致:roving tabindex + 方向键移动焦点,保证键盘可切换周期。
|
||||
const optionRefs = useRef<Record<string, HTMLButtonElement | null>>({});
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
@@ -648,15 +650,41 @@ function PlatformMembershipCycleSwitch({
|
||||
role="radiogroup"
|
||||
aria-label="付费周期"
|
||||
className="platform-subpanel flex gap-1 rounded-full p-1"
|
||||
onKeyDown={(event) => {
|
||||
if (locked) {
|
||||
return;
|
||||
}
|
||||
const currentIndex = options.findIndex(
|
||||
(option) => option.id === cycleKind,
|
||||
);
|
||||
const nextIndex = resolveRovingIndex(
|
||||
event.key,
|
||||
currentIndex,
|
||||
options.length,
|
||||
);
|
||||
if (nextIndex === null) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const nextOption = options[nextIndex];
|
||||
if (nextOption) {
|
||||
onChange(nextOption.id);
|
||||
optionRefs.current[nextOption.id]?.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{options.map((option) => {
|
||||
const selected = option.id === cycleKind;
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
ref={(node) => {
|
||||
optionRefs.current[option.id] = node;
|
||||
}}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
tabIndex={selected ? 0 : -1}
|
||||
disabled={locked && !selected}
|
||||
onClick={() => onChange(option.id)}
|
||||
className={joinClassNames(
|
||||
|
||||
Reference in New Issue
Block a user