mirror of
https://github.com/ant-design/ant-design.git
synced 2025-08-06 16:06:28 +08:00
commit
103f82b6db
80
.cursor/rules/demo.mdc
Normal file
80
.cursor/rules/demo.mdc
Normal file
@ -0,0 +1,80 @@
|
||||
---
|
||||
description:
|
||||
globs: components/*/demo/**
|
||||
alwaysApply: false
|
||||
---
|
||||
# Demo 规范
|
||||
|
||||
- demo 代码尽可能简洁
|
||||
- 避免冗余代码,方便用户复制到项目直接使用
|
||||
- 每个 demo 聚焦展示一个功能点
|
||||
- 提供中英文两个版本的说明
|
||||
- demo 文件扩展名:
|
||||
- 基础 demo:.tsx
|
||||
- markdown 说明:.md
|
||||
- 遵循展示优先原则,确保视觉效果良好
|
||||
- 展示组件的主要使用场景
|
||||
- 按照由简到繁的顺序排列 demo
|
||||
- 确保 demo 在各种尺寸下都能正常展示
|
||||
- 对于复杂交互提供必要的操作说明
|
||||
|
||||
## 文件组织
|
||||
|
||||
- 每个组件演示包含 `.md`(说明文档)和 `.tsx`(实际代码)两个文件
|
||||
- 位置:组件目录下的 `demo` 子目录,如 `components/button/demo/`
|
||||
- 命名:短横线连接的小写英文单词,如 `basic.tsx`、`custom-filter.tsx`
|
||||
- 文件名应简洁地描述示例内容
|
||||
|
||||
## MD 文档规范
|
||||
|
||||
- 必须包含 `## zh-CN` 和 `## en-US` 两种语言说明
|
||||
- 内容简洁明了,突出组件特性和用法
|
||||
- 避免冗长段落,必要时使用列表或粗体
|
||||
- 标注注意事项和实验性功能
|
||||
|
||||
## TSX 代码规范
|
||||
|
||||
- 导入顺序:React → 依赖库 → 组件库 → 自定义组件 → 类型 → 样式
|
||||
- 类型:为复杂数据定义清晰接口,避免 `any`
|
||||
- 结构:
|
||||
- 使用函数式组件和 Hooks
|
||||
- 复杂逻辑提取为独立函数
|
||||
- 状态管理用 `useState`/`useEffect`
|
||||
- 风格:
|
||||
- 2空格缩进,箭头函数,驼峰命名
|
||||
- 常量大写+下划线,布尔props用`is`/`has`前缀
|
||||
|
||||
## 示例类型
|
||||
|
||||
1. **基础用法**:展示最简用法,置于首位,代码精简
|
||||
2. **变体展示**:不同大小、类型、状态,合理分组
|
||||
3. **交互演示**:展示状态变化和用户交互
|
||||
4. **数据交互**:展示加载、过滤、排序等
|
||||
5. **边界情况**:空数据、错误状态处理
|
||||
|
||||
## 代码质量
|
||||
|
||||
- 实用且专注于单一功能
|
||||
- 关键处添加简洁注释
|
||||
- 使用有意义的数据和变量
|
||||
- 优先使用 antd 内置组件,减少外部依赖
|
||||
- 性能优化:适当使用 `useMemo`/`useCallback`,清理副作用
|
||||
|
||||
## 命名规则
|
||||
|
||||
- 基础文件:`basic.tsx`、`controlled.tsx` 等
|
||||
- 调试类:使用 `debug-` 前缀
|
||||
- 实验性:使用 `experimental-` 前缀
|
||||
|
||||
## 特殊示例
|
||||
|
||||
- **调试示例**:用于开发测试,通常不在文档显示
|
||||
- **无障碍**:展示标签关联和键盘导航
|
||||
- **响应式**:展示不同视口下的行为
|
||||
- **多语言**:展示国际化配置方法
|
||||
|
||||
## 质量要求
|
||||
|
||||
- 确保代码运行正常,无控制台错误
|
||||
- 适配常见浏览器
|
||||
- 避免过时 API,及时更新到新推荐用法
|
60
.cursor/rules/docs.mdc
Normal file
60
.cursor/rules/docs.mdc
Normal file
@ -0,0 +1,60 @@
|
||||
---
|
||||
description: 规范项目文档和 Changelog
|
||||
globs: ["**/CHANGELOG*.md", "components/**/index.*.md"]
|
||||
alwaysApply: false
|
||||
---
|
||||
# 文档和 Changelog 规范
|
||||
|
||||
## 基本要求
|
||||
|
||||
- 提供中英文两个版本
|
||||
- 新的属性需要声明可用的版本号
|
||||
- 属性命名符合 antd 的 API 命名规则:https://github.com/ant-design/ant-design/wiki/API-Naming-rules
|
||||
|
||||
## Changelog 规范
|
||||
|
||||
- 在 CHANGELOG.en-US.md 和 CHANGELOG.zh-CN.md 书写每个版本的变更
|
||||
- 对用户使用上无感知的改动建议(文档修补、微小的样式优化、代码风格重构等等)不要提及,保持 CHANGELOG 的内容有效性
|
||||
- 非 antd 组件的改动(如工程化、构建工具、开发流程等)在 changelog 区块中写 "-"
|
||||
- 用面向开发者的角度和叙述方式撰写 CHANGELOG,不描述修复细节,描述问题和对开发者的影响;描述用户的原始问题,而非你的解决方式
|
||||
|
||||
### 示例
|
||||
|
||||
- 不好的例子:修复组件 Typography 的 dom 结构问题
|
||||
- 好的例子:修复了 List.Item 中内容空格丢失的问题
|
||||
|
||||
### 其他要求
|
||||
|
||||
- 新增属性时,建议用易于理解的语言补充描述用户可以感知的变化
|
||||
- 尽量给出原始的 PR 链接,社区提交的 PR 改动加上提交者的链接
|
||||
- 底层模块升级中间版本要去 rc-component 里找到改动,给出变动说明
|
||||
- 建议参考之前版本的日志写法
|
||||
- 将同组件的改动放在一起,内容子级缩进
|
||||
|
||||
### Changelog Emoji 规范
|
||||
|
||||
- 🐞 Bug 修复
|
||||
- 💄 样式更新或 token 更新
|
||||
- 🆕 新增特性,新增属性
|
||||
- 🔥 极其值得关注的新增特性
|
||||
- 🇺🇸🇨🇳🇬🇧 国际化改动
|
||||
- 📖 📝 文档或网站改进
|
||||
- ✅ 新增或更新测试用例
|
||||
- 🛎 更新警告/提示信息
|
||||
- ⌨️ ♿ 可访问性增强
|
||||
- 🗑 废弃或移除
|
||||
- 🛠 重构或工具链优化
|
||||
- ⚡️ 性能提升
|
||||
|
||||
# 文档规范
|
||||
|
||||
- 提供中英文两个版本
|
||||
- 新属性需声明可用的版本号
|
||||
- 属性命名符合 API 命名规则
|
||||
- 组件文档包含:使用场景、基础用法、API 说明
|
||||
- 文档示例应简洁明了
|
||||
- 属性的描述应清晰易懂
|
||||
- 对复杂功能提供详细说明
|
||||
- 加入 TypeScript 定义
|
||||
- 提供常见问题解答
|
||||
- 更新文档时同步更新中英文版本
|
128
.cursor/rules/git.mdc
Normal file
128
.cursor/rules/git.mdc
Normal file
@ -0,0 +1,128 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Git 规范
|
||||
|
||||
## 分支管理
|
||||
|
||||
- 禁止直接提交到以下保护分支:
|
||||
- `master`:主分支,用于发布
|
||||
- `feature`:特性分支,用于开发新版本
|
||||
- `next`:下一个版本分支
|
||||
|
||||
## 开发流程
|
||||
|
||||
1. 从保护分支(通常是 `master`)创建新的功能分支
|
||||
2. 在新分支上进行开发
|
||||
3. 提交 Pull Request 到目标分支
|
||||
4. 等待 Code Review 和 CI 通过
|
||||
5. 合并到目标分支
|
||||
|
||||
## 分支命名规范
|
||||
|
||||
- 功能开发:`feat/description-of-feature`
|
||||
- 例如:`feat/add-dark-mode`
|
||||
- 例如:`feat/improve-table-performance`
|
||||
- 问题修复:`fix/issue-number-or-description`
|
||||
- 例如:`fix/button-style-issue`
|
||||
- 例如:`fix/issue-1234`
|
||||
- 文档更新:`docs/what-is-changed`
|
||||
- 例如:`docs/update-api-reference`
|
||||
- 例如:`docs/fix-typos`
|
||||
- 代码重构:`refactor/what-is-changed`
|
||||
- 例如:`refactor/button-component`
|
||||
- 例如:`refactor/remove-deprecated-api`
|
||||
- 样式修改:`style/what-is-changed`
|
||||
- 例如:`style/update-button-tokens`
|
||||
- 例如:`style/improve-mobile-layout`
|
||||
- 测试相关:`test/what-is-changed`
|
||||
- 例如:`test/add-button-test`
|
||||
- 例如:`test/improve-coverage`
|
||||
- 构建相关:`build/what-is-changed`
|
||||
- 例如:`build/upgrade-webpack`
|
||||
- 例如:`build/fix-ts-config`
|
||||
- 持续集成:`ci/what-is-changed`
|
||||
- 例如:`ci/add-e2e-test`
|
||||
- 例如:`ci/fix-deploy-script`
|
||||
- 性能优化:`perf/what-is-changed`
|
||||
- 例如:`perf/optimize-render`
|
||||
- 例如:`perf/reduce-bundle-size`
|
||||
- 依赖升级:`deps/package-name-version`
|
||||
- 例如:`deps/upgrade-react-18`
|
||||
- 例如:`deps/update-dependencies`
|
||||
|
||||
## 分支命名注意事项
|
||||
|
||||
1. 使用小写字母
|
||||
2. 使用连字符(-)分隔单词
|
||||
3. 简短但具有描述性
|
||||
4. 避免使用下划线或其他特殊字符
|
||||
5. 如果与 Issue 关联,可以包含 Issue 编号
|
||||
|
||||
## Pull Request 规范
|
||||
|
||||
### PR 标题
|
||||
|
||||
- PR 标题始终使用英文
|
||||
- 遵循格式:`类型: 简短描述`
|
||||
- 例如:`fix: fix button style issues in Safari browser`
|
||||
- 例如:`feat: add dark mode support`
|
||||
|
||||
### PR 内容
|
||||
|
||||
- PR 内容默认使用英文
|
||||
- 尽量简洁清晰地描述改动内容和目的
|
||||
- 可以视需要在英文描述后附上中文说明
|
||||
|
||||
### PR 模板
|
||||
|
||||
提交 PR 时请使用项目中提供的模板:
|
||||
|
||||
- 英文模板(推荐):[PULL_REQUEST_TEMPLATE.md](mdc:.github/PULL_REQUEST_TEMPLATE.md)
|
||||
- 中文模板:[PULL_REQUEST_TEMPLATE_CN.md](mdc:.github/PULL_REQUEST_TEMPLATE_CN.md)
|
||||
|
||||
### PR 提交注意事项
|
||||
|
||||
1. **合并策略**:
|
||||
|
||||
- 新特性请提交至 `feature` 分支
|
||||
- 其余可提交至 `master` 分支
|
||||
|
||||
2. **审核流程**:
|
||||
|
||||
- PR 需要由至少一名维护者审核通过后才能合并
|
||||
- 确保所有 CI 检查都通过
|
||||
- 解决所有 Code Review 中提出的问题
|
||||
|
||||
3. **PR 质量要求**:
|
||||
|
||||
- 确保代码符合项目代码风格
|
||||
- 添加必要的测试用例
|
||||
- 更新相关文档
|
||||
- 大型改动需要更详细的说明和更多的审核者参与
|
||||
|
||||
4. **工具标注**:
|
||||
- 如果是用 Cursor 提交的代码,请在 PR body 末尾进行标注:`> Submitted by Cursor`
|
||||
|
||||
## 新增内容
|
||||
|
||||
- Pull Request 标题格式:[组件名]: 描述
|
||||
- 从 master 分支创建新分支
|
||||
- 分支命名规范:
|
||||
- feature/xxx:新特性
|
||||
- fix/xxx:Bug 修复
|
||||
- docs/xxx:文档更新
|
||||
- PR 说明中选择改动类型:
|
||||
- 🆕 新特性提交
|
||||
- 🐞 Bug 修复
|
||||
- 📝 文档改进
|
||||
- 📽️ 演示代码改进
|
||||
- 💄 样式/交互改进
|
||||
- 🤖 TypeScript 更新
|
||||
- 📦 包体积优化
|
||||
- ⚡️ 性能优化
|
||||
- 🌐 国际化改进
|
||||
- 提供改动背景和解决方案
|
||||
- 更新日志同时提供英文和中文版本
|
108
.cursor/rules/naming.mdc
Normal file
108
.cursor/rules/naming.mdc
Normal file
@ -0,0 +1,108 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
Basically, antd naming requires **FULL NAME** instead of Abbreviation.
|
||||
|
||||
## Props
|
||||
* Initialize prop: `default` + `PropName`
|
||||
* Force render: `forceRender`
|
||||
* Force render sub component: `force` + `Sub Component Name` + `Render`
|
||||
* Sub Component Render: `Sub Component Name` + `Render`. e.g.
|
||||
> panelRender(originNode, info: { SubComponent1, SubComponent2, [somePassedProps]: someValue })
|
||||
* Sub Item Render: `Sub Item Name` + `Render`. e.g.
|
||||
> cellRender(date, info: { [somePassedProps]: someValue })
|
||||
* Data Source: `dataSource`
|
||||
* Panel open: popup & dropdown `open`, additional popup `popupName` + `Open` like `tooltipOpen`
|
||||
* Do not use `visible` to make sure all the visible api align
|
||||
* `children`:
|
||||
* Mainly display content. To avoid additional prop name.
|
||||
* Option list like `Select.Option` or `Tree.TreeNode`.
|
||||
* Customize wrapped component can consider use `component` prop if `children` may have other usage in future.
|
||||
* Display related naming: `show` + `PropName`
|
||||
* Functional: `PropName` + `able`
|
||||
* Disable: `disabled`
|
||||
* sub component: `disabled` + `Sub Component Name`
|
||||
* Extra: `extra`
|
||||
* sub component: `Sub Component Name` + `extra`. e.g. `titleExtra`
|
||||
* mainly icon: `icon`
|
||||
* Merge with function first: `functionName: { icon }`. e.g. `expandable: { icon: <Smile /> }`
|
||||
* Multiple icons: `FunctionName` + `Icon`
|
||||
* Trigger: `trigger`
|
||||
* Sub function trigger: `Sub Function` + `Trigger`
|
||||
* Trigger on the time point: `xxx` + `On` + `EventName` (e.g. `destroyOnClose`)
|
||||
* Component use other component config. Naming as component.(e.g. `<Table pagination={{...}} />`)
|
||||
* ClassName: `className`
|
||||
* Additional classes should be merged into `classes` (e.g. `<Button classes={{ inner: 'custom-inner' }} />`)
|
||||
* Format
|
||||
* Not modify value when blur: `preserveInvalidOnBlur`
|
||||
|
||||
## Event
|
||||
* Trigger event: `on` + `EventName`
|
||||
* Trigger sub component event: `on` + `SubComponentName` + `EventName` (e.g.`onSearchChange`)
|
||||
* Trigger prop event: `on` + `PropName` + `EventName` (e.g.`onDragStart`)
|
||||
* Before trigger event: `before` + `EventName`
|
||||
* After trigger event: `after` + `EventName`
|
||||
* After continuous action, such as drag Slider: `on` + `EventName` + `Complete`
|
||||
|
||||
## Reference
|
||||
|
||||
Component should have `ref` prop. Which should provide the structure:
|
||||
|
||||
```tsx
|
||||
ComponentRef {
|
||||
nativeElement: HTMLElement;
|
||||
// Other function
|
||||
focus: VoidFunction;
|
||||
}
|
||||
```
|
||||
|
||||
## Component Token
|
||||
`variant (optional)` + `semantic part` + `semantic part variant (optional)` + `css property` + `size/disabled (optional)`
|
||||
|
||||
All component tokens should follow the structure above, and should not conflict with Global Token.
|
||||
* `variant` means this token only works in certain variant, like `horizontal` `borderless`.
|
||||
* `semantic part` means the typical element of component, like `item` `header`. If there's.
|
||||
* `semantic part status` means the variant of the semantic part before it, like `hover` `selected`.
|
||||
* `css property` means the exact property where the token is used, like `fontSize` `width`.
|
||||
|
||||
For example:
|
||||
| v4 | v5 | Note |
|
||||
| --- | --- | --- |
|
||||
| `@menu-item-color` | `itemColor` | Remove the component prefix |
|
||||
| `@select-item-selected-bg` | `itemSelectedBg` | `selected` is variant of item |
|
||||
| `@select-single-item-height-lg` | `itemHeightLG` | `single` is variant of Select (by default), `LG` is size of Select |
|
||||
|
||||
> Note: If there's no semantic part for one component token, for example, the root borderRadius of Button, it is not suitable to add it. Because we could modify it easily with `className` and `style`.
|
||||
|
||||
## Current listing api & Chinese version
|
||||
ref: [#16048](mdc:https:/github.com/ant-design/ant-design/issues/16048)
|
||||
|
||||
## API standard in the document
|
||||
|
||||
### Examples
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| --------- | ---------------------- | ------------------------------ | ------ |
|
||||
| htmlType | xxx | string | `button ` |
|
||||
| type | xxx | `horizontal ` \| `vertical ` | `horizontal` |
|
||||
| disabled | xxx | boolean | false |
|
||||
| minLength | xxx | number | 0 |
|
||||
| style | xxx | CSSProperties | - |
|
||||
| character | xxx | (props) => ReactNode | - |
|
||||
| offset | xxx| \[number, number] | \[0, 0] |
|
||||
| value | xxx | string \| number | `small` |
|
||||
|
||||
### Promise
|
||||
- When string type, the **Default** use ` `` `.
|
||||
- Can also list string optional values in **Type**.
|
||||
- When boolean type, the **Default** value is true or false.
|
||||
- When number type, the **Default** value use numbers directly.
|
||||
- When function type, use an arrow function expression in **Type**.
|
||||
- No default value use - .
|
||||
- Capitalize the first letter in **Description** apart from `someProp`.
|
||||
- No period at the end of the **Description**.
|
||||
- API order is arranged in alphabetical order, and can be put together under special circumstances (such as: xs sm md).
|
||||
|
||||
ref: [#25066](mdc:https:/github.com/ant-design/ant-design/issues/25066)
|
27
.cursor/rules/project.mdc
Normal file
27
.cursor/rules/project.mdc
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# 项目背景
|
||||
|
||||
这是 ant-design/ant-design(antd)的源代码仓库,是一个 React 组件库,发布为 npm 包 antd。
|
||||
|
||||
- 使用 TypeScript 和 React 开发
|
||||
- 兼容 React 16 ~ 19 版本
|
||||
- 组件库设计精美,功能完善,广泛应用于企业级中后台产品
|
||||
- 遵循 Ant Design 设计规范
|
||||
- 支持国际化
|
||||
|
||||
# 编码规范
|
||||
|
||||
- 使用 TypeScript 和 React 书写
|
||||
- 使用函数式组件和 hooks,避免类组件
|
||||
- 使用提前返回(early returns)提高代码可读性
|
||||
- 避免引入新依赖,严控打包体积
|
||||
- 兼容 Chrome 80+ 浏览器
|
||||
- 支持服务端渲染
|
||||
- 保持向下兼容,避免 breaking change
|
||||
- 组件名使用大驼峰(PascalCase)
|
||||
- 属性名使用小驼峰(camelCase)
|
||||
- 合理使用 React.memo、useMemo 和 useCallback 优化性能
|
79
.cursor/rules/styling.mdc
Normal file
79
.cursor/rules/styling.mdc
Normal file
@ -0,0 +1,79 @@
|
||||
---
|
||||
description:
|
||||
globs: components/*/style/**
|
||||
alwaysApply: false
|
||||
---
|
||||
# 样式规范
|
||||
|
||||
## 样式方案
|
||||
|
||||
- 使用 `@ant-design/cssinjs` 作为样式解决方案
|
||||
- 每个组件的样式应该放在 `style/` 目录下
|
||||
- 样式文件应该与组件结构保持一致
|
||||
- 使用 CSS-in-JS 时应当注意性能影响,避免不必要的样式重计算
|
||||
- 样式生成函数应遵循 `gen[ComponentName]Style` 的命名规范
|
||||
- 样式覆盖应使用类选择器而非标签选择器,提高样式特异性
|
||||
|
||||
## Token 系统
|
||||
|
||||
- 使用 Ant Design 的设计 Token 系统
|
||||
- 避免硬编码颜色、尺寸、间距等值
|
||||
- 组件样式应基于全局 Token 和组件级 Token
|
||||
- 自定义样式应尽可能使用现有的 Token,保持一致性
|
||||
- 组件级 Token 命名规范:`Component` + 属性名,如 `buttonPrimaryColor`
|
||||
- 对 Token 的修改应当向下传递,确保设计系统的一致性
|
||||
|
||||
## 响应式设计
|
||||
|
||||
- 组件应支持在不同屏幕尺寸下良好展示
|
||||
- 使用相对单位(如 em、rem)而非固定像素值
|
||||
- 关键断点应与设计系统保持一致
|
||||
- 在小屏幕上提供良好的降级方案
|
||||
- 使用 CSS Grid 和 Flexbox 布局实现响应式布局
|
||||
- 考虑移动设备上的触摸交互体验
|
||||
|
||||
## 暗色模式
|
||||
|
||||
- 所有组件必须支持暗色模式
|
||||
- 暗色模式应通过 Token 系统实现,不应硬编码
|
||||
- 测试暗色模式下的颜色对比度,确保可访问性
|
||||
- 在设计暗色模式时考虑降低亮度和饱和度
|
||||
- 确保文本在暗色背景上有足够的对比度
|
||||
- 图片和图标应提供适合暗色模式的版本
|
||||
|
||||
## RTL 支持
|
||||
|
||||
- 组件应支持从右到左(RTL)的阅读方向
|
||||
- 使用 CSS 逻辑属性(如 margin-inline-start)替代方向性属性(如 margin-left)
|
||||
- 图标和方向性元素应随 RTL 模式翻转
|
||||
- 测试组件在 RTL 模式下的布局和交互
|
||||
- 确保文本对齐和方向符合 RTL 规范
|
||||
- 处理好数字和日期等特殊内容在 RTL 模式下的显示
|
||||
|
||||
## 动画效果
|
||||
|
||||
- 使用 CSS 过渡实现简单动画
|
||||
- 复杂动画使用 rc-motion 实现
|
||||
- 尊重用户的减少动画设置(prefers-reduced-motion)
|
||||
- 动画时长和缓动函数应保持一致性
|
||||
- 动画不应干扰用户的操作和阅读体验
|
||||
- 为关键操作提供合适的反馈动画
|
||||
- 避免使用会导致性能问题的 CSS 属性(如 box-shadow)进行动画
|
||||
|
||||
## 主题定制
|
||||
|
||||
- 支持通过 ConfigProvider 进行主题定制
|
||||
- 提供完整的组件级 Token 配置
|
||||
- 保持向后兼容性,不轻易改变 Token 含义
|
||||
- 避免在组件内使用不可覆盖的样式
|
||||
- 提供主题切换的平滑过渡效果
|
||||
- 测试自定义主题在各种组件组合下的效果
|
||||
|
||||
## 可访问性样式
|
||||
|
||||
- 遵循 WCAG 2.1 AA 级别标准
|
||||
- 确保焦点状态有明显的视觉提示
|
||||
- 提供足够的色彩对比度
|
||||
- 不依赖颜色来传达信息
|
||||
- 支持用户放大页面至 200% 时的正常布局
|
||||
- 避免使用会导致闪烁的动画
|
11
.cursor/rules/testing.mdc
Normal file
11
.cursor/rules/testing.mdc
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
description:
|
||||
globs: **/__tests__/**,**/*.test.tsx,**/*.test.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
# 测试规范
|
||||
|
||||
- 使用 Jest 和 React Testing Library 编写单元测试
|
||||
- 对 UI 组件使用快照测试 (Snapshot Testing)
|
||||
- 测试覆盖率要求 100%
|
||||
- 测试文件放在 __tests__ 目录,命名格式为:index.test.tsx 或 xxx.test.tsx
|
81
.cursor/rules/typescript.mdc
Normal file
81
.cursor/rules/typescript.mdc
Normal file
@ -0,0 +1,81 @@
|
||||
# TypeScript 规范
|
||||
|
||||
## 基本原则
|
||||
|
||||
- 所有组件和函数必须提供准确的类型定义
|
||||
- 避免使用 `any` 类型,尽可能精确地定义类型
|
||||
- 使用接口而非类型别名定义对象结构
|
||||
- 导出所有公共接口类型,方便用户使用
|
||||
- 严格遵循 TypeScript 类型设计原则,确保类型安全
|
||||
- 确保编译无任何类型错误或警告
|
||||
|
||||
## 组件类型定义
|
||||
|
||||
- 组件 props 应使用 interface 定义,便于扩展
|
||||
- 组件 props 接口命名应为 `ComponentNameProps`
|
||||
- 为组件状态定义专门的接口,如 `ComponentNameState`
|
||||
- 复杂的数据结构应拆分为多个接口定义
|
||||
- 组件的 ref 类型应该明确定义,使用 `React.ForwardRefRenderFunction`
|
||||
- 所有回调函数类型应明确定义参数和返回值
|
||||
|
||||
## 泛型使用
|
||||
|
||||
- 适当使用泛型增强类型灵活性
|
||||
- 为泛型参数提供合理的默认类型和约束
|
||||
- 避免过度使用泛型导致类型复杂化
|
||||
- 在泛型参数上应用限制条件(constraints)确保类型安全
|
||||
- 为复杂泛型提供类型别名以提高可读性
|
||||
|
||||
## 类型合并与扩展
|
||||
|
||||
- 使用交叉类型(&)合并多个类型
|
||||
- 使用 Partial<T>、Pick<T, K>、Omit<T, K> 等工具类型修改现有类型
|
||||
- 扩展原生 DOM 元素属性时,继承相应的内置类型
|
||||
- 使用 type 定义联合类型和交叉类型
|
||||
- 优先使用自带的工具类型,避免重复定义
|
||||
|
||||
## 枚举和常量
|
||||
|
||||
- 使用字面量联合类型定义有限的选项集合
|
||||
- 为复杂的枚举值提供类型守卫函数
|
||||
- 避免使用 `enum`,优先使用联合类型和 `as const`
|
||||
- 对于关键常量,使用 `as const` 断言确保类型严格
|
||||
- 为联合类型中的每个值提供适当的注释
|
||||
|
||||
## 类型推断与断言
|
||||
|
||||
- 尽可能依赖 TypeScript 的类型推断
|
||||
- 只在必要时使用类型断言(as)
|
||||
- 使用类型守卫函数进行运行时类型检查
|
||||
- 避免使用非空断言操作符(!)
|
||||
- 使用 `instanceof` 和 `typeof` 进行类型守卫
|
||||
- 为自定义类型创建类型谓词(type predicates)函数
|
||||
|
||||
## JSDoc 注释
|
||||
|
||||
- 为复杂的类型、函数、组件添加 JSDoc 注释
|
||||
- 使用 `@deprecated` 标记已废弃的 API
|
||||
- 在注释中提供使用示例
|
||||
- 说明参数和返回值的含义与约束
|
||||
- 在 interface 和重要类型定义上添加文档注释
|
||||
- 使用 `@template` 标记泛型参数
|
||||
|
||||
## 类型兼容性
|
||||
|
||||
- 确保类型定义兼容不同版本的 React
|
||||
- 避免使用实验性或不稳定的 TypeScript 特性
|
||||
- 为第三方库未提供的类型编写声明文件
|
||||
- 使用条件类型处理复杂的类型逻辑
|
||||
- 验证类型在不同 TypeScript 版本下的兼容性
|
||||
|
||||
## 严格使用 TypeScript 类型
|
||||
|
||||
- 导出组件类型和接口
|
||||
- 使用 React.FC<Props> 或明确的返回类型
|
||||
- 避免使用 any,优先使用 unknown
|
||||
- 组件 Props 使用 interface 定义
|
||||
- 工具类型使用 type 定义
|
||||
- 使用明确的命名约定
|
||||
- 合理使用泛型提高复用性
|
||||
- 导出类型时使用 export type
|
||||
- 组件属性使用 JSDoc 注释说明用途
|
76
.cursorrules
76
.cursorrules
@ -1,76 +0,0 @@
|
||||
## 项目背景
|
||||
|
||||
这是 ant-design/ant-design(antd)的源代码仓库,是一个 React 组件库,发布为 npm 包 antd 。
|
||||
|
||||
## 编码规范
|
||||
|
||||
- 使用 TypeScript 和 React 书写
|
||||
- 兼容 React 16 ~ 19 版本
|
||||
- 使用 @ant-design/cssinjs 书写 css
|
||||
- 使用 @ant-design/icons 图标库
|
||||
- 使用函数式组件和 hooks,避免类组件
|
||||
- 尽可能使用提前返回(early returns)以提高代码的可读性
|
||||
- 避免引入新的依赖,严控打包体积
|
||||
- demo 代码尽可能简洁,避免冗余代码,方便用户复制到应用项目里直接可用
|
||||
- 兼容 chrome 80+ 浏览器
|
||||
- 修改时请保持向下兼容,避免制造 break change
|
||||
|
||||
## 测试要求
|
||||
|
||||
- 使用 Jest 和 React Testing Library 编写单元测试
|
||||
- 对 UI 组件使用快照测试 (Snapshot Testing)
|
||||
- 测试覆盖率要求 100%
|
||||
|
||||
# 文档规范
|
||||
|
||||
- 提供中英文两个版本
|
||||
- 新的属性需要声明可用的版本号
|
||||
- 属性命名符合 antd 的 API 命名规则:https://github.com/ant-design/ant-design/wiki/API-Naming-rules
|
||||
|
||||
## Changelog 规范
|
||||
|
||||
- 在 CHANGELOG.en-US.md 和 CHANGELOG.zh-CN.md 书写每个版本的变更
|
||||
- 对用户使用上无感知的改动建议(文档修补、微小的样式优化、代码风格重构等等)不要提及,保持 CHANGELOG 的内容有效性。
|
||||
- 用面向开发者的角度和叙述方式撰写 CHANGELOG,不描述修复细节,描述问题和对开发者的影响;描述用户的原始问题,而非你的解决方式。
|
||||
* 例子一
|
||||
bad: 修复组件 Typography 的 dom 结构问题。
|
||||
good: 重构并简化了 List Item 的 dom 结构,并且修复了 Item 中内容空格丢失的问题。
|
||||
* 例子二
|
||||
bad: 修复 lib 下样式文件路径问题。
|
||||
good: 修复部分组件样式丢失的问题。
|
||||
- 新增属性时,建议用易于理解的语言补充描述用户可以感知的变化。(例如,新增 onCellClick 属性,可以定义单元格点击事件)
|
||||
- 尽量给出原始的 PR 链接,社区提交的 PR 改动加上提交者的链接。
|
||||
- 底层模块升级中间版本要去 rc-component 里找到改动,给出变动说明。
|
||||
- 建议参考之前版本的日志写法
|
||||
- 将同组件的改动放在一起,内容子级缩进。
|
||||
- 每一个改动前加 emoji 增加更新日志的可读性和生动性,可选 emoji 参考:
|
||||
- 🐞 Bug 修复
|
||||
- 💄 样式更新或 token 更新
|
||||
- 🆕 新增特性,新增属性
|
||||
- 🔥 极其值得关注的新增特性
|
||||
- 🇺🇸🇨🇳🇬🇧 国际化改动,注意这里直接用对应国家/地区的旗帜。
|
||||
- 📖 📝 文档或网站改进
|
||||
- ✅ 新增或更新测试用例
|
||||
- 🛎 更新警告/提示信息
|
||||
- ⌨️ ♿ 可访问性增强
|
||||
- 🗑 废弃或移除
|
||||
- 🛠 重构或工具链优化
|
||||
- ⚡️ 性能提升
|
||||
|
||||
## 实现要求
|
||||
|
||||
你是一名资深的前端开发工程师,同时也是 React、Ant Design、TypeScript、HTML、CSS 的专家。你思维缜密,能够给出细致入微的答案,并且在逻辑推理方面表现出色。你会谨慎地提供准确、真实、深思熟虑的答案,并且擅长逻辑推理。
|
||||
|
||||
- 严格遵循用户的需求,逐字逐句完成。
|
||||
- 首先逐步思考——用伪代码详细描述你要构建的内容的计划。
|
||||
- 确认后,再编写代码!
|
||||
- 始终编写正确、符合最佳实践、遵循 DRY 原则(不要重复自己)、无错误、功能齐全且可运行的代码。
|
||||
- 注重代码的易读性和可维护性。
|
||||
- 非常关注代码压缩后的体积。
|
||||
- 全面实现所有请求的功能。
|
||||
- 不留任何待办事项、占位符或缺失的部分。
|
||||
- 确保代码完整!彻底验证最终结果。
|
||||
- 包含所有必要的导入,并确保关键组件的命名得当。
|
||||
- 简洁明了,尽量减少其他冗长的说明。
|
||||
- 如果你认为可能没有正确的答案,请明确说明。
|
||||
- 如果你不知道答案,请坦诚说明,而不是猜测。
|
@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
// @ts-ignore
|
||||
import { TinyColor } from 'dumi-plugin-color-chunk/component';
|
||||
import { FastColor } from '@ant-design/fast-color';
|
||||
import type { ColorInput } from '@ant-design/fast-color';
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
const useStyle = createStyles(({ token, css }) => ({
|
||||
@ -22,17 +22,14 @@ const useStyle = createStyles(({ token, css }) => ({
|
||||
}));
|
||||
|
||||
interface ColorChunkProps {
|
||||
value: any;
|
||||
value: ColorInput;
|
||||
}
|
||||
|
||||
const ColorChunk: React.FC<React.PropsWithChildren<ColorChunkProps>> = (props) => {
|
||||
const { styles } = useStyle();
|
||||
const { value, children } = props;
|
||||
|
||||
const dotColor = React.useMemo(() => {
|
||||
const _color = new TinyColor(value).toHex8String();
|
||||
return _color.endsWith('ff') ? _color.slice(0, -2) : _color;
|
||||
}, [value]);
|
||||
const dotColor = React.useMemo(() => new FastColor(value).toHexString(), [value]);
|
||||
|
||||
return (
|
||||
<span className={styles.codeSpan}>
|
||||
|
@ -72,6 +72,7 @@ export default function useResponsiveObserver() {
|
||||
let screens: Partial<Record<Breakpoint, boolean>> = {};
|
||||
|
||||
return {
|
||||
responsiveMap,
|
||||
matchHandlers: {} as {
|
||||
[prop: string]: {
|
||||
mql: MediaQueryList;
|
||||
@ -98,6 +99,18 @@ export default function useResponsiveObserver() {
|
||||
this.unregister();
|
||||
}
|
||||
},
|
||||
register() {
|
||||
Object.keys(responsiveMap).forEach((screen) => {
|
||||
const matchMediaQuery = responsiveMap[screen as Breakpoint];
|
||||
const listener = ({ matches }: { matches: boolean }) => {
|
||||
this.dispatch({ ...screens, [screen]: matches });
|
||||
};
|
||||
const mql = window.matchMedia(matchMediaQuery);
|
||||
mql.addListener(listener);
|
||||
this.matchHandlers[matchMediaQuery] = { mql, listener };
|
||||
listener(mql);
|
||||
});
|
||||
},
|
||||
unregister() {
|
||||
Object.keys(responsiveMap).forEach((screen) => {
|
||||
const matchMediaQuery = responsiveMap[screen as Breakpoint];
|
||||
@ -106,25 +119,6 @@ export default function useResponsiveObserver() {
|
||||
});
|
||||
subscribers.clear();
|
||||
},
|
||||
register() {
|
||||
Object.keys(responsiveMap).forEach((screen) => {
|
||||
const matchMediaQuery = responsiveMap[screen as Breakpoint];
|
||||
const listener = ({ matches }: { matches: boolean }) => {
|
||||
this.dispatch({
|
||||
...screens,
|
||||
[screen]: matches,
|
||||
});
|
||||
};
|
||||
const mql = window.matchMedia(matchMediaQuery);
|
||||
mql.addListener(listener);
|
||||
this.matchHandlers[matchMediaQuery] = {
|
||||
mql,
|
||||
listener,
|
||||
};
|
||||
listener(mql);
|
||||
});
|
||||
},
|
||||
responsiveMap,
|
||||
};
|
||||
}, [token]);
|
||||
}
|
||||
|
@ -194,8 +194,8 @@ const genArrowsStyle: GenerateStyle<CarouselToken> = (token) => {
|
||||
width: arrowLength,
|
||||
height: arrowLength,
|
||||
border: `0 solid currentcolor`,
|
||||
borderInlineWidth: '2px 0',
|
||||
borderBlockWidth: '2px 0',
|
||||
borderInlineStartWidth: 2,
|
||||
borderBlockStartWidth: 2,
|
||||
borderRadius: 1,
|
||||
content: '""',
|
||||
},
|
||||
|
@ -5,9 +5,10 @@ import type { ColorPickerToken } from './index';
|
||||
|
||||
/**
|
||||
* @private Internal usage only
|
||||
* see: https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/conic-gradient#checkerboard
|
||||
*/
|
||||
export const getTransBg = (size: string, colorFill: string): CSSObject => ({
|
||||
backgroundImage: `conic-gradient(${colorFill} 0 25%, transparent 0 50%, ${colorFill} 0 75%, transparent 0)`,
|
||||
backgroundImage: `conic-gradient(${colorFill} 25%, transparent 25% 50%, ${colorFill} 50% 75%, transparent 75% 100%)`,
|
||||
backgroundSize: `${size} ${size}`,
|
||||
});
|
||||
|
||||
|
@ -321,8 +321,8 @@ export const genPanelStyle = (token: SharedPickerToken): CSSObject => {
|
||||
width: pickerControlIconSize,
|
||||
height: pickerControlIconSize,
|
||||
border: `0 solid currentcolor`,
|
||||
borderBlockWidth: `${unit(pickerControlIconBorderWidth)} 0`,
|
||||
borderInlineWidth: `${unit(pickerControlIconBorderWidth)} 0`,
|
||||
borderBlockStartWidth: pickerControlIconBorderWidth,
|
||||
borderInlineStartWidth: pickerControlIconBorderWidth,
|
||||
content: '""',
|
||||
},
|
||||
},
|
||||
@ -337,8 +337,8 @@ export const genPanelStyle = (token: SharedPickerToken): CSSObject => {
|
||||
width: pickerControlIconSize,
|
||||
height: pickerControlIconSize,
|
||||
border: '0 solid currentcolor',
|
||||
borderBlockWidth: `${unit(pickerControlIconBorderWidth)} 0`,
|
||||
borderInlineWidth: `${unit(pickerControlIconBorderWidth)} 0`,
|
||||
borderBlockStartWidth: pickerControlIconBorderWidth,
|
||||
borderInlineStartWidth: pickerControlIconBorderWidth,
|
||||
content: '""',
|
||||
},
|
||||
},
|
||||
|
@ -31,7 +31,7 @@ const floatButtonGroupMotion = (token: FloatButtonToken) => {
|
||||
});
|
||||
const moveRightIn = new Keyframes('antFloatButtonMoveRightIn', {
|
||||
'0%': {
|
||||
transform: `translate3d(${calc(floatButtonSize).mul(-1).equal()}, 0, 0)`,
|
||||
transform: `translate3d(${unit(calc(floatButtonSize).mul(-1).equal())}, 0, 0)`,
|
||||
transformOrigin: '0 0',
|
||||
opacity: 0,
|
||||
},
|
||||
@ -48,14 +48,14 @@ const floatButtonGroupMotion = (token: FloatButtonToken) => {
|
||||
opacity: 1,
|
||||
},
|
||||
'100%': {
|
||||
transform: `translate3d(${calc(floatButtonSize).mul(-1).equal()}, 0, 0)`,
|
||||
transform: `translate3d(${unit(calc(floatButtonSize).mul(-1).equal())}, 0, 0)`,
|
||||
transformOrigin: '0 0',
|
||||
opacity: 0,
|
||||
},
|
||||
});
|
||||
const moveBottomIn = new Keyframes('antFloatButtonMoveBottomIn', {
|
||||
'0%': {
|
||||
transform: `translate3d(0, ${calc(floatButtonSize).mul(-1).equal()}, 0)`,
|
||||
transform: `translate3d(0, ${unit(calc(floatButtonSize).mul(-1).equal())}, 0)`,
|
||||
transformOrigin: '0 0',
|
||||
opacity: 0,
|
||||
},
|
||||
@ -72,7 +72,7 @@ const floatButtonGroupMotion = (token: FloatButtonToken) => {
|
||||
opacity: 1,
|
||||
},
|
||||
'100%': {
|
||||
transform: `translate3d(0, ${calc(floatButtonSize).mul(-1).equal()}, 0)`,
|
||||
transform: `translate3d(0, ${unit(calc(floatButtonSize).mul(-1).equal())}, 0)`,
|
||||
transformOrigin: '0 0',
|
||||
opacity: 0,
|
||||
},
|
||||
|
@ -283,7 +283,7 @@ const genFormItemStyle: GenerateStyle<FormToken> = (token) => {
|
||||
marginInlineStart: token.marginXXS,
|
||||
color: token.colorTextDescription,
|
||||
|
||||
[`&.${formItemCls}-required-mark-hidden`]: {
|
||||
[`&${formItemCls}-required-mark-hidden`]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { unit } from '@ant-design/cssinjs';
|
||||
|
||||
import { genBasicInputStyle, genInputGroupStyle, genPlaceholderStyle, initInputToken } from '../../input/style';
|
||||
import {
|
||||
genBasicInputStyle,
|
||||
genInputGroupStyle,
|
||||
genPlaceholderStyle,
|
||||
initInputToken,
|
||||
} from '../../input/style';
|
||||
import {
|
||||
genBorderlessStyle,
|
||||
genFilledGroupStyle,
|
||||
@ -235,7 +240,6 @@ const genInputNumberStyles: GenerateStyle<InputNumberToken> = (token: InputNumbe
|
||||
'&[type="number"]::-webkit-inner-spin-button, &[type="number"]::-webkit-outer-spin-button':
|
||||
{
|
||||
margin: 0,
|
||||
webkitAppearance: 'none',
|
||||
appearance: 'none',
|
||||
},
|
||||
},
|
||||
|
@ -17,7 +17,6 @@ import type { SizeType } from '../config-provider/SizeContext';
|
||||
import { FormItemInputContext } from '../form/context';
|
||||
import useVariant from '../form/hooks/useVariants';
|
||||
import { useCompactItemContext } from '../space/Compact';
|
||||
import useHandleResizeWrapper from './hooks/useHandleResizeWrapper';
|
||||
import type { InputFocusOptions } from './Input';
|
||||
import { triggerFocus } from './Input';
|
||||
import { useSharedStyle } from './style';
|
||||
@ -57,6 +56,8 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
|
||||
styles,
|
||||
variant: customVariant,
|
||||
showCount,
|
||||
onMouseDown,
|
||||
onResize,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
@ -76,11 +77,11 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
|
||||
styles: contextStyles,
|
||||
} = useComponentConfig('textArea');
|
||||
|
||||
// ===================== Disabled =====================
|
||||
// =================== Disabled ===================
|
||||
const disabled = React.useContext(DisabledContext);
|
||||
const mergedDisabled = customDisabled ?? disabled;
|
||||
|
||||
// ===================== Status =====================
|
||||
// ==================== Status ====================
|
||||
const {
|
||||
status: contextStatus,
|
||||
hasFeedback,
|
||||
@ -88,7 +89,7 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
|
||||
} = React.useContext(FormItemInputContext);
|
||||
const mergedStatus = getMergedStatus(contextStatus, customStatus);
|
||||
|
||||
// ===================== Ref =====================
|
||||
// ===================== Ref ======================
|
||||
const innerRef = React.useRef<RcTextAreaRef>(null);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
@ -101,12 +102,12 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
|
||||
|
||||
const prefixCls = getPrefixCls('input', customizePrefixCls);
|
||||
|
||||
// ===================== Style =====================
|
||||
// ==================== Style =====================
|
||||
const rootCls = useCSSVarCls(prefixCls);
|
||||
const [wrapSharedCSSVar, hashId, cssVarCls] = useSharedStyle(prefixCls, rootClassName);
|
||||
const [wrapCSSVar] = useStyle(prefixCls, rootCls);
|
||||
|
||||
// ===================== Compact Item =====================
|
||||
// ================= Compact Item =================
|
||||
const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction);
|
||||
|
||||
// ===================== Size =====================
|
||||
@ -116,8 +117,39 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
|
||||
|
||||
const mergedAllowClear = getAllowClear(allowClear ?? contextAllowClear);
|
||||
|
||||
const handleResizeWrapper = useHandleResizeWrapper();
|
||||
// ==================== Resize ====================
|
||||
// https://github.com/ant-design/ant-design/issues/51594
|
||||
const [isMouseDown, setIsMouseDown] = React.useState(false);
|
||||
|
||||
// When has wrapper, resize will make as dirty for `resize: both` style
|
||||
const [resizeDirty, setResizeDirty] = React.useState(false);
|
||||
|
||||
const onInternalMouseDown: typeof onMouseDown = (e) => {
|
||||
setIsMouseDown(true);
|
||||
onMouseDown?.(e);
|
||||
|
||||
const onMouseUp = () => {
|
||||
setIsMouseDown(false);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
const onInternalResize: RcTextAreaProps['onResize'] = (size) => {
|
||||
onResize?.(size);
|
||||
|
||||
// Change to dirty since this maybe from the `resize: both` style
|
||||
if (isMouseDown && typeof getComputedStyle === 'function') {
|
||||
const ele = innerRef.current?.nativeElement?.querySelector('textarea');
|
||||
|
||||
if (ele && getComputedStyle(ele).resize === 'both') {
|
||||
setResizeDirty(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== Render ====================
|
||||
return wrapSharedCSSVar(
|
||||
wrapCSSVar(
|
||||
<RcTextArea
|
||||
@ -134,6 +166,8 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
|
||||
rootClassName,
|
||||
compactItemClassnames,
|
||||
contextClassName,
|
||||
// Only for wrapper
|
||||
resizeDirty && `${prefixCls}-textarea-affix-wrapper-resize-dirty`,
|
||||
)}
|
||||
classNames={{
|
||||
...classes,
|
||||
@ -146,6 +180,7 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
|
||||
hashId,
|
||||
classes?.textarea,
|
||||
contextClassNames.textarea,
|
||||
isMouseDown && `${prefixCls}-mouse-active`,
|
||||
),
|
||||
variant: classNames(
|
||||
{
|
||||
@ -159,7 +194,7 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
|
||||
[`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
|
||||
[`${prefixCls}-affix-wrapper-sm`]: mergedSize === 'small',
|
||||
[`${prefixCls}-affix-wrapper-lg`]: mergedSize === 'large',
|
||||
[`${prefixCls}-textarea-show-count`]: props.showCount || props.count?.show,
|
||||
[`${prefixCls}-textarea-show-count`]: showCount || props.count?.show,
|
||||
},
|
||||
hashId,
|
||||
),
|
||||
@ -170,10 +205,8 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
|
||||
}
|
||||
showCount={showCount}
|
||||
ref={innerRef}
|
||||
onResize={(size) => {
|
||||
rest.onResize?.(size);
|
||||
showCount && handleResizeWrapper(innerRef.current);
|
||||
}}
|
||||
onResize={onInternalResize}
|
||||
onMouseDown={onInternalMouseDown}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
@ -11956,6 +11956,26 @@ Array [
|
||||
</button>
|
||||
</span>
|
||||
</span>,
|
||||
<br />,
|
||||
<span
|
||||
class="ant-input-affix-wrapper ant-input-textarea-affix-wrapper ant-input-textarea-show-count ant-input-show-count ant-input-outlined"
|
||||
data-count="0"
|
||||
style="resize: both;"
|
||||
>
|
||||
<textarea
|
||||
class="ant-input"
|
||||
style="resize: both;"
|
||||
/>
|
||||
<span
|
||||
class="ant-input-suffix"
|
||||
>
|
||||
<span
|
||||
class="ant-input-data-count"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</span>
|
||||
</span>,
|
||||
]
|
||||
`;
|
||||
|
||||
|
@ -5264,6 +5264,26 @@ Array [
|
||||
</button>
|
||||
</span>
|
||||
</span>,
|
||||
<br />,
|
||||
<span
|
||||
class="ant-input-affix-wrapper ant-input-textarea-affix-wrapper ant-input-textarea-show-count ant-input-show-count ant-input-outlined"
|
||||
data-count="0"
|
||||
style="resize:both"
|
||||
>
|
||||
<textarea
|
||||
class="ant-input"
|
||||
style="resize:both"
|
||||
/>
|
||||
<span
|
||||
class="ant-input-suffix"
|
||||
>
|
||||
<span
|
||||
class="ant-input-data-count"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</span>
|
||||
</span>,
|
||||
]
|
||||
`;
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import type { ChangeEventHandler, TextareaHTMLAttributes } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import type { TextAreaRef as RcTextAreaRef } from 'rc-textarea';
|
||||
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
|
||||
|
||||
import Input from '..';
|
||||
@ -10,12 +9,10 @@ import {
|
||||
fireEvent,
|
||||
pureRender,
|
||||
render,
|
||||
renderHook,
|
||||
triggerResize,
|
||||
waitFakeTimer,
|
||||
waitFakeTimer19,
|
||||
} from '../../../tests/utils';
|
||||
import useHandleResizeWrapper from '../hooks/useHandleResizeWrapper';
|
||||
import type { TextAreaRef } from '../TextArea';
|
||||
|
||||
const { TextArea } = Input;
|
||||
@ -533,113 +530,21 @@ describe('TextArea allowClear', () => {
|
||||
expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('`bordered` is deprecated'));
|
||||
errSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TextArea useHandleResizeWrapper', () => {
|
||||
let requestAnimationFrameSpy: jest.SpyInstance;
|
||||
it('resize: both', async () => {
|
||||
const { container } = render(<TextArea showCount style={{ resize: 'both' }} />);
|
||||
|
||||
beforeAll(() => {
|
||||
// Use fake timers to control requestAnimationFrame.
|
||||
jest.useFakeTimers();
|
||||
// Override requestAnimationFrame to simulate a 16ms delay.
|
||||
requestAnimationFrameSpy = jest
|
||||
.spyOn(window, 'requestAnimationFrame')
|
||||
.mockImplementation((cb: FrameRequestCallback) => {
|
||||
return window.setTimeout(() => cb(performance.now()), 16);
|
||||
});
|
||||
});
|
||||
fireEvent.mouseDown(container.querySelector('textarea')!);
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
requestAnimationFrameSpy.mockRestore();
|
||||
});
|
||||
triggerResize(container.querySelector('textarea')!);
|
||||
await waitFakeTimer();
|
||||
|
||||
it('does nothing when rcTextArea is null', () => {
|
||||
const { result } = renderHook(useHandleResizeWrapper);
|
||||
// Calling with null should not throw or change anything.
|
||||
expect(() => result.current?.(null)).not.toThrow();
|
||||
});
|
||||
expect(container.querySelector('.ant-input-textarea-affix-wrapper')).toHaveClass(
|
||||
'ant-input-textarea-affix-wrapper-resize-dirty',
|
||||
);
|
||||
expect(container.querySelector('.ant-input-mouse-active')).toBeTruthy();
|
||||
|
||||
it('does nothing when style width does not include "px"', () => {
|
||||
const { result } = renderHook(useHandleResizeWrapper);
|
||||
|
||||
const fakeRcTextArea = {
|
||||
resizableTextArea: {
|
||||
textArea: {
|
||||
style: {
|
||||
width: '100', // missing 'px'
|
||||
},
|
||||
},
|
||||
},
|
||||
nativeElement: {
|
||||
offsetWidth: 110,
|
||||
style: {},
|
||||
},
|
||||
} as unknown as RcTextAreaRef;
|
||||
|
||||
result.current?.(fakeRcTextArea);
|
||||
|
||||
// Fast-forward time to see if any scheduled callback would execute.
|
||||
jest.advanceTimersByTime(16);
|
||||
// nativeElement.style.width remains unchanged.
|
||||
expect(fakeRcTextArea.nativeElement.style.width).toBeUndefined();
|
||||
});
|
||||
|
||||
it('adjusts width correctly when offsetWidth is slightly greater than the textArea width (increased scenario)', () => {
|
||||
const { result } = renderHook(useHandleResizeWrapper);
|
||||
|
||||
const fakeRcTextArea = {
|
||||
resizableTextArea: {
|
||||
textArea: {
|
||||
style: {
|
||||
width: '100px', // valid width with px
|
||||
},
|
||||
},
|
||||
},
|
||||
// offsetWidth is 101 so the difference is 1 (< ELEMENT_GAP of 2)
|
||||
nativeElement: {
|
||||
offsetWidth: 101,
|
||||
style: {},
|
||||
},
|
||||
} as unknown as RcTextAreaRef;
|
||||
|
||||
// Immediately after calling handleResizeWrapper, the update is scheduled.
|
||||
expect(fakeRcTextArea.nativeElement.style.width).toBeUndefined();
|
||||
|
||||
result.current?.(fakeRcTextArea);
|
||||
|
||||
// Fast-forward time to trigger the requestAnimationFrame callback.
|
||||
jest.advanceTimersByTime(16);
|
||||
// Expected new width: 100 + 2 = 102px.
|
||||
expect(fakeRcTextArea.nativeElement.style.width).toBe('102px');
|
||||
});
|
||||
|
||||
it('adjusts width correctly when offsetWidth is significantly greater than the textArea width (decreased scenario)', () => {
|
||||
const { result } = renderHook(useHandleResizeWrapper);
|
||||
|
||||
const fakeRcTextArea = {
|
||||
resizableTextArea: {
|
||||
textArea: {
|
||||
style: {
|
||||
width: '100px',
|
||||
},
|
||||
},
|
||||
},
|
||||
// offsetWidth is 105 so the difference is 5 (> ELEMENT_GAP of 2)
|
||||
nativeElement: {
|
||||
offsetWidth: 105,
|
||||
style: {},
|
||||
},
|
||||
} as unknown as RcTextAreaRef;
|
||||
|
||||
// Immediately after calling handleResizeWrapper, the update is scheduled.
|
||||
expect(fakeRcTextArea.nativeElement.style.width).toBeUndefined();
|
||||
|
||||
result.current?.(fakeRcTextArea);
|
||||
|
||||
// Fast-forward time to trigger the requestAnimationFrame callback.
|
||||
jest.advanceTimersByTime(16);
|
||||
// Expected new width remains: 100 + 2 = 102px.
|
||||
expect(fakeRcTextArea.nativeElement.style.width).toBe('102px');
|
||||
fireEvent.mouseUp(container.querySelector('textarea')!);
|
||||
expect(container.querySelector('.ant-input-mouse-active')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
@ -16,6 +16,13 @@ const App: React.FC = () => {
|
||||
</Button>
|
||||
<TextArea rows={4} autoSize={autoResize} defaultValue={defaultValue} />
|
||||
<TextArea allowClear style={{ width: 93 }} />
|
||||
<br />
|
||||
<TextArea
|
||||
style={{
|
||||
resize: 'both',
|
||||
}}
|
||||
showCount
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import type { TextAreaRef } from 'rc-textarea';
|
||||
import raf from 'rc-util/lib/raf';
|
||||
|
||||
type ResizeWrapperHandler = (rcTextArea: TextAreaRef | null) => void;
|
||||
|
||||
const ELEMENT_GAP = 2;
|
||||
|
||||
const adjustElementWidth = (width: number, wrapper: HTMLElement): void => {
|
||||
if (wrapper.offsetWidth - width < ELEMENT_GAP) {
|
||||
// The textarea's width is increased
|
||||
wrapper.style.width = `${width + ELEMENT_GAP}px`;
|
||||
} else if (wrapper.offsetWidth - width > ELEMENT_GAP) {
|
||||
// The textarea's width is decreased
|
||||
wrapper.style.width = `${width + ELEMENT_GAP}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const useHandleResizeWrapper = () => {
|
||||
const handleResizeWrapper = React.useCallback<ResizeWrapperHandler>((rcTextArea) => {
|
||||
if (!rcTextArea) {
|
||||
return;
|
||||
}
|
||||
if (rcTextArea.resizableTextArea.textArea.style.width.includes('px')) {
|
||||
const width = Number.parseInt(
|
||||
rcTextArea.resizableTextArea.textArea.style.width.replace(/px/, ''),
|
||||
);
|
||||
raf(() => adjustElementWidth(width, rcTextArea.nativeElement));
|
||||
}
|
||||
}, []);
|
||||
return handleResizeWrapper;
|
||||
};
|
||||
|
||||
export default useHandleResizeWrapper;
|
@ -70,17 +70,6 @@ export const genBasicInputStyle = (token: InputToken): CSSObject => ({
|
||||
transition: `all ${token.motionDurationMid}`,
|
||||
...genPlaceholderStyle(token.colorTextPlaceholder),
|
||||
|
||||
// Reset height for `textarea`s
|
||||
'textarea&': {
|
||||
maxWidth: '100%', // prevent textarea resize from coming out of its container
|
||||
height: 'auto',
|
||||
minHeight: token.controlHeight,
|
||||
lineHeight: token.lineHeight,
|
||||
verticalAlign: 'bottom',
|
||||
transition: `all ${token.motionDurationSlow}, height 0s`,
|
||||
resize: 'vertical',
|
||||
},
|
||||
|
||||
// Size
|
||||
'&-lg': {
|
||||
...genInputLargeStyle(token),
|
||||
@ -411,7 +400,7 @@ export const genInputStyle: GenerateStyle<InputToken> = (token: InputToken) => {
|
||||
|
||||
'&[type="search"]::-webkit-search-cancel-button, &[type="search"]::-webkit-search-decoration':
|
||||
{
|
||||
'-webkit-appearance': 'none',
|
||||
appearance: 'none',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -11,6 +11,26 @@ const genTextAreaStyle: GenerateStyle<InputToken> = (token) => {
|
||||
const textareaPrefixCls = `${componentCls}-textarea`;
|
||||
|
||||
return {
|
||||
// Raw Textarea
|
||||
[`textarea${componentCls}`]: {
|
||||
maxWidth: '100%', // prevent textarea resize from coming out of its container
|
||||
height: 'auto',
|
||||
minHeight: token.controlHeight,
|
||||
lineHeight: token.lineHeight,
|
||||
verticalAlign: 'bottom',
|
||||
transition: `all ${token.motionDurationSlow}`,
|
||||
resize: 'vertical',
|
||||
|
||||
[`&${componentCls}-mouse-active`]: {
|
||||
transition: `all ${token.motionDurationSlow}, height 0s, width 0s`,
|
||||
},
|
||||
},
|
||||
|
||||
// Wrapper for resize
|
||||
[`${componentCls}-textarea-affix-wrapper-resize-dirty`]: {
|
||||
width: 'auto',
|
||||
},
|
||||
|
||||
[textareaPrefixCls]: {
|
||||
position: 'relative',
|
||||
|
||||
|
@ -223,7 +223,7 @@ const genBaseFilledStyle = (
|
||||
borderColor: 'transparent',
|
||||
|
||||
'input&, & input, textarea&, & textarea': {
|
||||
color: options?.inputColor,
|
||||
color: options?.inputColor ?? 'unset',
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
|
@ -25,7 +25,7 @@ const localeValues: Locale = {
|
||||
filterSearchPlaceholder: 'جستجو در فیلترها',
|
||||
emptyText: 'بدون داده',
|
||||
selectAll: 'انتخاب صفحهی کنونی',
|
||||
selectInvert: 'معکوس کردن انتخابها در صفحه ی کنونی',
|
||||
selectInvert: 'معکوس کردن انتخابها در صفحهی کنونی',
|
||||
selectNone: 'انتخاب هیچکدام',
|
||||
selectionAll: 'انتخاب همهی دادهها',
|
||||
sortTitle: 'مرتب سازی',
|
||||
@ -58,8 +58,9 @@ const localeValues: Locale = {
|
||||
selectCurrent: 'انتخاب صفحه فعلی',
|
||||
removeCurrent: 'پاک کردن انتخابهای صفحه فعلی',
|
||||
selectAll: 'انتخاب همه',
|
||||
deselectAll: 'لغو انتخاب همه',
|
||||
removeAll: 'پاک کردن همه انتخابها',
|
||||
selectInvert: 'معکوس کردن انتخابها در صفحه ی کنونی',
|
||||
selectInvert: 'معکوس کردن انتخابها در صفحهی کنونی',
|
||||
},
|
||||
Upload: {
|
||||
uploading: 'در حال آپلود...',
|
||||
@ -79,6 +80,7 @@ const localeValues: Locale = {
|
||||
copy: 'کپی',
|
||||
copied: 'کپی شد',
|
||||
expand: 'توسعه',
|
||||
collapse: 'بستن',
|
||||
},
|
||||
Form: {
|
||||
optional: '(اختیاری)',
|
||||
@ -134,8 +136,15 @@ const localeValues: Locale = {
|
||||
preview: 'پیشنمایش',
|
||||
},
|
||||
QRCode: {
|
||||
expired: 'QR Code منقضی شذد',
|
||||
expired: 'کد QR منقضی شد',
|
||||
refresh: 'بهروزرسانی',
|
||||
scanned: 'اسکن شد',
|
||||
},
|
||||
ColorPicker: {
|
||||
presetEmpty: 'خالی',
|
||||
transparent: 'شفاف',
|
||||
singleColor: 'تکرنگ',
|
||||
gradientColor: 'گرادینت',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { unit } from '@ant-design/cssinjs';
|
||||
|
||||
import { genBasicInputStyle, genPlaceholderStyle, initComponentToken, initInputToken } from '../../input/style';
|
||||
import {
|
||||
genBasicInputStyle,
|
||||
genPlaceholderStyle,
|
||||
initComponentToken,
|
||||
initInputToken,
|
||||
} from '../../input/style';
|
||||
import type { SharedComponentToken, SharedInputToken } from '../../input/style/token';
|
||||
import {
|
||||
genBorderlessStyle,
|
||||
@ -163,7 +168,7 @@ const genMentionsStyle: GenerateStyle<MentionsToken> = (token) => {
|
||||
[`> textarea, ${componentCls}-measure`]: {
|
||||
color: colorText,
|
||||
boxSizing: 'border-box',
|
||||
minHeight: token.calc(controlHeight).sub(2),
|
||||
minHeight: token.calc(controlHeight).sub(2).equal(),
|
||||
margin: 0,
|
||||
padding: `${unit(paddingBlock)} ${unit(paddingInline)}`,
|
||||
overflow: 'inherit',
|
||||
|
@ -213,7 +213,6 @@ export const genNoticeStyle = (token: NotificationToken): CSSObject => {
|
||||
position: 'absolute',
|
||||
display: 'block',
|
||||
appearance: 'none',
|
||||
WebkitAppearance: 'none',
|
||||
inlineSize: `calc(100% - ${unit(borderRadiusLG)} * 2)`,
|
||||
left: {
|
||||
_skip_check_: true,
|
||||
|
@ -62,7 +62,7 @@ const getSearchInputWithoutBorderStyle: GenerateStyle<SelectToken, CSSObject> =
|
||||
|
||||
'&::-webkit-search-cancel-button': {
|
||||
display: 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
appearance: 'none',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -419,7 +419,7 @@ export const prepareComponentToken: GetDefaultToken<'Steps'> = (token) => ({
|
||||
dotSize: token.controlHeight / 4,
|
||||
dotCurrentSize: token.controlHeightLG / 4,
|
||||
navArrowColor: token.colorTextDisabled,
|
||||
navContentMaxWidth: 'auto',
|
||||
navContentMaxWidth: 'unset',
|
||||
descriptionMaxWidth: 140,
|
||||
waitIconColor: token.wireframe ? token.colorTextDisabled : token.colorTextLabel,
|
||||
waitIconBgColor: token.wireframe ? token.colorBgContainer : token.colorFillContent,
|
||||
|
@ -48,7 +48,7 @@ const genStickyStyle: GenerateStyle<TableToken, CSSObject> = (token) => {
|
||||
height: tableScrollThumbSize,
|
||||
backgroundColor: tableScrollThumbBg,
|
||||
borderRadius: stickyScrollBarBorderRadius,
|
||||
transition: `all ${token.motionDurationSlow}, transform none`,
|
||||
transition: `all ${token.motionDurationSlow}, transform 0s`,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
|
||||
|
@ -3,10 +3,10 @@ import { unit } from '@ant-design/cssinjs';
|
||||
import { getStyle as getCheckboxStyle } from '../../checkbox/style';
|
||||
import type {
|
||||
AliasToken,
|
||||
CSSUtil,
|
||||
FullToken,
|
||||
GenerateStyle,
|
||||
GetDefaultToken,
|
||||
CSSUtil,
|
||||
} from '../../theme/internal';
|
||||
import { genStyleHooks, mergeToken } from '../../theme/internal';
|
||||
import type { TreeSharedToken } from '../../tree/style';
|
||||
@ -39,6 +39,7 @@ const genBaseStyle: GenerateStyle<TreeSelectToken> = (token) => {
|
||||
mergeToken<AliasToken & TreeSharedToken & CSSUtil>(token, {
|
||||
colorBgContainer: colorBgElevated,
|
||||
}),
|
||||
false, // No need style of directory tree
|
||||
),
|
||||
{
|
||||
[treeCls]: {
|
||||
|
@ -432,6 +432,12 @@ export const genBaseStyle = (prefixCls: string, token: TreeToken): CSSObject =>
|
||||
export const genTreeStyle = (
|
||||
prefixCls: string,
|
||||
token: AliasToken & TreeSharedToken & CSSUtil,
|
||||
/**
|
||||
* @descCN 是否启用目录树样式
|
||||
* @descEN Whether to enable directory style
|
||||
* @default true
|
||||
*/
|
||||
enableDirectory = true,
|
||||
): CSSInterpolation => {
|
||||
const treeCls = `.${prefixCls}`;
|
||||
const treeNodeCls = `${treeCls}-treenode`;
|
||||
@ -448,8 +454,8 @@ export const genTreeStyle = (
|
||||
// Basic
|
||||
genBaseStyle(prefixCls, treeToken),
|
||||
// Directory
|
||||
genDirectoryStyle(treeToken),
|
||||
];
|
||||
enableDirectory && genDirectoryStyle(treeToken),
|
||||
].filter(Boolean);
|
||||
};
|
||||
|
||||
export const initComponentToken = (token: AliasToken): TreeSharedToken => {
|
||||
|
@ -220,6 +220,8 @@ export default () => (
|
||||
|
||||
### TailwindCSS Arrange `@layer`
|
||||
|
||||
Before starting the following configuration, you need to enable [`@layer`](#layer) feature.
|
||||
|
||||
#### TailwindCSS v3
|
||||
|
||||
In global.css, adjust `@layer` to control the order of style override. Place `tailwind-base` before `antd`:
|
||||
|
@ -220,6 +220,8 @@ export default () => (
|
||||
|
||||
### TailwindCSS 排布 `@layer`
|
||||
|
||||
在开始以下配置前,你需要先启用 [`@layer`](#layer-样式优先级降权) 功能。
|
||||
|
||||
#### TailwindCSS v3
|
||||
|
||||
在 global.css 中,调整 `@layer` 来控制样式的覆盖顺序。让 `tailwind-base` 置于 `antd` 之前:
|
||||
|
@ -192,6 +192,7 @@
|
||||
"@types/adm-zip": "^0.5.6",
|
||||
"@types/ali-oss": "^6.16.11",
|
||||
"@types/cli-progress": "^3.11.6",
|
||||
"@types/css-tree": "^2.3.10",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/gtag.js": "^0.0.20",
|
||||
"@types/http-server": "^0.12.4",
|
||||
@ -232,11 +233,13 @@
|
||||
"cli-progress": "^3.12.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"css-tree": "^3.1.0",
|
||||
"csstree-validator": "^4.0.1",
|
||||
"cypress-image-diff-html-report": "2.2.0",
|
||||
"dekko": "^0.2.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"dumi": "~2.4.17",
|
||||
"dumi-plugin-color-chunk": "^1.1.2",
|
||||
"dumi-plugin-color-chunk": "^2.1.0",
|
||||
"env-paths": "^3.0.0",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-plugin-compat": "^6.0.1",
|
||||
|
@ -1,5 +1,8 @@
|
||||
import path from 'path';
|
||||
import React from 'react';
|
||||
import {
|
||||
createCache,
|
||||
extractStyle,
|
||||
legacyNotSelectorLinter,
|
||||
logicalPropertiesLinter,
|
||||
NaNLinter,
|
||||
@ -7,11 +10,19 @@ import {
|
||||
StyleProvider,
|
||||
} from '@ant-design/cssinjs';
|
||||
import chalk from 'chalk';
|
||||
import { parse } from 'css-tree';
|
||||
import type { SyntaxParseError } from 'css-tree';
|
||||
import { validate } from 'csstree-validator';
|
||||
import fs from 'fs-extra';
|
||||
import isCI from 'is-ci';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
|
||||
import { ConfigProvider } from '../components';
|
||||
import { generateCssinjs } from './generate-cssinjs';
|
||||
|
||||
const tmpDir = path.join(`${__filename}.tmp`);
|
||||
fs.emptyDirSync(tmpDir);
|
||||
|
||||
console.log(chalk.green(`🔥 Checking CSS-in-JS...`));
|
||||
|
||||
let errorCount = 0;
|
||||
@ -25,6 +36,20 @@ console.error = (msg: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
// https://github.com/csstree/validator/blob/7df8ca/lib/validate.js#L187
|
||||
function cssValidate(css: string, filename: string) {
|
||||
const errors: SyntaxParseError[] = [];
|
||||
const ast = parse(css, {
|
||||
filename,
|
||||
positions: true,
|
||||
onParseError(error) {
|
||||
errors.push(error);
|
||||
},
|
||||
});
|
||||
|
||||
return errors.concat(validate(ast));
|
||||
}
|
||||
|
||||
async function checkCSSVar() {
|
||||
await generateCssinjs({
|
||||
key: 'check',
|
||||
@ -39,6 +64,38 @@ async function checkCSSVar() {
|
||||
},
|
||||
});
|
||||
}
|
||||
async function checkCSSContent() {
|
||||
const errors = new Map();
|
||||
await generateCssinjs({
|
||||
key: 'css-validate',
|
||||
render(Component: any, filePath: string) {
|
||||
const cache = createCache();
|
||||
ReactDOMServer.renderToString(
|
||||
<StyleProvider cache={cache}>
|
||||
<Component />
|
||||
</StyleProvider>,
|
||||
);
|
||||
|
||||
const css = extractStyle(cache, { types: 'style', plain: true });
|
||||
let showPath = filePath;
|
||||
if (!isCI) {
|
||||
const [, name] = filePath.split(path.sep);
|
||||
const writeLocalPath = path.join(tmpDir, `${name}.css`);
|
||||
showPath = path.relative(process.cwd(), writeLocalPath);
|
||||
fs.writeFileSync(writeLocalPath, `/* ${filePath} */\n${css}`);
|
||||
}
|
||||
errors.set(filePath, cssValidate(css, showPath));
|
||||
},
|
||||
});
|
||||
|
||||
for (const [filePath, error] of errors) {
|
||||
if (error.length > 0) {
|
||||
errorCount += error.length;
|
||||
console.log(chalk.red(`❌ ${filePath} has ${error.length} errors:`));
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await generateCssinjs({
|
||||
@ -55,6 +112,7 @@ async function checkCSSVar() {
|
||||
});
|
||||
|
||||
await checkCSSVar();
|
||||
await checkCSSContent();
|
||||
|
||||
if (errorCount > 0) {
|
||||
console.log(chalk.red(`❌ CSS-in-JS check failed with ${errorCount} errors.`));
|
||||
|
@ -7,7 +7,7 @@ type StyleFn = (prefix?: string) => void;
|
||||
|
||||
interface GenCssinjsOptions {
|
||||
key: string;
|
||||
render: (component: React.FC) => void;
|
||||
render: (Component: React.FC, filepath: string) => void;
|
||||
beforeRender?: (componentName: string) => void;
|
||||
}
|
||||
|
||||
@ -35,6 +35,10 @@ export const generateCssinjs = ({ key, beforeRender, render }: GenCssinjsOptions
|
||||
useRowStyle(prefixCls);
|
||||
useColStyle(prefixCls);
|
||||
};
|
||||
} else if (file.includes('tree-select')) {
|
||||
const originalUseStyle = (await import(absPath)).default;
|
||||
useStyle = (prefixCls, treePrefixCls = `${prefixCls}-tree`) =>
|
||||
originalUseStyle(prefixCls, treePrefixCls);
|
||||
} else {
|
||||
useStyle = (await import(absPath)).default;
|
||||
}
|
||||
@ -43,6 +47,6 @@ export const generateCssinjs = ({ key, beforeRender, render }: GenCssinjsOptions
|
||||
return React.createElement('div');
|
||||
};
|
||||
beforeRender?.(componentName);
|
||||
render?.(Demo);
|
||||
render?.(Demo, path.relative(process.cwd(), file));
|
||||
}),
|
||||
);
|
||||
|
2
typings/custom-typings.d.ts
vendored
2
typings/custom-typings.d.ts
vendored
@ -29,3 +29,5 @@ declare module '@npmcli/run-script' {
|
||||
declare module '@microflash/rehype-figure';
|
||||
|
||||
declare module 'dekko';
|
||||
|
||||
declare module 'csstree-validator';
|
||||
|
Loading…
Reference in New Issue
Block a user