├── .editorconfig ├── .eslintrc.js ├── .fatherrc.ts ├── .github └── workflows │ └── react-component-ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .umirc.ts ├── LICENSE ├── README.md ├── docs ├── demo │ ├── Comment.md │ ├── Form.md │ └── Theme.md └── examples │ ├── Comment.tsx │ ├── Form.tsx │ └── Theme.tsx ├── jest.config.js ├── now.json ├── package.json ├── src ├── CompatibleConsumer.tsx ├── _util │ ├── types.ts │ ├── upgradeMessage.ts │ └── warning.ts ├── comment │ ├── index.tsx │ └── style │ │ └── index.tsx ├── form │ ├── Form.tsx │ ├── FormItem.tsx │ ├── constants.tsx │ ├── context.ts │ ├── index.tsx │ ├── interface.ts │ └── style │ │ ├── feedback.tsx │ │ ├── index.less │ │ ├── index.tsx │ │ ├── layout.tsx │ │ ├── mixin.less │ │ └── mixin.tsx ├── icon │ ├── index.tsx │ └── utils.ts ├── index.tsx └── theme │ ├── convertLegacyToken.ts │ ├── dark.ts │ ├── default.ts │ ├── genColorMapToken.ts │ └── index.ts ├── tests ├── Comment.test.tsx ├── Form.test.tsx ├── Icon.test.tsx ├── __mocks__ │ ├── rc-trigger.js │ └── rc-virtual-list.js ├── __snapshots__ │ ├── Comment.test.tsx.snap │ ├── Form.test.tsx.snap │ └── Icon.test.tsx.snap ├── less.test.tsx └── setupAfterEnv.ts ├── tsconfig.json └── typings.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const base = require('@umijs/fabric/dist/eslint'); 2 | 3 | module.exports = { 4 | ...base, 5 | rules: { 6 | ...base.rules, 7 | 'arrow-parens': 0, 8 | 'no-confusing-arrow': 0, 9 | 'no-template-curly-in-string': 0, 10 | 'prefer-promise-reject-errors': 0, 11 | 'react/no-array-index-key': 0, 12 | 'react/sort-comp': 0, 13 | 'import/no-named-as-default-member': 0, 14 | 'jsx-a11y/label-has-for': 0, 15 | 'jsx-a11y/label-has-associated-control': 0, 16 | 'import/no-extraneous-dependencies': 0, 17 | 'import/no-unresolved': 0, 18 | '@typescript-eslint/no-redeclare': 0, 19 | '@typescript-eslint/method-signature-style': 0, 20 | 'no-async-promise-executor': 0, 21 | '@typescript-eslint/consistent-type-definitions': 0, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | plugins: ['@rc-component/father-plugin'], 5 | }); 6 | -------------------------------------------------------------------------------- /.github/workflows/react-component-ci.yml: -------------------------------------------------------------------------------- 1 | name: ✅ test 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | uses: react-component/rc-test/.github/workflows/test.yml@main 6 | secrets: inherit 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /npm-debug.log* 6 | /yarn-error.log 7 | /yarn.lock 8 | /package-lock.json 9 | 10 | # production 11 | /dist 12 | /docs-dist 13 | /es 14 | /lib 15 | .doc 16 | /coverage 17 | 18 | # misc 19 | .DS_Store 20 | .vscode 21 | .idea 22 | 23 | # umi 24 | .umi 25 | .umi-production 26 | .umi-test 27 | .env.local 28 | 29 | ~* 30 | package-lock.json 31 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | **/*.ejs 3 | **/*.html 4 | package.json 5 | .umi 6 | .umi-production 7 | .umi-test 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "endOfLine": "lf", 4 | "semi": true, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "all", 8 | "printWidth": 100 9 | } 10 | -------------------------------------------------------------------------------- /.umirc.ts: -------------------------------------------------------------------------------- 1 | // more config: https://d.umijs.org/config 2 | import { defineConfig } from 'dumi'; 3 | 4 | export default defineConfig({ 5 | title: '@ant-design/compatible', 6 | favicon: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', 7 | logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', 8 | outputPath: '.doc', 9 | exportStatic: {}, 10 | styles: [ 11 | // Remove when antd fix this 12 | ` 13 | .markdown table { 14 | width: auto !important; 15 | } 16 | 17 | * { 18 | box-sizing: border-box; 19 | } 20 | `, 21 | ], 22 | }); 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Copyright (c) 2019-present Ant UED, https://xtech.antfin.com/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ant Design Compatible 2 | 3 | [![NPM version](https://img.shields.io/npm/v/@ant-design/compatible.svg?style=flat)](https://npmjs.org/package/@ant-design/compatible) 4 | [![NPM downloads](http://img.shields.io/npm/dm/@ant-design/compatible.svg?style=flat)](https://npmjs.org/package/@ant-design/compatible) 5 | [![CircleCI](https://circleci.com/gh/ant-design/compatible.svg?style=svg)](https://circleci.com/gh/ant-design/compatible) 6 | 7 | ## Install 8 | 9 | ```bash 10 | yarn add @ant-design/compatible@v5-compatible-v4 11 | ``` 12 | 13 | ## Usage 14 | 15 | Helps you to compatible different components between v4 and v5. 16 | 17 | ```diff 18 | -- import { Button, Select, Dropdown } from 'antd'; 19 | ++ import { Button, Select, Dropdown } from '@ant-design/compatible'; 20 | ``` 21 | 22 | ## FAQ 23 | 24 | -------------------------------------------------------------------------------- /docs/demo/Comment.md: -------------------------------------------------------------------------------- 1 | ## Comment 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/demo/Form.md: -------------------------------------------------------------------------------- 1 | ## Form 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/demo/Theme.md: -------------------------------------------------------------------------------- 1 | ## Theme 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/examples/Comment.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, createElement } from 'react'; 2 | import { DislikeFilled, DislikeOutlined, LikeFilled, LikeOutlined } from '@ant-design/icons'; 3 | import { Avatar, Tooltip } from 'antd'; 4 | import { Comment } from '../../src'; 5 | 6 | // import { presetPalettes } from '@ant-design/colors' 7 | // console.log(presetPalettes); 8 | 9 | // const lines = []; 10 | // Object.keys(presetPalettes).forEach((key) => { 11 | // for (let i = 0; i < 10; i += 1) { 12 | // lines.push(`@${key}-${i + 1}: ${presetPalettes[key][i]};`); 13 | // } 14 | // }); 15 | 16 | // console.log(lines.join('\n')); 17 | 18 | const App: React.FC = () => { 19 | const [likes, setLikes] = useState(0); 20 | const [dislikes, setDislikes] = useState(0); 21 | const [action, setAction] = useState(null); 22 | 23 | const like = () => { 24 | setLikes(1); 25 | setDislikes(0); 26 | setAction('liked'); 27 | }; 28 | 29 | const dislike = () => { 30 | setLikes(0); 31 | setDislikes(1); 32 | setAction('disliked'); 33 | }; 34 | 35 | const actions = [ 36 | 37 | 38 | {createElement(action === 'liked' ? LikeFilled : LikeOutlined)} 39 | {likes} 40 | 41 | , 42 | 43 | 44 | {React.createElement(action === 'disliked' ? DislikeFilled : DislikeOutlined)} 45 | {dislikes} 46 | 47 | , 48 | Reply to, 49 | ]; 50 | 51 | return ( 52 | Han Solo} 55 | avatar={} 56 | content={ 57 |

58 | We supply a series of design principles, practical patterns and high quality design 59 | resources (Sketch and Axure), to help people create their product prototypes beautifully 60 | and efficiently. 61 |

