Browse Source

fix: resolve jsx-a11y/anchor-is-valid and react-doctor lint warnings

Use semantic HTML approach for different link categories:

- **Action links** (with onClick/trigger actions): replace `<a href="#">`
  with `<Button type="link">` — semantically correct for non-navigation
  interactions (settings actions, edit/delete triggers, expand/collapse)
- **Navigation links** (user names, team members): replace `<a href="#">`
  with umi `<Link to="/" prefetch>` for proper client-side routing
- **Placeholder links** (demo data): replace `href="#"` with valid href
  using `item.href` from data or `/` as fallback
- **Wrapped button**: remove invalid `<a href="#">` wrapper around `<Button>`

Also fix react-doctor/no-prevent-default by removing preventDefault calls
from replaced anchor elements, and add eslint-disable comments for
react-doctor/rendering-hydration-mismatch-time false positives where
crypto.randomUUID() is only called in event handlers.

Closes #11784

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pull/11785/head
afc163 5 months ago
parent
commit
1a660e91c6
  1. 1
      README.md
  2. 1
      README.zh-CN.md
  3. 2
      docs/cheatsheet.en-US.md
  4. 2
      docs/cheatsheet.zh-CN.md
  5. 19
      src/components/TagSelect/index.tsx
  6. 2
      src/pages/account/center/components/Projects/index.tsx
  7. 14
      src/pages/account/settings/components/binding.tsx
  8. 22
      src/pages/account/settings/components/security.tsx
  9. 2
      src/pages/chatbot/index.tsx
  10. 8
      src/pages/dashboard/workplace/index.tsx
  11. 15
      src/pages/form/advanced-form/index.tsx
  12. 15
      src/pages/list/basic-list/index.tsx
  13. 16
      src/pages/list/card-list/index.tsx
  14. 17
      src/pages/list/search/articles/index.tsx
  15. 2
      src/pages/list/search/projects/index.tsx
  16. 6
      src/pages/profile/advanced/index.tsx
  17. 12
      src/pages/result/fail/index.tsx
  18. 4
      src/pages/result/success/index.tsx
  19. 13
      src/pages/table-list/index.tsx
  20. 48
      src/pages/user/login/__snapshots__/login.test.tsx.snap
  21. 2
      src/pages/user/login/index.tsx
  22. 8
      src/pages/user/register-result/index.tsx

1
README.md

@ -10,6 +10,7 @@ An out-of-box UI solution for enterprise applications as a React boilerplate.
[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg)](https://umijs.org/)
[![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev)
[![Ant Design](https://badgen.net/badge/icon/Ant%20Design?icon=https://gw.alipayobjects.com/zos/antfincdn/Pp4WPgVDB3/KDpgvguMpGfqaHPjicRK.svg&label)](https://ant.design/)
[![React Review](https://react.review/api/badge/ant-design/ant-design-pro)](https://react.review/repo/ant-design/ant-design-pro)
Language: 🇺🇸 | [🇨🇳](./README.zh-CN.md)

1
README.zh-CN.md

@ -12,6 +12,7 @@ Language : [🇺🇸](./README.md) | 🇨🇳
[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg)](https://umijs.org/)
[![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev)
[![Ant Design](https://badgen.net/badge/icon/Ant%20Design?icon=https://gw.alipayobjects.com/zos/antfincdn/Pp4WPgVDB3/KDpgvguMpGfqaHPjicRK.svg&label)](https://ant.design/)
[![React Review](https://react.review/api/badge/ant-design/ant-design-pro)](https://react.review/repo/ant-design/ant-design-pro)
![](https://github.com/user-attachments/assets/fde29061-3d9a-4397-8ac2-397b0e033ef5)

2
docs/cheatsheet.en-US.md

@ -1,6 +1,6 @@
# Ant Design Pro Cheatsheet
[![GitHub](https://img.shields.io/badge/GitHub-ant--design%2Fant--design--pro-181717?logo=github)](https://github.com/ant-design/ant-design-pro) [![Stars](https://img.shields.io/github/stars/ant-design/ant-design-pro?style=social)](https://github.com/ant-design/ant-design-pro) [![Version](https://img.shields.io/github/package-json/v/ant-design/ant-design-pro)](https://github.com/ant-design/ant-design-pro/releases) [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
[![GitHub](https://img.shields.io/badge/GitHub-ant--design%2Fant--design--pro-181717?logo=github)](https://github.com/ant-design/ant-design-pro) [![Stars](https://img.shields.io/github/stars/ant-design/ant-design-pro?style=social)](https://github.com/ant-design/ant-design-pro) [![Version](https://img.shields.io/github/package-json/v/ant-design/ant-design-pro)](https://github.com/ant-design/ant-design-pro/releases) [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-339933?logo=node.js&logoColor=white)](https://nodejs.org/) [![React Review](https://react.review/api/badge/ant-design/ant-design-pro)](https://react.review/repo/ant-design/ant-design-pro)
![Ant Design Pro](https://mdn.alipayobjects.com/huamei_fkc4p0/afts/img/A*EX3ISYC2ghEAAAAAddAAAAgAeobDAQ/original)

2
docs/cheatsheet.zh-CN.md

@ -1,6 +1,6 @@
# Ant Design Pro Cheatsheet
[![GitHub](https://img.shields.io/badge/GitHub-ant--design%2Fant--design--pro-181717?logo=github)](https://github.com/ant-design/ant-design-pro) [![Stars](https://img.shields.io/github/stars/ant-design/ant-design-pro?style=social)](https://github.com/ant-design/ant-design-pro) [![Version](https://img.shields.io/github/package-json/v/ant-design/ant-design-pro)](https://github.com/ant-design/ant-design-pro/releases) [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
[![GitHub](https://img.shields.io/badge/GitHub-ant--design%2Fant--design--pro-181717?logo=github)](https://github.com/ant-design/ant-design-pro) [![Stars](https://img.shields.io/github/stars/ant-design/ant-design-pro?style=social)](https://github.com/ant-design/ant-design-pro) [![Version](https://img.shields.io/github/package-json/v/ant-design/ant-design-pro)](https://github.com/ant-design/ant-design-pro/releases) [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-339933?logo=node.js&logoColor=white)](https://nodejs.org/) [![React Review](https://react.review/api/badge/ant-design/ant-design-pro)](https://react.review/repo/ant-design/ant-design-pro)
![Ant Design Pro](https://mdn.alipayobjects.com/huamei_fkc4p0/afts/img/A*EX3ISYC2ghEAAAAAddAAAAgAeobDAQ/original)

19
src/components/TagSelect/index.tsx

@ -1,6 +1,6 @@
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import { useMergedState } from '@rc-component/util';
import { Tag } from 'antd';
import { Button, Tag } from 'antd';
import { clsx } from 'clsx';
import React, { type FC, useMemo, useState } from 'react';
import useStyles from './index.style';
@ -136,19 +136,10 @@ const TagSelect: FC<TagSelectProps> & {
return child;
})}
{expandable && (
<a
<Button
type="link"
className={styles.trigger}
href="#"
onClick={(e) => {
e.preventDefault();
setExpand(!expand);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
setExpand(!expand);
}
}}
onClick={() => setExpand(!expand)}
>
{expand ? (
<>
@ -160,7 +151,7 @@ const TagSelect: FC<TagSelectProps> & {
<DownOutlined />
</>
)}
</a>
</Button>
)}
</div>
);

2
src/pages/account/center/components/Projects/index.tsx

@ -36,7 +36,7 @@ const Projects: React.FC = () => {
cover={<img alt={item.title} src={item.cover} />}
>
<Card.Meta
title={<a href="#">{item.title}</a>}
title={<a href={item.href}>{item.title}</a>}
description={item.subDescription}
/>
<div className={styles.cardItemContent}>

14
src/pages/account/settings/components/binding.tsx

@ -3,7 +3,7 @@ import {
DingdingOutlined,
TaobaoOutlined,
} from '@ant-design/icons';
import { List } from 'antd';
import { Button, List } from 'antd';
import React from 'react';
const BindingView: React.FC = () => {
@ -12,9 +12,9 @@ const BindingView: React.FC = () => {
title: '绑定淘宝',
description: '当前未绑定淘宝账号',
actions: [
<a key="Bind" href="#">
<Button key="Bind" type="link">
绑定
</a>,
</Button>,
],
avatar: <TaobaoOutlined className="taobao" />,
},
@ -22,9 +22,9 @@ const BindingView: React.FC = () => {
title: '绑定支付宝',
description: '当前未绑定支付宝账号',
actions: [
<a key="Bind" href="#">
<Button key="Bind" type="link">
绑定
</a>,
</Button>,
],
avatar: <AlipayOutlined className="alipay" />,
},
@ -32,9 +32,9 @@ const BindingView: React.FC = () => {
title: '绑定钉钉',
description: '当前未绑定钉钉账号',
actions: [
<a key="Bind" href="#">
<Button key="Bind" type="link">
绑定
</a>,
</Button>,
],
avatar: <DingdingOutlined className="dingding" />,
},

22
src/pages/account/settings/components/security.tsx

@ -1,4 +1,4 @@
import { List } from 'antd';
import { Button, List } from 'antd';
import React from 'react';
type Unpacked<T> = T extends (infer U)[] ? U : T;
@ -20,45 +20,45 @@ const SecurityView: React.FC = () => {
</>
),
actions: [
<a key="Modify" href="#">
<Button key="Modify" type="link">
修改
</a>,
</Button>,
],
},
{
title: '密保手机',
description: `已绑定手机:138****8293`,
actions: [
<a key="Modify" href="#">
<Button key="Modify" type="link">
修改
</a>,
</Button>,
],
},
{
title: '密保问题',
description: '未设置密保问题,密保问题可有效保护账户安全',
actions: [
<a key="Set" href="#">
<Button key="Set" type="link">
设置
</a>,
</Button>,
],
},
{
title: '备用邮箱',
description: `已绑定邮箱:ant***sign.com`,
actions: [
<a key="Modify" href="#">
<Button key="Modify" type="link">
修改
</a>,
</Button>,
],
},
{
title: 'MFA 设备',
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
actions: [
<a key="bind" href="#">
<Button key="bind" type="link">
绑定
</a>,
</Button>,
],
},
];

2
src/pages/chatbot/index.tsx

@ -180,6 +180,7 @@ const ChatbotPage: React.FC = () => {
};
const newChat = () => {
// eslint-disable-next-line react-doctor/rendering-hydration-mismatch-time -- only called in event handlers, not during render
const key = crypto.randomUUID();
setConversations((prev) => [
{ key, label: '新对话', group: '今天', isDraft: true },
@ -261,6 +262,7 @@ const ChatbotPage: React.FC = () => {
(c) => c.key !== conversation.key,
);
if (next.length === 0) {
// eslint-disable-next-line react-doctor/rendering-hydration-mismatch-time -- only called in event handlers, not during render
const key = crypto.randomUUID();
next.push({
key,

8
src/pages/dashboard/workplace/index.tsx

@ -118,9 +118,9 @@ const Workplace: FC = () => {
avatar={<Avatar src={item.user.avatar} />}
title={
<span>
<a className={styles.username} href="#">
<Link className={styles.username} to="/" prefetch>
{item.user.name}
</a>
</Link>
&nbsp;
<span className={styles.event}>{events}</span>
</span>
@ -281,12 +281,12 @@ const Workplace: FC = () => {
{projectNotice.map((item) => {
return (
<Col span={12} key={`members-item-${item.id}`}>
<a href="#">
<Link to="/" prefetch>
<Avatar src={item.logo} size="small" />
<span className={styles.member}>
{item.member.substring(0, 3)}
</span>
</a>
</Link>
</Col>
);
})}

15
src/pages/form/advanced-form/index.tsx

@ -10,7 +10,7 @@ import {
ProFormText,
ProFormTimePicker,
} from '@ant-design/pro-components';
import { Card, Col, message, Popover, Row } from 'antd';
import { Button, Card, Col, message, Popover, Row } from 'antd';
import type { FC } from 'react';
import { useRef, useState } from 'react';
import { fakeSubmitForm } from './service';
@ -161,16 +161,13 @@ const AdvancedForm: FC<Record<string, any>> = () => {
valueType: 'option',
render: (_, record: TableFormDateType, _index, action) => {
return [
<a
key="eidit"
href="#"
onClick={(e) => {
e.preventDefault();
action?.startEditable(record.key);
}}
<Button
key="edit"
type="link"
onClick={() => action?.startEditable(record.key)}
>
编辑
</a>,
</Button>,
];
},
},

15
src/pages/list/basic-list/index.tsx

@ -171,9 +171,9 @@ const BasicList: FC = () => {
],
}}
>
<a href="#">
<Button type="link">
更多 <DownOutlined />
</a>
</Button>
</Dropdown>
);
};
@ -229,16 +229,13 @@ const BasicList: FC = () => {
renderItem={(item) => (
<List.Item
actions={[
<a
<Button
key="edit"
href="#"
onClick={(e) => {
e.preventDefault();
showEditModal(item);
}}
type="link"
onClick={() => showEditModal(item)}
>
编辑
</a>,
</Button>,
renderMoreBtn(item),
]}
>

16
src/pages/list/card-list/index.tsx

@ -22,21 +22,21 @@ const CardList = () => {
提供跨越设计与开发的体验解决方案。
</p>
<div className={styles.contentLink}>
<a href="#">
<a href="/">
<img
alt=""
src="https://gw.alipayobjects.com/zos/rmsportal/MjEImQtenlyueSmVEfUD.svg"
/>{' '}
快速开始
</a>
<a href="#">
<a href="/">
<img
alt=""
src="https://gw.alipayobjects.com/zos/rmsportal/NbuDUAuBlIApFuDvWiND.svg"
/>{' '}
产品简介
</a>
<a href="#">
<a href="/">
<img
alt=""
src="https://gw.alipayobjects.com/zos/rmsportal/ohOEPSYdDTNnyMbGuyLb.svg"
@ -79,12 +79,12 @@ const CardList = () => {
hoverable
className={styles.card}
actions={[
<a key="option1" href="#">
<Button type="link" key="option1">
操作一
</a>,
<a key="option2" href="#">
</Button>,
<Button type="link" key="option2">
操作二
</a>,
</Button>,
]}
>
<Card.Meta
@ -95,7 +95,7 @@ const CardList = () => {
src={item.avatar}
/>
}
title={<a href="#">{item.title}</a>}
title={<a href={item.href}>{item.title}</a>}
description={
<Paragraph
className={styles.item}

17
src/pages/list/search/articles/index.tsx

@ -183,22 +183,13 @@ const Articles: FC = () => {
options={ownerOptions}
/>
</FormItem>
<a
<Button
type="link"
className={styles.selfTrigger}
href="#"
onClick={(e) => {
e.preventDefault();
setOwner();
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
setOwner();
}
}}
onClick={() => setOwner()}
>
只看自己的
</a>
</Button>
</StandardFormRow>
<StandardFormRow title="其它选项" grid last>
<Row gutter={16}>

2
src/pages/list/search/projects/index.tsx

@ -50,7 +50,7 @@ const Projects: FC = () => {
cover={<img alt={item.title} src={item.cover} />}
>
<Card.Meta
title={<a href="#">{item.title}</a>}
title={<a href={item.href}>{item.title}</a>}
description={
<Paragraph
ellipsis={{

6
src/pages/profile/advanced/index.tsx

@ -150,7 +150,7 @@ const descriptionItems: DescriptionsProps['items'] = [
{ key: '1', label: '创建人', children: '曲丽丽' },
{ key: '2', label: '订购产品', children: 'XX 服务' },
{ key: '3', label: '创建时间', children: '2017-07-07' },
{ key: '4', label: '关联单据', children: <a href="#">12421</a> },
{ key: '4', label: '关联单据', children: <a href="/">12421</a> },
{ key: '5', label: '生效日期', children: '2017-07-07 ~ 2017-08-08' },
{ key: '6', label: '备注', children: '请于两个工作日内确认' },
];
@ -305,7 +305,9 @@ const Advanced: FC = () => {
}}
/>
<div>
<a href="#">催一下</a>
<Button type="link" size="small">
催一下
</Button>
</div>
</div>
);

12
src/pages/result/fail/index.tsx

@ -22,15 +22,15 @@ export default () => {
className={styles.error_icon}
/>
<span>您的账户已被冻结</span>
<a
href="#"
<Button
type="link"
style={{
marginLeft: 16,
}}
>
<span>立即解冻</span>
<RightOutlined />
</a>
</Button>
</div>
<div>
<CloseCircleOutlined
@ -40,15 +40,15 @@ export default () => {
className={styles.error_icon}
/>
<span>您的账户还不具备申请资格</span>
<a
href="#"
<Button
type="link"
style={{
marginLeft: 16,
}}
>
<span>立即升级</span>
<RightOutlined />
</a>
</Button>
</div>
</>
);

4
src/pages/result/success/index.tsx

@ -51,7 +51,7 @@ const Success: React.FC = () => {
}}
>
<span>周毛毛</span>
<a href="#">
<Button type="link">
<DingdingOutlined
style={{
color: '#00A0E9',
@ -59,7 +59,7 @@ const Success: React.FC = () => {
}}
/>
<span>催一下</span>
</a>
</Button>
</div>
</div>
);

13
src/pages/table-list/index.tsx

@ -58,16 +58,15 @@ const TableList: React.FC = () => {
dataIndex: 'name',
render: (dom, entity) => {
return (
<a
href="#"
onClick={(e) => {
e.preventDefault();
<Button
type="link"
onClick={() => {
setCurrentRow(entity);
setShowDetail(true);
}}
>
{dom}
</a>
</Button>
);
},
},
@ -197,12 +196,12 @@ const TableList: React.FC = () => {
render: (_, record) => [
<UpdateForm
trigger={
<a href="#">
<Button type="link">
<FormattedMessage
id="pages.searchTable.config"
defaultMessage="Configuration"
/>
</a>
</Button>
}
key="config"
onOk={actionRef.current?.reload}

48
src/pages/user/login/__snapshots__/login.test.tsx.snap

@ -884,27 +884,35 @@ exports[`Login Page should show login form 1`] = `
class="ant-input-suffix"
>
<span
aria-label="eye-invisible"
class="anticon anticon-eye-invisible ant-input-password-icon"
role="img"
tabindex="-1"
aria-disabled="false"
aria-label="Show"
aria-pressed="false"
class="ant-input-password-icon"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
data-icon="eye-invisible"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="eye-invisible"
class="anticon anticon-eye-invisible"
role="img"
>
<path
d="M942.2 486.2Q889.47 375.11 816.7 305l-50.88 50.88C807.31 395.53 843.45 447.4 874.7 512 791.5 684.2 673.4 766 512 766q-72.67 0-133.87-22.38L323 798.75Q408 838 512 838q288.3 0 430.2-300.3a60.29 60.29 0 000-51.5zm-63.57-320.64L836 122.88a8 8 0 00-11.32 0L715.31 232.2Q624.86 186 512 186q-288.3 0-430.2 300.3a60.3 60.3 0 000 51.5q56.69 119.4 136.5 191.41L112.48 835a8 8 0 000 11.31L155.17 889a8 8 0 0011.31 0l712.15-712.12a8 8 0 000-11.32zM149.3 512C232.6 339.8 350.7 258 512 258c54.54 0 104.13 9.36 149.12 28.39l-70.3 70.3a176 176 0 00-238.13 238.13l-83.42 83.42C223.1 637.49 183.3 582.28 149.3 512zm246.7 0a112.11 112.11 0 01146.2-106.69L401.31 546.2A112 112 0 01396 512z"
/>
<path
d="M508 624c-3.46 0-6.87-.16-10.25-.47l-52.82 52.82a176.09 176.09 0 00227.42-227.42l-52.82 52.82c.31 3.38.47 6.79.47 10.25a111.94 111.94 0 01-112 112z"
/>
</svg>
<svg
aria-hidden="true"
data-icon="eye-invisible"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M942.2 486.2Q889.47 375.11 816.7 305l-50.88 50.88C807.31 395.53 843.45 447.4 874.7 512 791.5 684.2 673.4 766 512 766q-72.67 0-133.87-22.38L323 798.75Q408 838 512 838q288.3 0 430.2-300.3a60.29 60.29 0 000-51.5zm-63.57-320.64L836 122.88a8 8 0 00-11.32 0L715.31 232.2Q624.86 186 512 186q-288.3 0-430.2 300.3a60.3 60.3 0 000 51.5q56.69 119.4 136.5 191.41L112.48 835a8 8 0 000 11.31L155.17 889a8 8 0 0011.31 0l712.15-712.12a8 8 0 000-11.32zM149.3 512C232.6 339.8 350.7 258 512 258c54.54 0 104.13 9.36 149.12 28.39l-70.3 70.3a176 176 0 00-238.13 238.13l-83.42 83.42C223.1 637.49 183.3 582.28 149.3 512zm246.7 0a112.11 112.11 0 01146.2-106.69L401.31 546.2A112 112 0 01396 512z"
/>
<path
d="M508 624c-3.46 0-6.87-.16-10.25-.47l-52.82 52.82a176.09 176.09 0 00227.42-227.42l-52.82 52.82c.31 3.38.47 6.79.47 10.25a111.94 111.94 0 01-112 112z"
/>
</svg>
</span>
</span>
</span>
</span>
@ -948,7 +956,7 @@ exports[`Login Page should show login form 1`] = `
type="button"
>
<span>
Login
登 录
</span>
</button>
</form>

2
src/pages/user/login/index.tsx

@ -398,7 +398,7 @@ const Login: React.FC = () => {
/>
</ProFormCheckbox>
<a
href="#"
href="/"
style={{
float: 'right',
}}

8
src/pages/user/register-result/index.tsx

@ -9,11 +9,9 @@ const RegisterResult: React.FC<Record<string, unknown>> = () => {
const actions = (
<div className={styles.actions}>
<a href="#">
<Button size="large" type="primary">
<span>查看邮箱</span>
</Button>
</a>
<Button size="large" type="primary">
<span>查看邮箱</span>
</Button>
<Link to="/" prefetch>
<Button size="large">返回首页</Button>
</Link>

Loading…
Cancel
Save