62 | } 63 | datetime={ 64 | 65 | 8 hours ago 66 | 67 | } 68 | > 69 | } 73 | content={

This is a nest comment

} 74 | datetime="unknown" 75 | >
76 |
77 | ); 78 | }; 79 | 80 | export default App; 81 | -------------------------------------------------------------------------------- /docs/examples/Form.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Row, 4 | Col, 5 | Input, 6 | Select, 7 | DatePicker, 8 | Button, 9 | TimePicker, 10 | Cascader, 11 | InputNumber, 12 | } from 'antd'; 13 | import { Icon, Form } from '../../src'; 14 | 15 | const { Option } = Select; 16 | 17 | const LAYOUT = { 18 | ROW: { gutter: 24 }, 19 | COL: { span: 8 }, 20 | NORMAL: { 21 | labelCol: { span: 3 }, 22 | wrapperCol: { span: 21 }, 23 | }, 24 | WIDTH: { 25 | labelCol: { span: 6 }, 26 | wrapperCol: { span: 18 }, 27 | }, 28 | }; 29 | 30 | const { MonthPicker, RangePicker } = DatePicker; 31 | 32 | const formItemLayout = { 33 | labelCol: { 34 | xs: { span: 24 }, 35 | sm: { span: 5 }, 36 | }, 37 | wrapperCol: { 38 | xs: { span: 24 }, 39 | sm: { span: 12 }, 40 | }, 41 | }; 42 | 43 | const config = { 44 | rules: [{ type: 'object', required: true, message: 'Please select time!' }], 45 | }; 46 | const rangeConfig = { 47 | rules: [{ type: 'array', required: true, message: 'Please select time!' }], 48 | }; 49 | 50 | class FormVertical extends React.Component { 51 | onSubmit = async event => { 52 | event.preventDefault(); 53 | const { validateFields } = this.props.form; 54 | const values = await validateFields(); 55 | console.log(values); 56 | }; 57 | 58 | render() { 59 | const { getFieldDecorator } = this.props.form; 60 | return ( 61 |
62 |
63 | 64 | 65 | 66 | {getFieldDecorator('input')()} 67 | 68 | 69 | 70 | 71 | {getFieldDecorator('select')( 72 | , 77 | )} 78 | 79 | 80 | 81 | 82 | {getFieldDecorator('input2')()} 83 | 84 | 85 | 86 |
87 |
92 |
93 | 94 | {getFieldDecorator('username', { 95 | rules: [{ required: true, message: 'Please input your username!' }], 96 | })( 97 | } 99 | placeholder="Username" 100 | />, 101 | )} 102 | 103 | 104 | {getFieldDecorator('password', { 105 | rules: [{ required: true, message: 'Please input your Password!' }], 106 | })( 107 | } 109 | type="password" 110 | placeholder="Password" 111 | />, 112 | )} 113 | 114 | 115 | 118 | 119 |
120 |
121 | 122 | 123 | 124 | 125 | 126 | 127 |
128 |
129 | 130 | 131 | 132 | 133 | 134 | 135 |
136 |
137 | 138 | 139 | 140 | 141 | 142 | 143 |
144 |
145 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 197 | 198 | 199 | 205 | 206 | 207 | 208 | 209 | 214 | 215 | 216 | 223 | - 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 269 | 272 | 273 |
274 |
275 |
276 | ); 277 | } 278 | } 279 | export default Form.create()(FormVertical); 280 | -------------------------------------------------------------------------------- /docs/examples/Theme.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigProvider, Menu, MenuProps } from 'antd'; 2 | import { defaultTheme, darkTheme } from '@ant-design/compatible'; 3 | import { AppstoreOutlined, CalendarOutlined, LinkOutlined, MailOutlined, SettingOutlined } from '@ant-design/icons'; 4 | import React from 'react'; 5 | 6 | type MenuItem = Required['items'][number]; 7 | 8 | function getItem( 9 | label: React.ReactNode, 10 | key?: React.Key | null, 11 | icon?: React.ReactNode, 12 | children?: MenuItem[], 13 | ): MenuItem { 14 | return { 15 | key, 16 | icon, 17 | children, 18 | label, 19 | } as MenuItem; 20 | } 21 | 22 | const items: MenuItem[] = [ 23 | getItem('Navigation One', '1', ), 24 | getItem('Navigation Two', '2', ), 25 | getItem('Navigation Two', 'sub1', , [ 26 | getItem('Option 3', '3'), 27 | getItem('Option 4', '4'), 28 | getItem('Submenu', 'sub1-2', null, [getItem('Option 5', '5'), getItem('Option 6', '6')]), 29 | ]), 30 | getItem('Navigation Three', 'sub2', , [ 31 | getItem('Option 7', '7'), 32 | getItem('Option 8', '8'), 33 | getItem('Option 9', '9'), 34 | getItem('Option 10', '10'), 35 | ]), 36 | getItem( 37 | 38 | Ant Design 39 | , 40 | 'link', 41 | , 42 | ), 43 | ]; 44 | 45 | const Demo = () => { 46 | return ( 47 | 51 | ) 52 | } 53 | 54 | export default () => { 55 | return ( 56 |
57 |
58 | Default: 59 | 60 | 61 | 62 |
63 |
64 | Dark: 65 | 66 | 67 | 68 |
69 |
70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const compileModules = ['antd', '@babel']; 2 | 3 | const ignoreList = []; 4 | 5 | // cnpm use `_` as prefix 6 | ['', '_'].forEach(prefix => { 7 | compileModules.forEach(module => { 8 | ignoreList.push(`${prefix}${module}`); 9 | }); 10 | }); 11 | 12 | const transformIgnorePatterns = [ 13 | // Ignore modules without es dir. 14 | // Update: @babel/runtime should also be transformed 15 | `/node_modules/(?!${ignoreList.join('|')})[^/]+?/(?!(es)/)`, 16 | ]; 17 | 18 | module.exports = { 19 | setupFilesAfterEnv: ['/tests/setupAfterEnv.ts'], 20 | transformIgnorePatterns, 21 | }; 22 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "@ant-design/compatible", 4 | "builds": [ 5 | { 6 | "src": "package.json", 7 | "use": "@now/static-build", 8 | "config": { "distDir": ".doc" } 9 | } 10 | ], 11 | "routes": [ 12 | { "src": "/(.*)", "dest": "/dist/$1" } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ant-design/compatible", 3 | "version": "5.1.4", 4 | "description": "Ant Design v3 to v4 compatible package", 5 | "keywords": [ 6 | "antd", 7 | "compatible" 8 | ], 9 | "files": [ 10 | "lib", 11 | "es", 12 | "assets/*.css", 13 | "assets/*.less" 14 | ], 15 | "sideEffects": [ 16 | "es/**/style/*", 17 | "lib/**/style/*", 18 | "assets/*" 19 | ], 20 | "main": "lib/index.js", 21 | "module": "es/index.js", 22 | "repository": "https://github.com/ant-design/compatible", 23 | "publishConfig": { 24 | "registry": "https://registry.npmjs.org" 25 | }, 26 | "scripts": { 27 | "start": "dumi dev", 28 | "dev": "father dev", 29 | "docs:build": "dumi build", 30 | "docs:deploy": "gh-pages -d docs-dist", 31 | "compile": "father build", 32 | "deploy": "npm run docs:build && npm run docs:deploy", 33 | "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"", 34 | "test": "umi-test", 35 | "test:coverage": "umi-test --coverage", 36 | "prepublishOnly": "npm run compile && np --no-cleanup --yolo --no-publish", 37 | "lint": "eslint src/ --ext .tsx,.ts", 38 | "lint:tsc": "tsc -p tsconfig.json --noEmit", 39 | "now-build": "npm run docs:build", 40 | "watch": "father dev" 41 | }, 42 | "license": "MIT", 43 | "dependencies": { 44 | "@ant-design/cssinjs": "^1.23.0", 45 | "@ctrl/tinycolor": "^3.6.1", 46 | "classnames": "^2.2.6", 47 | "dayjs": "^1.11.4", 48 | "lodash.camelcase": "^4.3.0", 49 | "lodash.upperfirst": "^4.3.1", 50 | "rc-animate": "^3.1.1", 51 | "rc-form": "^2.4.12", 52 | "rc-util": "^5.24.5" 53 | }, 54 | "peerDependencies": { 55 | "antd": "^5.0.1", 56 | "react": ">=16.0.0", 57 | "react-dom": ">=16.0.0" 58 | }, 59 | "devDependencies": { 60 | "@ant-design/icons": "^4.7.0", 61 | "@rc-component/father-plugin": "^1.0.1", 62 | "@testing-library/jest-dom": "^5.16.4", 63 | "@testing-library/react": "^13.0.0", 64 | "@types/enzyme": "^3.10.5", 65 | "@types/jest": "^26.0.20", 66 | "@types/less": "^3.0.3", 67 | "@types/lodash": "^4.14.135", 68 | "@types/react": "^18.0.0", 69 | "@types/react-dom": "^18.0.0", 70 | "@umijs/fabric": "^2.5.2", 71 | "antd": "^5.0.1", 72 | "dumi": "^1.1.0", 73 | "eslint": "^7.18.0", 74 | "father": "^4.0.0", 75 | "gh-pages": "^3.1.0", 76 | "less": "^4.1.3", 77 | "np": "^5.0.3", 78 | "prettier": "^2.1.2", 79 | "rc-trigger": "^5.3.1", 80 | "react": "^18.0.0", 81 | "react-dom": "^18.0.0", 82 | "typescript": "^4.6.3", 83 | "umi-test": "^1.9.7" 84 | }, 85 | "overrides": { 86 | "cheerio": "1.0.0-rc.12" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/CompatibleConsumer.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigProvider } from 'antd'; 2 | import type { ConfigConsumerProps } from 'antd/lib/config-provider'; 3 | 4 | export type { ConfigConsumerProps }; 5 | 6 | const MergedConfigConsumer = ConfigProvider.ConfigContext.Consumer; 7 | 8 | export default MergedConfigConsumer; 9 | -------------------------------------------------------------------------------- /src/_util/types.ts: -------------------------------------------------------------------------------- 1 | export type Omit = Pick>; 2 | 3 | export const tuple = (...args: T) => args; 4 | -------------------------------------------------------------------------------- /src/_util/upgradeMessage.ts: -------------------------------------------------------------------------------- 1 | import warning from './warning'; 2 | 3 | export default (component: string) => 4 | warning( 5 | false, 6 | component, 7 | `The legacy component has been deprecated, and ant design 4.0 now released! Please follow https://ant.design/components/${component.toLowerCase()}${ 8 | component === 'Mention' ? 's' : '' 9 | } to upgrade.`, 10 | ); 11 | -------------------------------------------------------------------------------- /src/_util/warning.ts: -------------------------------------------------------------------------------- 1 | import warning from 'rc-util/lib/warning'; 2 | 3 | export default (valid: boolean, component: string, message: string): void => { 4 | warning(valid, `[antd-compatible: ${component}] ${message}`); 5 | }; 6 | -------------------------------------------------------------------------------- /src/comment/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import * as React from 'react'; 3 | import { ConfigProvider } from 'antd'; 4 | 5 | import useStyle from './style'; 6 | 7 | const { ConfigContext } = ConfigProvider; 8 | 9 | export interface CommentProps { 10 | /** List of action items rendered below the comment content */ 11 | actions?: React.ReactNode[]; 12 | /** The element to display as the comment author. */ 13 | author?: React.ReactNode; 14 | /** The element to display as the comment avatar - generally an antd Avatar */ 15 | avatar?: React.ReactNode; 16 | /** ClassName of comment */ 17 | className?: string; 18 | /** The main content of the comment */ 19 | content: React.ReactNode; 20 | /** Nested comments should be provided as children of the Comment */ 21 | children?: React.ReactNode; 22 | /** Comment prefix defaults to '.ant-comment' */ 23 | prefixCls?: string; 24 | /** Additional style for the comment */ 25 | style?: React.CSSProperties; 26 | /** A datetime element containing the time to be displayed */ 27 | datetime?: React.ReactNode; 28 | } 29 | 30 | const Comment: React.FC = ({ 31 | actions, 32 | author, 33 | avatar, 34 | children, 35 | className, 36 | content, 37 | prefixCls: customizePrefixCls, 38 | datetime, 39 | ...otherProps 40 | }) => { 41 | const { getPrefixCls, direction } = React.useContext(ConfigContext); 42 | 43 | const renderNested = (prefixCls: string, nestedChildren: any) => ( 44 |
{nestedChildren}
45 | ); 46 | 47 | const prefixCls = getPrefixCls('comment', customizePrefixCls); 48 | const [wrapSSR, hashId] = useStyle(prefixCls); 49 | 50 | const avatarDom = avatar ? ( 51 |
52 | {typeof avatar === 'string' ? comment-avatar : avatar} 53 |
54 | ) : null; 55 | 56 | const actionDom = 57 | actions && actions.length ? ( 58 |
    59 | {actions.map((action, index) => ( 60 |
  • {action}
  • // eslint-disable-line react/no-array-index-key 61 | ))} 62 |
63 | ) : null; 64 | 65 | const authorContent = (author || datetime) && ( 66 |
67 | {author && {author}} 68 | {datetime && {datetime}} 69 |
70 | ); 71 | 72 | const contentDom = ( 73 |
74 | {authorContent} 75 |
{content}
76 | {actionDom} 77 |
78 | ); 79 | 80 | const cls = classNames( 81 | prefixCls, 82 | { 83 | [`${prefixCls}-rtl`]: direction === 'rtl', 84 | }, 85 | className, 86 | hashId, 87 | ); 88 | 89 | return wrapSSR( 90 |
91 |
92 | {avatarDom} 93 | {contentDom} 94 |
95 | {children ? renderNested(prefixCls, children) : null} 96 |
, 97 | ); 98 | }; 99 | 100 | export default Comment; 101 | -------------------------------------------------------------------------------- /src/comment/style/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import type { CSSInterpolation } from '@ant-design/cssinjs'; 3 | import { useStyleRegister } from '@ant-design/cssinjs'; 4 | import { theme as antdTheme, ConfigProvider } from 'antd'; 5 | import type { GlobalToken } from 'antd/lib/theme/interface'; 6 | import { resetComponent } from 'antd/lib/style'; 7 | 8 | interface MergedToken extends GlobalToken { 9 | componentCls: string; 10 | } 11 | 12 | // ============================== Export ============================== 13 | const genSharedButtonStyle = (token: MergedToken): CSSInterpolation => { 14 | const { 15 | componentCls, 16 | colorBgContainer, 17 | fontSize, 18 | fontSizeSM, 19 | padding, 20 | paddingXS, 21 | marginSM, 22 | marginXXS, 23 | controlHeight, 24 | lineHeightSM, 25 | colorText, 26 | colorTextSecondary, 27 | colorTextTertiary, 28 | motionDurationSlow, 29 | } = token; 30 | 31 | return { 32 | [componentCls]: { 33 | ...resetComponent(token) as any, 34 | 35 | position: 'relative', 36 | backgroundColor: colorBgContainer, 37 | 38 | [`${componentCls}-inner`]: { 39 | display: 'flex', 40 | paddingBlock: padding, 41 | }, 42 | 43 | [`${componentCls}-avatar`]: { 44 | position: 'relative', 45 | flexShrink: 0, 46 | marginInlineEnd: marginSM, 47 | cursor: 'pointer', 48 | 49 | img: { 50 | width: controlHeight, 51 | height: controlHeight, 52 | borderRadius: '50%', 53 | }, 54 | }, 55 | 56 | [`${componentCls}-content`]: { 57 | position: 'relative', 58 | flex: 'auto', 59 | minWidth: 0, 60 | wordWrap: 'break-word', 61 | 62 | '&-author': { 63 | display: 'flex', 64 | flexWrap: 'wrap', 65 | justifyContent: 'flex-start', 66 | marginBottom: marginXXS, 67 | 68 | '& > a, & > span': { 69 | paddingInlineEnd: paddingXS, 70 | fontSize: fontSizeSM, 71 | lineHeight: lineHeightSM, 72 | }, 73 | 74 | '&-name': { 75 | color: colorTextSecondary, 76 | fontSize, 77 | transition: `color ${motionDurationSlow}`, 78 | 79 | '> *': { 80 | color: colorTextSecondary, 81 | '&:hover': { 82 | color: colorTextSecondary, 83 | }, 84 | }, 85 | }, 86 | '&-time': { 87 | color: colorTextTertiary, 88 | whiteSpace: 'nowrap', 89 | cursor: 'auto', 90 | }, 91 | }, 92 | '&-detail p': { 93 | whiteSpace: 'pre-wrap', 94 | marginBlock: 0, 95 | }, 96 | }, 97 | 98 | [`${componentCls}-actions`]: { 99 | marginTop: marginSM, 100 | marginBottom: 0, 101 | paddingInlineStart: 0, 102 | 103 | '> li': { 104 | display: 'inline-block', 105 | color: colorTextSecondary, 106 | 107 | '> span': { 108 | marginInlineEnd: marginSM, 109 | color: colorTextSecondary, 110 | fontSize: fontSizeSM, 111 | cursor: 'pointer', 112 | transition: `color ${motionDurationSlow}`, 113 | userSelect: 'none', 114 | 115 | '&:hover': { 116 | color: colorText, 117 | }, 118 | }, 119 | }, 120 | }, 121 | 122 | [`${componentCls}-nested`]: { 123 | marginInlineStart: 44, 124 | }, 125 | }, 126 | }; 127 | }; 128 | 129 | export default function useStyle( 130 | prefixCls: string, 131 | ): [(node: React.ReactNode) => React.ReactElement, string] { 132 | const { theme, token, hashId } = antdTheme.useToken(); 133 | const { iconPrefixCls } = React.useContext(ConfigProvider.ConfigContext); 134 | 135 | return [ 136 | useStyleRegister( 137 | { theme, token, hashId, path: ['compatible', 'Comment', prefixCls, iconPrefixCls] }, 138 | () => { 139 | const mergedToken = { 140 | componentCls: `.${prefixCls}`, 141 | ...token, 142 | }; 143 | return [genSharedButtonStyle(mergedToken)]; 144 | }, 145 | ), 146 | hashId, 147 | ]; 148 | } 149 | -------------------------------------------------------------------------------- /src/form/Form.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/method-signature-style, @typescript-eslint/unified-signatures */ 2 | import * as React from 'react'; 3 | import classNames from 'classnames'; 4 | import createDOMForm from 'rc-form/lib/createDOMForm'; 5 | import createFormField from 'rc-form/lib/createFormField'; 6 | import omit from 'rc-util/lib/omit'; 7 | import type { ColProps } from 'antd/lib/grid/col'; 8 | import { tuple } from '../_util/types'; 9 | import warning from '../_util/warning'; 10 | import FormItem from './FormItem'; 11 | import type { FormLabelAlign } from './FormItem'; 12 | import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants'; 13 | import FormContext from './context'; 14 | import type { FormWrappedProps } from './interface'; 15 | import upgradeMessage from '../_util/upgradeMessage'; 16 | import CompatibleConsumer from '../CompatibleConsumer'; 17 | import type { ConfigConsumerProps } from '../CompatibleConsumer'; 18 | import useStyle from './style'; 19 | import { ConfigContext } from 'antd/lib/config-provider'; 20 | 21 | type FormCreateOptionMessagesCallback = (...args: any[]) => string; 22 | 23 | interface FormCreateOptionMessages { 24 | [messageId: string]: string | FormCreateOptionMessagesCallback | FormCreateOptionMessages; 25 | } 26 | 27 | export interface FormCreateOption { 28 | onFieldsChange?: (props: T, fields: any, allFields: any) => void; 29 | onValuesChange?: (props: T, changedValues: any, allValues: any) => void; 30 | mapPropsToFields?: (props: T) => void; 31 | validateMessages?: FormCreateOptionMessages; 32 | withRef?: boolean; 33 | name?: string; 34 | } 35 | 36 | const FormLayouts = tuple('horizontal', 'inline', 'vertical'); 37 | export type FormLayout = typeof FormLayouts[number]; 38 | 39 | export interface FormProps extends React.FormHTMLAttributes { 40 | layout?: FormLayout; 41 | form?: WrappedFormUtils; 42 | onSubmit?: React.FormEventHandler; 43 | style?: React.CSSProperties; 44 | className?: string; 45 | prefixCls?: string; 46 | hideRequiredMark?: boolean; 47 | /** 48 | * @since 3.14.0 49 | */ 50 | wrapperCol?: ColProps; 51 | labelCol?: ColProps; 52 | /** 53 | * @since 3.15.0 54 | */ 55 | colon?: boolean; 56 | labelAlign?: FormLabelAlign; 57 | } 58 | 59 | export interface ValidationRule { 60 | /** validation error message */ 61 | message?: React.ReactNode; 62 | /** built-in validation type, available options: https://github.com/yiminghe/async-validator#type */ 63 | type?: string; 64 | /** indicates whether field is required */ 65 | required?: boolean; 66 | /** treat required fields that only contain whitespace as errors */ 67 | whitespace?: boolean; 68 | /** validate the exact length of a field */ 69 | len?: number; 70 | /** validate the min length of a field */ 71 | min?: number; 72 | /** validate the max length of a field */ 73 | max?: number; 74 | /** validate the value from a list of possible values */ 75 | enum?: string | string[]; 76 | /** validate from a regular expression */ 77 | pattern?: RegExp; 78 | /** transform a value before validation */ 79 | transform?: (value: any) => any; 80 | /** custom validate function (Note: callback must be called) */ 81 | validator?: (rule: any, value: any, callback: any, source?: any, options?: any) => any; 82 | } 83 | 84 | export type ValidateCallback = (errors: any, values: V) => void; 85 | 86 | export interface GetFieldDecoratorOptions { 87 | /** 子节点的值的属性,如 Checkbox 的是 'checked' */ 88 | valuePropName?: string; 89 | /** 子节点的初始值,类型、可选值均由子节点决定 */ 90 | initialValue?: any; 91 | /** 收集子节点的值的时机 */ 92 | trigger?: string; 93 | /** 可以把 onChange 的参数转化为控件的值,例如 DatePicker 可设为:(date, dateString) => dateString */ 94 | getValueFromEvent?: (...args: any[]) => any; 95 | /** Get the component props according to field value. */ 96 | getValueProps?: (value: any) => any; 97 | /** 校验子节点值的时机 */ 98 | validateTrigger?: string | string[]; 99 | /** 校验规则,参见 [async-validator](https://github.com/yiminghe/async-validator) */ 100 | rules?: ValidationRule[]; 101 | /** 是否和其他控件互斥,特别用于 Radio 单选控件 */ 102 | exclusive?: boolean; 103 | /** Normalize value to form component */ 104 | normalize?: (value: any, prevValue: any, allValues: any) => any; 105 | /** Whether stop validate on first rule of error for this field. */ 106 | validateFirst?: boolean; 107 | /** 是否一直保留子节点的信息 */ 108 | preserve?: boolean; 109 | } 110 | 111 | /** dom-scroll-into-view 组件配置参数 */ 112 | export interface DomScrollIntoViewConfig { 113 | /** 是否和左边界对齐 */ 114 | alignWithLeft?: boolean; 115 | /** 是否和上边界对齐 */ 116 | alignWithTop?: boolean; 117 | /** 顶部偏移量 */ 118 | offsetTop?: number; 119 | /** 左侧偏移量 */ 120 | offsetLeft?: number; 121 | /** 底部偏移量 */ 122 | offsetBottom?: number; 123 | /** 右侧偏移量 */ 124 | offsetRight?: number; 125 | /** 是否允许容器水平滚动 */ 126 | allowHorizontalScroll?: boolean; 127 | /** 当内容可见时是否允许滚动容器 */ 128 | onlyScrollIfNeeded?: boolean; 129 | } 130 | 131 | export interface ValidateFieldsOptions { 132 | /** 所有表单域是否在第一个校验规则失败后停止继续校验 */ 133 | first?: boolean; 134 | /** 指定哪些表单域在第一个校验规则失败后停止继续校验 */ 135 | firstFields?: string[]; 136 | /** 已经校验过的表单域,在 validateTrigger 再次被触发时是否再次校验 */ 137 | force?: boolean; 138 | /** 定义 validateFieldsAndScroll 的滚动行为 */ 139 | scroll?: DomScrollIntoViewConfig; 140 | } 141 | 142 | // function create 143 | export interface WrappedFormUtils { 144 | /** 获取一组输入控件的值,如不传入参数,则获取全部组件的值 */ 145 | getFieldsValue(fieldNames?: string[]): Record; 146 | /** 获取一个输入控件的值 */ 147 | getFieldValue(fieldName: string): any; 148 | /** 设置一组输入控件的值 */ 149 | setFieldsValue(obj: Object, callback?: Function): void; 150 | /** 设置一组输入控件的值 */ 151 | setFields(obj: Object): void; 152 | /** 校验并获取一组输入域的值与 Error */ 153 | validateFields( 154 | fieldNames: string[], 155 | options: ValidateFieldsOptions, 156 | callback: ValidateCallback, 157 | ): void; 158 | validateFields(options: ValidateFieldsOptions, callback: ValidateCallback): void; 159 | validateFields(fieldNames: string[], callback: ValidateCallback): void; 160 | validateFields(fieldNames: string[], options: ValidateFieldsOptions): void; 161 | validateFields(fieldNames: string[]): void; 162 | validateFields(callback: ValidateCallback): void; 163 | validateFields(options: ValidateFieldsOptions): void; 164 | validateFields(): void; 165 | /** 与 `validateFields` 相似,但校验完后,如果校验不通过的菜单域不在可见范围内,则自动滚动进可见范围 */ 166 | validateFieldsAndScroll( 167 | fieldNames: string[], 168 | options: ValidateFieldsOptions, 169 | callback: ValidateCallback, 170 | ): void; 171 | validateFieldsAndScroll(options: ValidateFieldsOptions, callback: ValidateCallback): void; 172 | validateFieldsAndScroll(fieldNames: string[], callback: ValidateCallback): void; 173 | validateFieldsAndScroll(fieldNames: string[], options: ValidateFieldsOptions): void; 174 | validateFieldsAndScroll(fieldNames: string[]): void; 175 | validateFieldsAndScroll(callback: ValidateCallback): void; 176 | validateFieldsAndScroll(options: ValidateFieldsOptions): void; 177 | validateFieldsAndScroll(): void; 178 | /** 获取某个输入控件的 Error */ 179 | getFieldError(name: string): string[] | undefined; 180 | getFieldsError(names?: string[]): Record; 181 | /** 判断一个输入控件是否在校验状态 */ 182 | isFieldValidating(name: string): boolean; 183 | isFieldTouched(name: string): boolean; 184 | isFieldsTouched(names?: string[]): boolean; 185 | /** 重置一组输入控件的值与状态,如不传入参数,则重置所有组件 */ 186 | resetFields(names?: string[]): void; 187 | // tslint:disable-next-line:max-line-length 188 | getFieldDecorator( 189 | id: keyof T, 190 | options?: GetFieldDecoratorOptions, 191 | ): (node: React.ReactNode) => React.ReactNode; 192 | } 193 | 194 | export interface WrappedFormInternalProps { 195 | form: WrappedFormUtils; 196 | } 197 | 198 | export interface RcBaseFormProps { 199 | wrappedComponentRef?: any; 200 | } 201 | 202 | export interface FormComponentProps extends WrappedFormInternalProps, RcBaseFormProps { 203 | form: WrappedFormUtils; 204 | } 205 | 206 | class Form extends React.Component { 207 | constructor(props: FormProps) { 208 | super(props); 209 | 210 | warning(!props.form, 'Form', 'It is unnecessary to pass `form` to `Form` after antd@1.7.0.'); 211 | 212 | upgradeMessage('Form'); 213 | } 214 | 215 | componentDidMount() { 216 | try { 217 | warning( 218 | getComputedStyle(document.querySelector('.ant-col'), null).getPropertyValue('position') === 219 | 'relative', 220 | 'Form', 221 | 'If missing `Grid` style, you should import it, Please follow https://github.com/ant-design/compatible#faq.', 222 | ); 223 | } catch (error) { 224 | warning(false, 'Form', error); 225 | } 226 | } 227 | 228 | renderForm = ({ getPrefixCls }: ConfigConsumerProps) => { 229 | const { prefixCls: customizePrefixCls, hideRequiredMark, className = '', layout } = this.props; 230 | const prefixCls = getPrefixCls('legacy-form', customizePrefixCls); 231 | const formClassName = classNames( 232 | prefixCls, 233 | { 234 | [`${prefixCls}-horizontal`]: layout === 'horizontal', 235 | [`${prefixCls}-vertical`]: layout === 'vertical', 236 | [`${prefixCls}-inline`]: layout === 'inline', 237 | [`${prefixCls}-hide-required-mark`]: hideRequiredMark, 238 | }, 239 | className, 240 | ); 241 | 242 | const formProps = omit(this.props, [ 243 | 'prefixCls', 244 | 'className', 245 | 'layout', 246 | 'form', 247 | 'hideRequiredMark', 248 | 'wrapperCol', 249 | 'labelAlign', 250 | 'labelCol', 251 | 'colon', 252 | ]); 253 | 254 | return
; 255 | }; 256 | 257 | render() { 258 | const { wrapperCol, labelAlign, labelCol, layout, colon } = this.props; 259 | return ( 260 | 269 | {this.renderForm} 270 | 271 | ); 272 | } 273 | } 274 | 275 | const FormFC = React.forwardRef((props: FormProps, ref: any) => { 276 | const { prefixCls: customizePrefixCls, className } = props; 277 | const { getPrefixCls } = React.useContext(ConfigContext); 278 | const prefixCls = getPrefixCls('legacy-form', customizePrefixCls); 279 | const [wrapSSR, hashId] = useStyle(prefixCls); 280 | 281 | return wrapSSR( 282 | , 283 | ); 284 | }) as React.ForwardRefExoticComponent> & { 285 | Item: typeof FormItem; 286 | createFormField: typeof createFormField; 287 | create: typeof create; 288 | }; 289 | 290 | function create( 291 | options: FormCreateOption = {}, 292 | ): FormWrappedProps { 293 | return createDOMForm({ 294 | fieldNameProp: 'id', 295 | ...options, 296 | fieldMetaProp: FIELD_META_PROP, 297 | fieldDataProp: FIELD_DATA_PROP, 298 | }); 299 | } 300 | 301 | FormFC.defaultProps = { 302 | colon: true, 303 | layout: 'horizontal' as FormLayout, 304 | hideRequiredMark: false, 305 | onSubmit(e: React.FormEvent) { 306 | e.preventDefault(); 307 | }, 308 | }; 309 | 310 | FormFC.Item = FormItem; 311 | 312 | FormFC.createFormField = createFormField; 313 | 314 | FormFC.create = create; 315 | 316 | if (process.env.NODE_ENV !== 'production') { 317 | FormFC.displayName = 'Form'; 318 | } 319 | 320 | export default FormFC; 321 | -------------------------------------------------------------------------------- /src/form/FormItem.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-find-dom-node */ 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import classNames from 'classnames'; 5 | // import Animate from 'rc-animate'; 6 | import omit from 'rc-util/lib/omit'; 7 | import { Row, Col, Form as V5Form } from 'antd'; 8 | import type { FormItemInputContext } from 'antd/lib/form/context'; 9 | import { 10 | CheckCircleFilled, 11 | CloseCircleFilled, 12 | ExclamationCircleFilled, 13 | LoadingOutlined, 14 | } from '@ant-design/icons'; 15 | import type { ColProps } from 'antd/lib/grid/col'; 16 | import CompatibleConsumer from '../CompatibleConsumer'; 17 | import type { ConfigConsumerProps } from '../CompatibleConsumer'; 18 | import warning from '../_util/warning'; 19 | import { tuple } from '../_util/types'; 20 | import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants'; 21 | import FormContext from './context'; 22 | import type { FormContextProps } from './context'; 23 | 24 | const V5FormItemInputContext = (V5Form.Item.useStatus as any) 25 | .Context as typeof FormItemInputContext; 26 | 27 | const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', ''); 28 | 29 | const FormLabelAligns = tuple('left', 'right'); 30 | 31 | export type FormLabelAlign = typeof FormLabelAligns[number]; 32 | 33 | const IconMap = { 34 | success: CheckCircleFilled, 35 | warning: ExclamationCircleFilled, 36 | error: CloseCircleFilled, 37 | validating: LoadingOutlined, 38 | }; 39 | 40 | export interface FormItemProps { 41 | prefixCls?: string; 42 | className?: string; 43 | id?: string; 44 | htmlFor?: string; 45 | label?: React.ReactNode; 46 | name?: string; 47 | labelAlign?: FormLabelAlign; 48 | labelCol?: ColProps; 49 | wrapperCol?: ColProps; 50 | help?: React.ReactNode; 51 | extra?: React.ReactNode; 52 | validateStatus?: typeof ValidateStatuses[number]; 53 | hasFeedback?: boolean; 54 | required?: boolean; 55 | style?: React.CSSProperties; 56 | colon?: boolean; 57 | children?: React.ReactNode; 58 | } 59 | 60 | function intersperseSpace(list: T[]): (T | string)[] { 61 | return list.reduce((current, item) => [...current, ' ', item], []).slice(1); 62 | } 63 | 64 | export default class FormItem extends React.Component { 65 | helpShow = false; 66 | 67 | static defaultProps = { 68 | hasFeedback: false, 69 | }; 70 | 71 | componentDidMount() { 72 | const { children, help, validateStatus, id } = this.props; 73 | warning( 74 | this.getControls(children, true).length <= 1 || 75 | help !== undefined || 76 | validateStatus !== undefined, 77 | 'Form.Item', 78 | 'Cannot generate `validateStatus` and `help` automatically, ' + 79 | 'while there are more than one `getFieldDecorator` in it.', 80 | ); 81 | 82 | warning( 83 | !id, 84 | 'Form.Item', 85 | '`id` is deprecated for its label `htmlFor`. Please use `htmlFor` directly.', 86 | ); 87 | } 88 | 89 | getHelpMessage(): React.ReactNode { 90 | const { help } = this.props; 91 | if (help === undefined && this.getOnlyControl()) { 92 | const { errors } = this.getField(); 93 | if (errors) { 94 | return intersperseSpace( 95 | errors.map((e: any, index: number) => { 96 | let node: React.ReactElement | null = null; 97 | 98 | if (React.isValidElement(e)) { 99 | node = e; 100 | } else if (React.isValidElement(e.message)) { 101 | node = e.message; 102 | } 103 | // eslint-disable-next-line react/no-array-index-key 104 | return node ? React.cloneElement(node, { key: index }) : e.message; 105 | }), 106 | ); 107 | } 108 | return ''; 109 | } 110 | return help; 111 | } 112 | 113 | getControls(children: React.ReactNode, recursively: boolean) { 114 | let controls: React.ReactElement[] = []; 115 | const childrenArray = React.Children.toArray(children); 116 | for (let i = 0; i < childrenArray.length; i += 1) { 117 | if (!recursively && controls.length > 0) { 118 | break; 119 | } 120 | 121 | const child = childrenArray[i] as React.ReactElement; 122 | if ( 123 | child.type && 124 | ((child.type as any) === FormItem || (child.type as any).displayName === 'FormItem') 125 | ) { 126 | continue; 127 | } 128 | if (!child.props) { 129 | continue; 130 | } 131 | if (FIELD_META_PROP in child.props) { 132 | // And means FIELD_DATA_PROP in child.props, too. 133 | controls.push(child); 134 | } else if (child.props.children) { 135 | controls = controls.concat(this.getControls(child.props.children, recursively)); 136 | } 137 | } 138 | return controls; 139 | } 140 | 141 | getOnlyControl() { 142 | const child = this.getControls(this.props.children, false)[0]; 143 | return child !== undefined ? child : null; 144 | } 145 | 146 | getChildProp(prop: string) { 147 | const child = this.getOnlyControl() as React.ReactElement; 148 | return child && child.props && child.props[prop]; 149 | } 150 | 151 | getId() { 152 | return this.getChildProp('id'); 153 | } 154 | 155 | getMeta() { 156 | return this.getChildProp(FIELD_META_PROP); 157 | } 158 | 159 | getField() { 160 | return this.getChildProp(FIELD_DATA_PROP); 161 | } 162 | 163 | getValidateStatus() { 164 | const onlyControl = this.getOnlyControl(); 165 | if (!onlyControl) { 166 | return ''; 167 | } 168 | const field = this.getField(); 169 | if (field.validating) { 170 | return 'validating'; 171 | } 172 | if (field.errors) { 173 | return 'error'; 174 | } 175 | const fieldValue = 'value' in field ? field.value : this.getMeta().initialValue; 176 | if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') { 177 | return 'success'; 178 | } 179 | return ''; 180 | } 181 | 182 | // Resolve duplicated ids bug between different forms 183 | // https://github.com/ant-design/ant-design/issues/7351 184 | onLabelClick = () => { 185 | const id = this.props.id || this.getId(); 186 | if (!id) { 187 | return; 188 | } 189 | 190 | const formItemNode = ReactDOM.findDOMNode(this) as Element; 191 | const control = formItemNode.querySelector(`[id="${id}"]`) as HTMLElement; 192 | if (control && control.focus) { 193 | control.focus(); 194 | } 195 | }; 196 | 197 | // onHelpAnimEnd = (_key: string, helpShow: boolean) => { 198 | // this.helpShow = helpShow; 199 | // if (!helpShow) { 200 | // this.setState({}); 201 | // } 202 | // }; 203 | 204 | isRequired() { 205 | const { required } = this.props; 206 | if (required !== undefined) { 207 | return required; 208 | } 209 | if (this.getOnlyControl()) { 210 | const meta = this.getMeta() || {}; 211 | const validate = meta.validate || []; 212 | 213 | return validate 214 | .filter((item: any) => !!item.rules) 215 | .some((item: any) => item.rules.some((rule: any) => rule.required)); 216 | } 217 | return false; 218 | } 219 | 220 | renderHelp(prefixCls: string) { 221 | const help = this.getHelpMessage(); 222 | const children = help ? ( 223 |
224 | {help} 225 |
226 | ) : null; 227 | if (children) { 228 | this.helpShow = !!children; 229 | } 230 | 231 | return children; 232 | // return ( 233 | // 240 | // {children} 241 | // 242 | // ); 243 | } 244 | 245 | renderExtra(prefixCls: string) { 246 | const { extra } = this.props; 247 | return extra ?
{extra}
: null; 248 | } 249 | 250 | renderValidateWrapper( 251 | prefixCls: string, 252 | c1: React.ReactNode, 253 | c2: React.ReactNode, 254 | c3: React.ReactNode, 255 | ) { 256 | const { hasFeedback, validateStatus } = this.props; 257 | const onlyControl = this.getOnlyControl; 258 | const mergedValidateStatus = 259 | validateStatus === undefined && onlyControl ? this.getValidateStatus() : validateStatus; 260 | 261 | let classes = `${prefixCls}-item-control`; 262 | if (mergedValidateStatus) { 263 | classes = classNames(`${prefixCls}-item-control`, { 264 | 'has-feedback': hasFeedback || mergedValidateStatus === 'validating', 265 | 'has-success': mergedValidateStatus === 'success', 266 | 'has-warning': mergedValidateStatus === 'warning', 267 | 'has-error': mergedValidateStatus === 'error', 268 | 'is-validating': mergedValidateStatus === 'validating', 269 | }); 270 | } 271 | 272 | // let iconType: React.ReactNode = null; 273 | // switch (validateStatus) { 274 | // case 'success': 275 | // iconType = ; 276 | // break; 277 | // case 'warning': 278 | // iconType = ; 279 | // break; 280 | // case 'error': 281 | // iconType = ; 282 | // break; 283 | // case 'validating': 284 | // iconType = ; 285 | // break; 286 | // default: 287 | // break; 288 | // } 289 | 290 | // const icon = 291 | // props.hasFeedback && iconType ? ( 292 | // {iconType} 293 | // ) : null; 294 | 295 | // ========================== Feedback ========================== 296 | const IconComponent = mergedValidateStatus && IconMap[mergedValidateStatus]; 297 | const feedbackIcon = IconComponent ? ( 298 | 304 | 305 | 306 | ) : null; 307 | 308 | return ( 309 |
310 | 311 | 319 | {c1} 320 | 321 | {/* {icon} */} 322 | 323 | {c2} 324 | {c3} 325 |
326 | ); 327 | } 328 | 329 | renderWrapper(prefixCls: string, children: React.ReactNode) { 330 | return ( 331 | 332 | {({ wrapperCol: contextWrapperCol, vertical }: FormContextProps) => { 333 | const { wrapperCol } = this.props; 334 | const mergedWrapperCol: ColProps = 335 | ('wrapperCol' in this.props ? wrapperCol : contextWrapperCol) || {}; 336 | 337 | const className = classNames( 338 | `${prefixCls}-item-control-wrapper`, 339 | mergedWrapperCol.className, 340 | ); 341 | 342 | // No pass FormContext since it's useless 343 | return ( 344 | 345 | 346 | {children} 347 | 348 | 349 | ); 350 | }} 351 | 352 | ); 353 | } 354 | 355 | renderLabel(prefixCls: string) { 356 | return ( 357 | 358 | {({ 359 | vertical, 360 | labelAlign: contextLabelAlign, 361 | labelCol: contextLabelCol, 362 | colon: contextColon, 363 | }: FormContextProps) => { 364 | const { label, labelCol, labelAlign, colon, id, htmlFor } = this.props; 365 | const required = this.isRequired(); 366 | 367 | const mergedLabelCol: ColProps = 368 | ('labelCol' in this.props ? labelCol : contextLabelCol) || {}; 369 | 370 | const mergedLabelAlign: FormLabelAlign | undefined = 371 | 'labelAlign' in this.props ? labelAlign : contextLabelAlign; 372 | 373 | const labelClsBasic = `${prefixCls}-item-label`; 374 | const labelColClassName = classNames( 375 | labelClsBasic, 376 | mergedLabelAlign === 'left' && `${labelClsBasic}-left`, 377 | mergedLabelCol.className, 378 | ); 379 | 380 | let labelChildren = label; 381 | // Keep label is original where there should have no colon 382 | const computedColon = colon === true || (contextColon !== false && colon !== false); 383 | const haveColon = computedColon && !vertical; 384 | // Remove duplicated user input colon 385 | if (haveColon && typeof label === 'string' && (label as string).trim() !== '') { 386 | labelChildren = (label as string).replace(/[::]\s*$/, ''); 387 | } 388 | 389 | const labelClassName = classNames({ 390 | [`${prefixCls}-item-required`]: required, 391 | [`${prefixCls}-item-no-colon`]: !computedColon, 392 | }); 393 | 394 | return label ? ( 395 | 396 | 404 | 405 | ) : null; 406 | }} 407 | 408 | ); 409 | } 410 | 411 | renderChildren(prefixCls: string) { 412 | const { children } = this.props; 413 | return [ 414 | this.renderLabel(prefixCls), 415 | this.renderWrapper( 416 | prefixCls, 417 | this.renderValidateWrapper( 418 | prefixCls, 419 | children, 420 | this.renderHelp(prefixCls), 421 | this.renderExtra(prefixCls), 422 | ), 423 | ), 424 | ]; 425 | } 426 | 427 | renderFormItem = ({ getPrefixCls }: ConfigConsumerProps) => { 428 | const { prefixCls: customizePrefixCls, style, className, ...restProps } = this.props; 429 | const prefixCls = getPrefixCls('legacy-form', customizePrefixCls); 430 | const children = this.renderChildren(prefixCls); 431 | const itemClassName = { 432 | [`${prefixCls}-item`]: true, 433 | [`${prefixCls}-item-with-help`]: this.helpShow, 434 | [`${className}`]: !!className, 435 | }; 436 | 437 | return ( 438 | 457 | {children} 458 | 459 | ); 460 | }; 461 | 462 | render() { 463 | return {this.renderFormItem}; 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /src/form/constants.tsx: -------------------------------------------------------------------------------- 1 | export const FIELD_META_PROP = 'data-__meta'; 2 | export const FIELD_DATA_PROP = 'data-__field'; 3 | -------------------------------------------------------------------------------- /src/form/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import type { ColProps } from 'antd/lib/grid/col'; 3 | import type { FormLabelAlign } from './FormItem'; 4 | 5 | export interface FormContextProps { 6 | vertical: boolean; 7 | colon?: boolean; 8 | labelAlign?: FormLabelAlign; 9 | labelCol?: ColProps; 10 | wrapperCol?: ColProps; 11 | } 12 | 13 | const FormContext = React.createContext({ 14 | labelAlign: 'right', 15 | vertical: false, 16 | }); 17 | 18 | export default FormContext; 19 | -------------------------------------------------------------------------------- /src/form/index.tsx: -------------------------------------------------------------------------------- 1 | import Form from './Form'; 2 | 3 | export type { 4 | FormProps, 5 | FormComponentProps, 6 | FormCreateOption, 7 | ValidateCallback, 8 | ValidationRule, 9 | } from './Form'; 10 | export type { FormItemProps } from './FormItem'; 11 | 12 | export default Form; 13 | -------------------------------------------------------------------------------- /src/form/interface.ts: -------------------------------------------------------------------------------- 1 | import type * as React from 'react'; 2 | import type { Omit } from '../_util/types'; 3 | import type { WrappedFormInternalProps } from './Form'; 4 | 5 | // Heavily copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/hoist-non-react-statics/index.d.ts 6 | // tslint:disable-next-line:class-name 7 | interface REACT_STATICS { 8 | childContextTypes: true; 9 | contextType: true; 10 | contextTypes: true; 11 | defaultProps: true; 12 | displayName: true; 13 | getDefaultProps: true; 14 | getDerivedStateFromError: true; 15 | getDerivedStateFromProps: true; 16 | mixins: true; 17 | propTypes: true; 18 | type: true; 19 | } 20 | 21 | // tslint:disable-next-line:class-name 22 | interface KNOWN_STATICS { 23 | name: true; 24 | length: true; 25 | prototype: true; 26 | caller: true; 27 | callee: true; 28 | arguments: true; 29 | arity: true; 30 | } 31 | 32 | // tslint:disable-next-line:class-name 33 | interface MEMO_STATICS { 34 | $$typeof: true; 35 | compare: true; 36 | defaultProps: true; 37 | displayName: true; 38 | propTypes: true; 39 | type: true; 40 | } 41 | 42 | // tslint:disable-next-line:class-name 43 | interface FORWARD_REF_STATICS { 44 | $$typeof: true; 45 | render: true; 46 | defaultProps: true; 47 | displayName: true; 48 | propTypes: true; 49 | } 50 | 51 | type NonReactStatics< 52 | S extends React.ComponentType, 53 | C extends Record = {} 54 | > = { 55 | [key in Exclude< 56 | keyof S, 57 | S extends React.MemoExoticComponent 58 | ? keyof MEMO_STATICS | keyof C 59 | : S extends React.ForwardRefExoticComponent 60 | ? keyof FORWARD_REF_STATICS | keyof C 61 | : keyof REACT_STATICS | keyof KNOWN_STATICS | keyof C 62 | >]: S[key]; 63 | }; 64 | 65 | // Copy from @types/react-redux https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-redux/index.d.ts 66 | export type Matching = { 67 | [P in keyof DecorationTargetProps]: P extends keyof InjectedProps 68 | ? InjectedProps[P] extends DecorationTargetProps[P] 69 | ? DecorationTargetProps[P] 70 | : InjectedProps[P] 71 | : DecorationTargetProps[P]; 72 | }; 73 | 74 | export type GetProps = C extends React.ComponentType ? P : never; 75 | 76 | export type ConnectedComponentClass< 77 | C extends React.ComponentType, 78 | P 79 | > = React.ComponentClass> & 80 | NonReactStatics & { 81 | WrappedComponent: C; 82 | }; 83 | 84 | export type FormWrappedProps = < 85 | C extends React.ComponentType>> 86 | >( 87 | component: C, 88 | ) => ConnectedComponentClass< 89 | C, 90 | Omit 91 | >; 92 | -------------------------------------------------------------------------------- /src/form/style/feedback.tsx: -------------------------------------------------------------------------------- 1 | import type { CSSInterpolation } from '@ant-design/cssinjs'; 2 | import type { MergedToken } from '.'; 3 | import { genFormControlValidation } from './mixin'; 4 | 5 | // export const showHelpMotion = ( 6 | // className: string, 7 | // inKeyframes: Keyframes, 8 | // outKeyframes: Keyframes, 9 | // duration: string, 10 | // ): CSSObject => { 11 | // return { 12 | // // .make-motion(@className, @keyframeName, @duration); 13 | // ...(initMotion(className, inKeyframes, outKeyframes, duration) as any), 14 | 15 | // [`.${className}-enter, .${className}-appear`]: { 16 | // opacity: 0, 17 | // // animation-timing-function: @ease-in-out; 18 | // }, 19 | // [`.${className}-leave`]: { 20 | // // animation-timing-function: @ease-in-out; 21 | // }, 22 | // }; 23 | // }; 24 | 25 | // export const helpIn = new Keyframes('legacyAntShowHelpIn', { 26 | // '0%': { 27 | // transform: 'translateY(-5px)', 28 | // opacity: '0', 29 | // }, 30 | // '100%': { 31 | // transform: 'translateY(0)', 32 | // opacity: '1', 33 | // }, 34 | // }); 35 | 36 | // export const helpOut = new Keyframes('legacyAntShowHelpOut', { 37 | // to: { 38 | // transform: 'translateY(-5px)', 39 | // opacity: '0', 40 | // }, 41 | // }); 42 | 43 | export const genFeedbackStyle = (token: MergedToken): CSSInterpolation => { 44 | const { componentCls, colorSuccess, colorInfo, colorWarning, colorError } = token; 45 | 46 | return { 47 | [componentCls]: { 48 | // ==================== Form Icon Status ===================== 49 | [`${componentCls}-item-feedback-icon-success`]: { 50 | color: colorSuccess, 51 | }, 52 | [`${componentCls}-item-feedback-icon-validating`]: { 53 | color: colorInfo, 54 | }, 55 | [`${componentCls}-item-feedback-icon-warning`]: { 56 | color: colorWarning, 57 | }, 58 | [`${componentCls}-item-feedback-icon-error`]: { 59 | color: colorError, 60 | }, 61 | 62 | // ================== Form Feedback Status =================== 63 | '.has-warning': { 64 | ...genFormControlValidation(componentCls, colorWarning), 65 | }, 66 | '.has-error': { 67 | ...genFormControlValidation(componentCls, colorError), 68 | }, 69 | 70 | // ...showHelpMotion('show-help', helpIn, helpOut, token.motionDurationSlow), 71 | }, 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /src/form/style/index.less: -------------------------------------------------------------------------------- 1 | // @import (reference) 'antd/dist/antd.less'; 2 | // @import './mixin'; 3 | 4 | // @form-prefix-cls: ~'@{ant-prefix}-legacy-form'; 5 | // @form-component-height: @input-height-base; 6 | // @form-component-max-height: @input-height-lg; 7 | // @form-feedback-icon-size: @font-size-base; 8 | // @form-help-margin-top: (@form-component-height - @form-component-max-height) / 2 + 9 | // 2px; 10 | // @form-explain-font-size: @font-size-base; 11 | // // Extends additional 1px to fix precision issue. 12 | // // https://github.com/ant-design/ant-design/issues/12803 13 | // // https://github.com/ant-design/ant-design/issues/8220 14 | // @form-explain-precision: 1px; 15 | // @form-explain-height: floor(@form-explain-font-size * @line-height-base); 16 | // @input-affix-width: 19px; 17 | // @outline-fade: 20%; 18 | 19 | // .@{form-prefix-cls} { 20 | // .reset-component; 21 | // .reset-form; 22 | // } 23 | 24 | // .@{form-prefix-cls}-item-required::before { 25 | // display: inline-block; 26 | // margin-right: 4px; 27 | // color: @label-required-color; 28 | // font-size: @font-size-base; 29 | // font-family: SimSun, sans-serif; 30 | // line-height: 1; 31 | // content: '*'; 32 | // .@{form-prefix-cls}-hide-required-mark & { 33 | // display: none; 34 | // } 35 | // } 36 | 37 | // .@{form-prefix-cls}-item-label > label { 38 | // color: @label-color; 39 | 40 | // &::after { 41 | // & when (@form-item-trailing-colon=true) { 42 | // content: ':'; 43 | // } 44 | // & when not (@form-item-trailing-colon=true) { 45 | // content: ' '; 46 | // } 47 | 48 | // position: relative; 49 | // top: -0.5px; 50 | // margin: 0 @form-item-label-colon-margin-right 0 51 | // @form-item-label-colon-margin-left; 52 | // } 53 | 54 | // &.@{form-prefix-cls}-item-no-colon::after { 55 | // content: ' '; 56 | // } 57 | // } 58 | 59 | // // Form items 60 | // // You should wrap labels and controls in .@{form-prefix-cls}-item for optimum spacing 61 | // .@{form-prefix-cls}-item { 62 | // label { 63 | // position: relative; 64 | 65 | // > .@{iconfont-css-prefix} { 66 | // font-size: @font-size-base; 67 | // vertical-align: top; 68 | // } 69 | // } 70 | 71 | // .reset-component; 72 | 73 | // margin-bottom: @form-item-margin-bottom; 74 | // vertical-align: top; 75 | 76 | // &-control { 77 | // position: relative; 78 | // line-height: @form-component-max-height; 79 | // .clearfix; 80 | // } 81 | 82 | // &-children { 83 | // position: relative; 84 | // } 85 | 86 | // &-with-help { 87 | // margin-bottom: max( 88 | // 0, 89 | // @form-item-margin-bottom - @form-explain-height - @form-help-margin-top 90 | // ); 91 | // } 92 | 93 | // &-label { 94 | // display: inline-block; 95 | // overflow: hidden; 96 | // line-height: @form-component-max-height - 0.0001px; 97 | // white-space: nowrap; 98 | // text-align: right; 99 | // vertical-align: middle; 100 | // flex-grow: 0; 101 | 102 | // &-left { 103 | // text-align: left; 104 | // } 105 | // } 106 | 107 | // &-control-wrapper { 108 | // flex: 1 1 0; 109 | // } 110 | 111 | // .@{ant-prefix}-switch { 112 | // margin: 2px 0 4px; 113 | // } 114 | // } 115 | 116 | // .@{form-prefix-cls}-explain, 117 | // .@{form-prefix-cls}-extra { 118 | // clear: both; 119 | // min-height: @form-explain-height + @form-explain-precision; 120 | // margin-top: @form-help-margin-top; 121 | // color: @text-color-secondary; 122 | // font-size: @form-explain-font-size; 123 | // line-height: @line-height-base; 124 | // transition: color 0.3s @ease-out; // sync input color transition 125 | // } 126 | 127 | // .@{form-prefix-cls}-explain { 128 | // margin-bottom: -@form-explain-precision; 129 | // } 130 | 131 | // .@{form-prefix-cls}-extra { 132 | // padding-top: 4px; 133 | // } 134 | 135 | // .@{form-prefix-cls}-text { 136 | // display: inline-block; 137 | // padding-right: 8px; 138 | // } 139 | 140 | // .@{form-prefix-cls}-split { 141 | // display: block; 142 | // text-align: center; 143 | // } 144 | 145 | // form { 146 | // .has-feedback { 147 | // .@{ant-prefix}-input { 148 | // padding-right: @input-padding-horizontal-base + @input-affix-width; 149 | // } 150 | 151 | // // https://github.com/ant-design/ant-design/issues/19884 152 | // .@{ant-prefix}-input-affix-wrapper { 153 | // .@{ant-prefix}-input-suffix { 154 | // padding-right: 18px; 155 | // } 156 | // .@{ant-prefix}-input { 157 | // padding-right: @input-padding-horizontal-base + @input-affix-width * 2; 158 | // } 159 | // &.@{ant-prefix}-input-affix-wrapper-input-with-clear-btn { 160 | // .@{ant-prefix}-input { 161 | // padding-right: @input-padding-horizontal-base + @input-affix-width * 3; 162 | // } 163 | // } 164 | // } 165 | 166 | // // Fix overlapping between feedback icon and