diff --git a/.antd-tools.config.js b/.antd-tools.config.js index 7118b50004..36b9b75986 100644 --- a/.antd-tools.config.js +++ b/.antd-tools.config.js @@ -1,195 +1,36 @@ const fs = require('fs'); const path = require('path'); -const defaultVars = require('./scripts/default-vars'); -const darkVars = require('./scripts/dark-vars'); -const compactVars = require('./scripts/compact-vars'); -function generateThemeFileContent(theme) { - return `const { ${theme}ThemeSingle } = require('./theme');\nconst defaultTheme = require('./default-theme');\n -module.exports = { - ...defaultTheme, - ...${theme}ThemeSingle -}`; -} +const restCssPath = path.join(process.cwd(), 'components', 'style', 'reset.css'); +const tokenStatisticPath = path.join(process.cwd(), 'components', 'version', 'token.json'); +const tokenMetaPath = path.join(process.cwd(), 'components', 'version', 'token-meta.json'); -// We need compile additional content for antd user function finalizeCompile() { - if (fs.existsSync(path.join(__dirname, './lib'))) { - // Build a entry less file to dist/antd.less - const componentsPath = path.join(process.cwd(), 'components'); - let componentsLessContent = ''; - // Build components in one file: lib/style/components.less - fs.readdir(componentsPath, (err, files) => { - files.forEach(file => { - if (fs.existsSync(path.join(componentsPath, file, 'style', 'index.less'))) { - componentsLessContent += `@import "../${path.posix.join( - file, - 'style', - 'index-pure.less', - )}";\n`; - } - }); - fs.writeFileSync( - path.join(process.cwd(), 'lib', 'style', 'components.less'), - componentsLessContent, - ); - }); + if (fs.existsSync(path.join(__dirname, './es'))) { + fs.copyFileSync(restCssPath, path.join(process.cwd(), 'es', 'style', 'reset.css')); + fs.copyFileSync(tokenStatisticPath, path.join(process.cwd(), 'es', 'version', 'token.json')); + fs.copyFileSync(tokenMetaPath, path.join(process.cwd(), 'es', 'version', 'token-meta.json')); } -} -function buildThemeFile(theme, vars) { - // Build less entry file: dist/antd.${theme}.less - if (theme !== 'default') { - fs.writeFileSync( - path.join(process.cwd(), 'dist', `antd.${theme}.less`), - `@import "../lib/style/${theme}.less";\n@import "../lib/style/components.less";`, - ); - // eslint-disable-next-line no-console - console.log(`Built a entry less file to dist/antd.${theme}.less`); - } else { - fs.writeFileSync( - path.join(process.cwd(), 'dist', `default-theme.js`), - `module.exports = ${JSON.stringify(vars, null, 2)};\n`, - ); - return; + if (fs.existsSync(path.join(__dirname, './lib'))) { + fs.copyFileSync(restCssPath, path.join(process.cwd(), 'lib', 'style', 'reset.css')); + fs.copyFileSync(tokenStatisticPath, path.join(process.cwd(), 'lib', 'version', 'token.json')); + fs.copyFileSync(tokenMetaPath, path.join(process.cwd(), 'lib', 'version', 'token-meta.json')); } - - // Build ${theme}.js: dist/${theme}-theme.js, for less-loader - - fs.writeFileSync( - path.join(process.cwd(), 'dist', `theme.js`), - `const ${theme}ThemeSingle = ${JSON.stringify(vars, null, 2)};\n`, - { - flag: 'a', - }, - ); - - fs.writeFileSync( - path.join(process.cwd(), 'dist', `${theme}-theme.js`), - generateThemeFileContent(theme), - ); - - // eslint-disable-next-line no-console - console.log(`Built a ${theme} theme js file to dist/${theme}-theme.js`); } function finalizeDist() { if (fs.existsSync(path.join(__dirname, './dist'))) { - // Build less entry file: dist/antd.less - fs.writeFileSync( - path.join(process.cwd(), 'dist', 'antd.less'), - '@import "../lib/style/default.less";\n@import "../lib/style/components.less";', - ); - // eslint-disable-next-line no-console - fs.writeFileSync( - path.join(process.cwd(), 'dist', 'theme.js'), - `const defaultTheme = require('./default-theme.js');\n`, - ); - // eslint-disable-next-line no-console - console.log('Built a entry less file to dist/antd.less'); - buildThemeFile('default', defaultVars); - buildThemeFile('dark', darkVars); - buildThemeFile('compact', compactVars); - buildThemeFile('variable', {}); - fs.writeFileSync( - path.join(process.cwd(), 'dist', `theme.js`), - ` -function getThemeVariables(options = {}) { - let themeVar = { - 'hack': \`true;@import "\${require.resolve('ant-design-vue/lib/style/color/colorPalette.less')}";\`, - ...defaultTheme - }; - if(options.dark) { - themeVar = { - ...themeVar, - ...darkThemeSingle - } - } - if(options.compact){ - themeVar = { - ...themeVar, - ...compactThemeSingle - } + fs.copyFileSync(restCssPath, path.join(process.cwd(), 'dist', 'reset.css')); } - return themeVar; -} - -module.exports = { - darkThemeSingle, - compactThemeSingle, - getThemeVariables -}`, - { - flag: 'a', - }, - ); - } -} - -function isComponentStyleEntry(file) { - return file.path.match(/style(\/|\\)index\.tsx/); -} - -function needTransformStyle(content) { - return content.includes('../../style/index.less') || content.includes('./index.less'); } module.exports = { compile: { - includeLessFile: [/(\/|\\)components(\/|\\)style(\/|\\)default.less$/], - transformTSFile(file) { - if (isComponentStyleEntry(file)) { - let content = file.contents.toString(); - - if (needTransformStyle(content)) { - const cloneFile = file.clone(); - - // Origin - content = content.replace('../../style/index.less', '../../style/default.less'); - cloneFile.contents = Buffer.from(content); - - return cloneFile; - } - } - }, - transformFile(file) { - if (isComponentStyleEntry(file)) { - const indexLessFilePath = file.path.replace('index.tsx', 'index.less'); - - if (fs.existsSync(indexLessFilePath)) { - // We put origin `index.less` file to `index-pure.less` - const pureFile = file.clone(); - pureFile.contents = Buffer.from(fs.readFileSync(indexLessFilePath, 'utf8')); - pureFile.path = pureFile.path.replace('index.tsx', 'index-pure.less'); - - // Rewrite `index.less` file with `root-entry-name` - const indexLessFile = file.clone(); - indexLessFile.contents = Buffer.from( - [ - // Inject variable - '@root-entry-name: default;', - // Point to origin file - "@import './index-pure.less';", - ].join('\n\n'), - ); - indexLessFile.path = indexLessFile.path.replace('index.tsx', 'index.less'); - - return [indexLessFile, pureFile]; - } - } - - return []; - }, - lessConfig: { - modifyVars: { - 'root-entry-name': 'default', - }, - }, finalize: finalizeCompile, }, dist: { finalize: finalizeDist, }, - generateThemeFileContent, bail: true, }; diff --git a/.gitignore b/.gitignore index e68dd6e1d8..b45e673a2c 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ jspm_packages/ dist lib es +/locale _site yarn.lock package-lock.json @@ -78,5 +79,8 @@ report.html site/src/router/demoRoutes.js +components/version/version.ts components/version/version.tsx +components/version/token.json +components/version/token-meta.json ~component-api.json diff --git a/.jest.js b/.jest.js index 3086ad2d28..f75b9b1a5a 100644 --- a/.jest.js +++ b/.jest.js @@ -18,6 +18,7 @@ function getTestRegex(libDir) { module.exports = { verbose: true, setupFiles: ['./tests/setup.js'], + setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'], moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'vue', 'md', 'jpg'], modulePathIgnorePatterns: ['/_site/'], testPathIgnorePatterns: testPathIgnorePatterns, @@ -30,23 +31,19 @@ module.exports = { testRegex: getTestRegex(libDir), moduleNameMapper: { '^@/(.*)$/': '/$1', - 'ant-design-vue$/': '/components/index.ts', - 'ant-design-vue/es/': '/components', + '^ant-design-vue$': '/components/index', + '^ant-design-vue/es/(.*)$': '/components/$1', }, snapshotSerializers: ['/node_modules/jest-serializer-vue'], collectCoverage: process.env.COVERAGE === 'true', collectCoverageFrom: [ 'components/**/*.{js,jsx,vue}', - '!components/*/style/index.{js,jsx}', - '!components/style/*.{js,jsx}', - '!components/*/locale/*.{js,jsx}', '!components/*/__tests__/**/type.{js,jsx}', '!components/vc-*/**/*', '!components/*/demo/**/*', '!components/_util/**/*', '!components/align/**/*', '!components/trigger/**/*', - '!components/style.js', '!**/node_modules/**', ], testEnvironment: 'jsdom', diff --git a/.prettierignore b/.prettierignore index fc219ac377..b3b276aff4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,7 +18,6 @@ yarn-error.log .editorconfig .eslintignore **/*.yml -components/style/color/*.less **/assets .gitattributes .stylelintrc diff --git a/.stylelintrc.json b/.stylelintrc.json index 40ce56859f..4d48dbf970 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -4,13 +4,40 @@ "stylelint-config-rational-order", "stylelint-config-prettier" ], - "plugins": ["stylelint-order", "stylelint-declaration-block-no-ignored-properties"], + "customSyntax": "postcss-less", + "plugins": ["stylelint-declaration-block-no-ignored-properties"], "rules": { - "comment-empty-line-before": null, - "function-name-case": ["lower", { "ignoreFunctions": ["/colorPalette/"] }], - "no-invalid-double-slash-comments": null, + "function-name-case": ["lower"], + "function-no-unknown": [ + true, + { + "ignoreFunctions": [ + "fade", + "fadeout", + "tint", + "darken", + "ceil", + "fadein", + "floor", + "unit", + "shade", + "lighten", + "percentage", + "-" + ] + } + ], + "import-notation": null, "no-descending-specificity": null, - "declaration-empty-line-before": null - }, - "ignoreFiles": ["components/style/color/{bezierEasing,colorPalette,tinyColor}.less"] + "no-invalid-position-at-import-rule": null, + "declaration-empty-line-before": null, + "keyframes-name-pattern": null, + "custom-property-pattern": null, + "number-max-precision": 8, + "alpha-value-notation": "number", + "color-function-notation": "legacy", + "selector-class-pattern": null, + "selector-id-pattern": null, + "selector-not-notation": null + } } diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 7574c43e69..b69d44b335 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -10,6 +10,53 @@ --- +## 3.3.0-beta.3 + +`2022-08-11` + +- 🌟 Merged 3.2.10 and 3.2.11 features +- 🌟 Rate character Added slot parameters for customizing different characters + +## 3.3.0-beta.2 + +`2022-07-02` + +- 🐞 Fix dist/antd.css file missing issue caused by 3.3.0-beta.1 + +## 3.3.0-beta.1 + +`2022-07-02` + +- 💄 优化部分组件箭头样式。 + + + +- Input + - 🆕 新增 `clearIcon` 属性,支持自定义清除按钮。 +- Table + - 🆕 `column.filterSearch` 属性现在支持返回一个函数用于自定义搜索条件。 + - ⌨️ 增加 `aria-sort` 属性以优化屏幕阅读器的使用体验。 + - 🆕 列筛选条件重置时,支持重置为默认值而非空值。 +- 🆕 表单组件新增 `status` 属性以支持自定义状态。 + + 包含:Transfer、AutoComplete、TreeSelect、Cascader、Select、DatePicker、Mentions、InputNumber、Input + + + +- 🆕 InputNumber 组件支持 upIcon、downIcon 插槽用于自定义上下图标。 +- 🆕 Notification 组件弹窗位置新增支持 `top` / `bottom`。 +- 🆕 Select、Cascader、DatePicker、TimePicker 等组件新增 `placement` 用于自定义弹层方向。 +- 🆕 Skeleton.Input 添加 `block` 属性。 +- 🆕 合并 TimePicker `disabledHours`、`disabledMinutes`、`disabledSeconds` 至 `disabledTime` 以保持与 DatePicker 接口一致性。 +- 🆕 Grid 支持 `justify="space-evenly"`。 +- 💄 修改部分边框颜色和进度条的背景色为透明色以适应有色背景。 +- 🐞 修复 Typography.Title 进入编辑模式时大小不一致的问题。 +- Upload + - 🆕 Upload `picture-card` 模式支持配置图片的 `crossorigin` 属性。 + - 🐞 修复 Upload `prefixCls` 对列表不生效的问题。 + - 💄 优化 Upload 操作按钮的样式细节。 +- 🐞 修复 Switch 在暗黑主题下关闭时的颜色问题。 + ## 3.2.20 `2023-04-27` diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 02be1e765a..d6486a0d6d 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -10,6 +10,53 @@ --- +## 3.3.0-beta.3 + +`2022-08-11` + +- 🌟 合并 3.2.10 3.2.11 版本特性 +- 🌟 Rate character 新增插槽参数,用于自定义不同 character + +## 3.3.0-beta.2 + +`2022-07-02` + +- 🐞 修复 3.3.0-beta.1 引起的 dist/antd.css 文件丢失问题 + +## 3.3.0-beta.1 + +`2022-07-02` + +- 💄 优化部分组件箭头样式。 + + + +- Input + - 🆕 新增 `clearIcon` 属性,支持自定义清除按钮。 +- Table + - 🆕 `column.filterSearch` 属性现在支持返回一个函数用于自定义搜索条件。 + - ⌨️ 增加 `aria-sort` 属性以优化屏幕阅读器的使用体验。 + - 🆕 列筛选条件重置时,支持重置为默认值而非空值。 +- 🆕 表单组件新增 `status` 属性以支持自定义状态。 + + 包含:Transfer、AutoComplete、TreeSelect、Cascader、Select、DatePicker、Mentions、InputNumber、Input + + + +- 🆕 InputNumber 组件支持 upIcon、downIcon 插槽用于自定义上下图标。 +- 🆕 Notification 组件弹窗位置新增支持 `top` / `bottom`。 +- 🆕 Select、Cascader、DatePicker、TimePicker 等组件新增 `placement` 用于自定义弹层方向。 +- 🆕 Skeleton.Input 添加 `block` 属性。 +- 🆕 合并 TimePicker `disabledHours`、`disabledMinutes`、`disabledSeconds` 至 `disabledTime` 以保持与 DatePicker 接口一致性。 +- 🆕 Grid 支持 `justify="space-evenly"`。 +- 💄 修改部分边框颜色和进度条的背景色为透明色以适应有色背景。 +- 🐞 修复 Typography.Title 进入编辑模式时大小不一致的问题。 +- Upload + - 🆕 Upload `picture-card` 模式支持配置图片的 `crossorigin` 属性。 + - 🐞 修复 Upload `prefixCls` 对列表不生效的问题。 + - 💄 优化 Upload 操作按钮的样式细节。 +- 🐞 修复 Switch 在暗黑主题下关闭时的颜色问题。 + ## 3.2.20 `2023-04-27` diff --git a/README-zh_CN.md b/README-zh_CN.md index 7e209e3249..b13bfeb79b 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -66,6 +66,7 @@ $ yarn add ant-design-vue | [vue-cli-plugin-ant-design](https://github.com/vueComponent/vue-cli-plugin-ant-design) | 使用 vue-cli3 快速使用 ant-design-vue 组件库 | | [vue-dash-event](https://github.com/vueComponent/vue-dash-event) | 在 DOM 模板中,您可以使用 ant-design-vue 组件的自定义事件(camelCase) | | [@formily/antdv](https://github.com/formilyjs/antdv) | 这是一个结合了 Formily 和 ant-design-vue 的组件库 | +| [@ant-design-vue/nuxt](https://github.com/vueComponent/ant-design-vue-nuxt) | ant-design-vue 的 nuxt 模块扩展 | ## 问答 diff --git a/README.md b/README.md index 6ddc5b4f26..42c1cd8767 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ English | [简体中文](./README-zh_CN.md) ## Environment Support -- Modern browsers. v1.x support Internet Explorer 9+ (with [polyfills](https://www.antdv.com/docs/vue/getting-started/#Compatibility)) +- Modern browsers. v1.x support Internet Explorer 9+ (with [polyfills](https://www.antdv.com/docs/vue/getting-started/#compatibility)) - Server-side Rendering - Support Vue 2 & Vue 3 - [Electron](https://electronjs.org/) @@ -66,6 +66,7 @@ If you are in a bad network environment,you can try other registries and tools | [vue-cli-plugin-ant-design](https://github.com/vueComponent/vue-cli-plugin-ant-design) | Vue-cli 3 plugin to add ant-design-vue | | [vue-dash-event](https://github.com/vueComponent/vue-dash-event) | The library function, implemented in the DOM template, can use the custom event of the ant-design-vue component (camelCase) | | [@formily/antdv](https://github.com/formilyjs/antdv) | The Library with Formily and ant-design-vue | +| [@ant-design-vue/nuxt](https://github.com/vueComponent/ant-design-vue-nuxt) | A nuxt module for ant-design-vue | ## Donation diff --git a/antd-tools/generator-types/src/formatter.ts b/antd-tools/generator-types/src/formatter.ts index 722077ecf8..581ac46bef 100644 --- a/antd-tools/generator-types/src/formatter.ts +++ b/antd-tools/generator-types/src/formatter.ts @@ -91,7 +91,7 @@ export function formatter( !tableTitle.includes('()') ) { const childTag: VueTag = { - name: getComponentName(tableTitle.replaceAll('.', '').replaceAll('/', ''), tagPrefix), + name: getComponentName(tableTitle.replace(/\.|\//g, ''), tagPrefix), slots: [], events: [], attributes: [], diff --git a/antd-tools/generator-types/src/index.ts b/antd-tools/generator-types/src/index.ts index c500b38e10..4b1e3d43ff 100644 --- a/antd-tools/generator-types/src/index.ts +++ b/antd-tools/generator-types/src/index.ts @@ -7,6 +7,7 @@ import { outputFileSync, readFileSync } from 'fs-extra'; import type { Options, VueTag } from './type'; import { getComponentName, normalizePath, toKebabCase } from './utils'; import { genVeturAttributes, genVeturTags } from './vetur'; +import { flatMap } from 'lodash'; async function readMarkdown(options: Options): Promise> { const mdPaths = await glob(normalizePath(`${options.path}/**/*.md`)); @@ -22,7 +23,7 @@ async function readMarkdown(options: Options): Promise> { }) .filter(item => item) as VueTag[][]; const tags: Map = new Map(); - data.flatMap(item => item).forEach(mergedTag => mergeTag(tags, mergedTag)); + flatMap(data, item => item).forEach(mergedTag => mergeTag(tags, mergedTag)); return tags; } diff --git a/antd-tools/generator-types/src/parser.ts b/antd-tools/generator-types/src/parser.ts index 5a20559e5d..59f7d2483f 100644 --- a/antd-tools/generator-types/src/parser.ts +++ b/antd-tools/generator-types/src/parser.ts @@ -27,7 +27,7 @@ function readLine(input: string) { function splitTableLine(line: string) { line = line.replace(/\\\|/g, 'JOIN'); - const items = line.split('|').map(item => item.trim().replaceAll('JOIN', '|')); + const items = line.split('|').map(item => item.trim().replace(/JOIN/g, '|')); // remove pipe character on both sides items.pop(); diff --git a/antd-tools/getBabelCommonConfig.js b/antd-tools/getBabelCommonConfig.js index 53809d2875..3232837b9d 100644 --- a/antd-tools/getBabelCommonConfig.js +++ b/antd-tools/getBabelCommonConfig.js @@ -20,7 +20,8 @@ module.exports = function (modules) { resolve('@babel/plugin-transform-runtime'), { useESModules: modules === false, - version: '^7.10.4', + version: + require(`${process.cwd()}/package.json`).dependencies['@babel/runtime'] || '^7.10.4', }, ], // resolve('babel-plugin-inline-import-data-uri'), diff --git a/antd-tools/getWebpackConfig.js b/antd-tools/getWebpackConfig.js index 45ec355665..59d7bdba07 100644 --- a/antd-tools/getWebpackConfig.js +++ b/antd-tools/getWebpackConfig.js @@ -150,36 +150,6 @@ function getWebpackConfig(modules) { }, ], }, - { - test: /\.less$/, - use: [ - MiniCssExtractPlugin.loader, - { - loader: 'css-loader', - options: { - sourceMap: true, - }, - }, - { - loader: 'postcss-loader', - options: { - postcssOptions: { - plugins: ['autoprefixer'], - }, - sourceMap: true, - }, - }, - { - loader: 'less-loader', - options: { - lessOptions: { - javascriptEnabled: true, - }, - sourceMap: true, - }, - }, - ], - }, // Images { test: svgRegex, @@ -200,7 +170,7 @@ function getWebpackConfig(modules) { new webpack.BannerPlugin(` ${pkg.name} v${pkg.version} -Copyright 2017-present, ant-design-vue. +Copyright 2017-present, Ant Design Vue. All rights reserved. `), new WebpackBar({ @@ -215,7 +185,7 @@ All rights reserved. }; if (process.env.RUN_ENV === 'PRODUCTION') { - const entry = ['./index']; + let entry = ['./index']; config.externals = [ { vue: { @@ -223,11 +193,13 @@ All rights reserved. commonjs2: 'vue', commonjs: 'vue', amd: 'vue', + module: 'vue', }, }, ]; config.output.library = distFileBaseName; config.output.libraryTarget = 'umd'; + config.output.globalObject = 'this'; config.optimization = { minimizer: [ new TerserPlugin({ @@ -238,7 +210,6 @@ All rights reserved. }), ], }; - // Development const uncompressedConfig = merge({}, config, { entry: { diff --git a/antd-tools/gulpfile.js b/antd-tools/gulpfile.js index bb8c8b6dd8..b7c7c20cdb 100644 --- a/antd-tools/gulpfile.js +++ b/antd-tools/gulpfile.js @@ -5,7 +5,6 @@ const getBabelCommonConfig = require('./getBabelCommonConfig'); const merge2 = require('merge2'); const { execSync } = require('child_process'); const through2 = require('through2'); -const transformLess = require('./transformLess'); const webpack = require('webpack'); const babel = require('gulp-babel'); const argv = require('minimist')(process.argv.slice(2)); @@ -27,15 +26,22 @@ const compareVersions = require('compare-versions'); const getTSCommonConfig = require('./getTSCommonConfig'); const replaceLib = require('./replaceLib'); const sortApiTable = require('./sortApiTable'); +const { glob } = require('glob'); const packageJson = require(getProjectPath('package.json')); const tsDefaultReporter = ts.reporter.defaultReporter(); const cwd = process.cwd(); const libDir = getProjectPath('lib'); const esDir = getProjectPath('es'); +const localeDir = getProjectPath('locale'); const tsConfig = getTSCommonConfig(); +// FIXME: hard code, not find typescript can modify the path resolution +const localeDts = `import type { Locale } from '../lib/locale-provider'; +declare const localeValues: Locale; +export default localeValues;`; + function dist(done) { rimraf.sync(path.join(cwd, 'dist')); process.env.RUN_ENV = 'PRODUCTION'; @@ -108,6 +114,11 @@ gulp.task('tsc', () => ), ); +gulp.task('clean', () => { + rimraf.sync(getProjectPath('_site')); + rimraf.sync(getProjectPath('_data')); +}); + function babelify(js, modules) { const babelConfig = getBabelCommonConfig(modules); babelConfig.babelrc = false; @@ -118,17 +129,7 @@ function babelify(js, modules) { const stream = js.pipe(babel(babelConfig)).pipe( through2.obj(function z(file, encoding, next) { this.push(file.clone()); - if (file.path.match(/\/style\/index\.(js|jsx|ts|tsx)$/)) { - const content = file.contents.toString(encoding); - file.contents = Buffer.from( - content - .replace(/\/style\/?'/g, "/style/css'") - .replace(/\/style\/?"/g, '/style/css"') - .replace(/\.less/g, '.css'), - ); - file.path = file.path.replace(/index\.(js|jsx|ts|tsx)$/, 'css.js'); - this.push(file); - } else if (modules !== false) { + if (modules !== false) { const content = file.contents.toString(encoding); file.contents = Buffer.from( content @@ -144,47 +145,9 @@ function babelify(js, modules) { } function compile(modules) { - const { compile: { transformTSFile, transformFile, includeLessFile = [] } = {} } = getConfig(); + const { compile: { transformTSFile, transformFile } = {} } = getConfig(); rimraf.sync(modules !== false ? libDir : esDir); - // =============================== LESS =============================== - const less = gulp - .src(['components/**/*.less']) - .pipe( - through2.obj(function (file, encoding, next) { - // Replace content - const cloneFile = file.clone(); - const content = file.contents.toString().replace(/^\uFEFF/, ''); - - cloneFile.contents = Buffer.from(content); - - // Clone for css here since `this.push` will modify file.path - const cloneCssFile = cloneFile.clone(); - - this.push(cloneFile); - - // Transform less file - if ( - file.path.match(/(\/|\\)style(\/|\\)index\.less$/) || - file.path.match(/(\/|\\)style(\/|\\)v2-compatible-reset\.less$/) || - includeLessFile.some(regex => file.path.match(regex)) - ) { - transformLess(cloneCssFile.contents.toString(), cloneCssFile.path) - .then(css => { - cloneCssFile.contents = Buffer.from(css); - cloneCssFile.path = cloneCssFile.path.replace(/\.less$/, '.css'); - this.push(cloneCssFile); - next(); - }) - .catch(e => { - console.error(e); - }); - } else { - next(); - } - }), - ) - .pipe(gulp.dest(modules === false ? esDir : libDir)); const assets = gulp .src(['components/**/*.@(png|svg)']) .pipe(gulp.dest(modules === false ? esDir : libDir)); @@ -259,7 +222,26 @@ function compile(modules) { tsResult.on('end', check); const tsFilesStream = babelify(tsResult.js, modules); const tsd = tsResult.dts.pipe(gulp.dest(modules === false ? esDir : libDir)); - return merge2([less, tsFilesStream, tsd, assets, transformFileStream].filter(s => s)); + return merge2([tsFilesStream, tsd, assets, transformFileStream].filter(s => s)); +} + +function generateLocale() { + if (!fs.existsSync(localeDir)) { + fs.mkdirSync(localeDir); + } + + const localeFiles = glob.sync('components/locale/*.ts?(x)'); + localeFiles.forEach(item => { + const match = item.match(/components\/locale\/(.*)\.tsx?/); + if (match) { + const locale = match[1]; + fs.writeFileSync( + path.join(localeDir, `${locale}.js`), + `module.exports = require('../lib/locale/${locale}');`, + ); + fs.writeFileSync(path.join(localeDir, `${locale}.d.ts`), localeDts); + } + }); } function tag() { @@ -395,7 +377,10 @@ gulp.task('compile-with-es', done => { gulp.task('compile-with-lib', done => { console.log('[Parallel] Compile to js...'); - compile().on('finish', done); + compile().on('finish', () => { + generateLocale(); + done(); + }); }); gulp.task('compile-finalize', done => { diff --git a/antd-tools/transformLess.js b/antd-tools/transformLess.js deleted file mode 100644 index 0e949b2430..0000000000 --- a/antd-tools/transformLess.js +++ /dev/null @@ -1,27 +0,0 @@ -const less = require('less'); -const path = require('path'); -const postcss = require('postcss'); -const autoprefixer = require('autoprefixer'); -const NpmImportPlugin = require('less-plugin-npm-import'); -const { getConfig } = require('./utils/projectHelper'); - -function transformLess(lessContent, lessFilePath, config = {}) { - const { cwd = process.cwd() } = config; - const { compile: { lessConfig } = {} } = getConfig(); - const resolvedLessFile = path.resolve(cwd, lessFilePath); - - // Do less compile - const lessOpts = { - paths: [path.dirname(resolvedLessFile)], - filename: resolvedLessFile, - plugins: [new NpmImportPlugin({ prefix: '~' })], - javascriptEnabled: true, - ...lessConfig, - }; - return less - .render(lessContent, lessOpts) - .then(result => postcss([autoprefixer]).process(result.css, { from: undefined })) - .then(r => r.css); -} - -module.exports = transformLess; diff --git a/antd-tools/utils/styleUtil.js b/antd-tools/utils/styleUtil.js deleted file mode 100644 index 7b05ee3152..0000000000 --- a/antd-tools/utils/styleUtil.js +++ /dev/null @@ -1,11 +0,0 @@ -// We convert less import in es/lib to css file path -function cssInjection(content) { - return content - .replace(/\/style\/?'/g, "/style/css'") - .replace(/\/style\/?"/g, '/style/css"') - .replace(/\.less/g, '.css'); -} - -module.exports = { - cssInjection, -}; diff --git a/components/_util/ActionButton.tsx b/components/_util/ActionButton.tsx index 19b1365aed..0958d24047 100644 --- a/components/_util/ActionButton.tsx +++ b/components/_util/ActionButton.tsx @@ -1,10 +1,12 @@ import type { ExtractPropTypes, PropType } from 'vue'; -import { onMounted, ref, defineComponent, onBeforeUnmount } from 'vue'; +import { shallowRef, onMounted, defineComponent, onBeforeUnmount } from 'vue'; import Button from '../button'; import type { ButtonProps } from '../button'; import type { LegacyButtonType } from '../button/buttonTypes'; import { convertLegacyProps } from '../button/buttonTypes'; import useDestroyed from './hooks/useDestroyed'; +import { objectType } from './type'; +import { findDOMNode } from './props-util'; const actionButtonProps = { type: { @@ -14,15 +16,15 @@ const actionButtonProps = { close: Function, autofocus: Boolean, prefixCls: String, - buttonProps: Object as PropType, + buttonProps: objectType(), emitEvent: Boolean, quitOnNullishReturnValue: Boolean, }; export type ActionButtonProps = ExtractPropTypes; -function isThenable(thing?: PromiseLike): boolean { - return !!(thing && !!thing.then); +function isThenable(thing?: PromiseLike): boolean { + return !!(thing && thing.then); } export default defineComponent({ @@ -30,22 +32,25 @@ export default defineComponent({ name: 'ActionButton', props: actionButtonProps, setup(props, { slots }) { - const clickedRef = ref(false); - const buttonRef = ref(); - const loading = ref(false); + const clickedRef = shallowRef(false); + const buttonRef = shallowRef(); + const loading = shallowRef(false); let timeoutId: any; const isDestroyed = useDestroyed(); onMounted(() => { if (props.autofocus) { - timeoutId = setTimeout(() => buttonRef.value.$el?.focus()); + timeoutId = setTimeout(() => findDOMNode(buttonRef.value)?.focus?.()); } }); onBeforeUnmount(() => { clearTimeout(timeoutId); }); + const onInternalClose = (...args: any[]) => { + props.close?.(...args); + }; + const handlePromiseOnOk = (returnValueOfOnOk?: PromiseLike) => { - const { close } = props; if (!isThenable(returnValueOfOnOk)) { return; } @@ -55,48 +60,46 @@ export default defineComponent({ if (!isDestroyed.value) { loading.value = false; } - close(...args); + onInternalClose(...args); clickedRef.value = false; }, (e: Error) => { - // Emit error when catch promise reject - // eslint-disable-next-line no-console - console.error(e); // See: https://github.com/ant-design/ant-design/issues/6183 if (!isDestroyed.value) { loading.value = false; } clickedRef.value = false; + return Promise.reject(e); }, ); }; const onClick = (e: MouseEvent) => { - const { actionFn, close = () => {} } = props; + const { actionFn } = props; if (clickedRef.value) { return; } clickedRef.value = true; if (!actionFn) { - close(); + onInternalClose(); return; } - let returnValueOfOnOk; + let returnValueOfOnOk: PromiseLike; if (props.emitEvent) { returnValueOfOnOk = actionFn(e); if (props.quitOnNullishReturnValue && !isThenable(returnValueOfOnOk)) { clickedRef.value = false; - close(e); + onInternalClose(e); return; } } else if (actionFn.length) { - returnValueOfOnOk = actionFn(close); + returnValueOfOnOk = actionFn(props.close); // https://github.com/ant-design/ant-design/issues/23358 clickedRef.value = false; } else { returnValueOfOnOk = actionFn(); if (!returnValueOfOnOk) { - close(); + onInternalClose(); return; } } diff --git a/components/_util/BaseInput.tsx b/components/_util/BaseInput.tsx index 009613a742..db85e3a4f2 100644 --- a/components/_util/BaseInput.tsx +++ b/components/_util/BaseInput.tsx @@ -1,4 +1,4 @@ -import { defineComponent, ref, withDirectives } from 'vue'; +import { defineComponent, shallowRef, withDirectives } from 'vue'; import antInput from './antInputDirective'; import PropTypes from './vue-types'; const BaseInput = defineComponent({ @@ -8,7 +8,7 @@ const BaseInput = defineComponent({ }, emits: ['change', 'input'], setup(_p, { emit }) { - const inputRef = ref(null); + const inputRef = shallowRef(null); const handleChange = (e: Event) => { const { composing } = e.target as any; if ((e as any).isComposing || composing) { diff --git a/components/_util/PortalWrapper.tsx b/components/_util/PortalWrapper.tsx index 5c02f3ba38..65d15c7806 100644 --- a/components/_util/PortalWrapper.tsx +++ b/components/_util/PortalWrapper.tsx @@ -1,20 +1,20 @@ import PropTypes from './vue-types'; -import switchScrollingEffect from './switchScrollingEffect'; -import setStyle from './setStyle'; import Portal from './Portal'; import { defineComponent, - ref, + shallowRef, watch, onMounted, onBeforeUnmount, onUpdated, getCurrentInstance, nextTick, + computed, } from 'vue'; import canUseDom from './canUseDom'; -import ScrollLocker from '../vc-util/Dom/scrollLocker'; import raf from './raf'; +import { booleanType } from './type'; +import useScrollLocker from './hooks/useScrollLocker'; let openCount = 0; const supportDom = canUseDom(); @@ -24,17 +24,13 @@ export function getOpenCount() { return process.env.NODE_ENV === 'test' ? openCount : 0; } -// https://github.com/ant-design/ant-design/issues/19340 -// https://github.com/ant-design/ant-design/issues/19332 -let cacheOverflow = {}; - const getParent = (getContainer: GetContainer) => { if (!supportDom) { return null; } if (getContainer) { if (typeof getContainer === 'string') { - return document.querySelectorAll(getContainer)[0]; + return document.querySelectorAll(getContainer)[0] as HTMLElement; } if (typeof getContainer === 'function') { return getContainer(); @@ -57,24 +53,25 @@ export default defineComponent({ forceRender: { type: Boolean, default: undefined }, getContainer: PropTypes.any, visible: { type: Boolean, default: undefined }, + autoLock: booleanType(), + didUpdate: Function, }, setup(props, { slots }) { - const container = ref(); - const componentRef = ref(); - const rafId = ref(); - const scrollLocker = new ScrollLocker({ - container: getParent(props.getContainer) as HTMLElement, - }); + const container = shallowRef(); + const componentRef = shallowRef(); + const rafId = shallowRef(); const removeCurrentContainer = () => { // Portal will remove from `parentNode`. // Let's handle this again to avoid refactor issue. container.value?.parentNode?.removeChild(container.value); + container.value = null; }; + let parent: HTMLElement = null; const attachToParent = (force = false) => { if (force || (container.value && !container.value.parentNode)) { - const parent = getParent(props.getContainer); + parent = getParent(props.getContainer); if (parent) { parent.appendChild(container.value); return true; @@ -86,13 +83,13 @@ export default defineComponent({ return true; }; // attachToParent(); - + const defaultContainer = document.createElement('div'); const getContainer = () => { if (!supportDom) { return null; } if (!container.value) { - container.value = document.createElement('div'); + container.value = defaultContainer; attachToParent(true); } setWrapperClassName(); @@ -108,41 +105,33 @@ export default defineComponent({ setWrapperClassName(); attachToParent(); }); - /** - * Enhance ./switchScrollingEffect - * 1. Simulate document body scroll bar with - * 2. Record body has overflow style and recover when all of PortalWrapper invisible - * 3. Disable body scroll when PortalWrapper has open - * - * @memberof PortalWrapper - */ - const switchScrolling = () => { - if (openCount === 1 && !Object.keys(cacheOverflow).length) { - switchScrollingEffect(); - // Must be set after switchScrollingEffect - cacheOverflow = setStyle({ - overflow: 'hidden', - overflowX: 'hidden', - overflowY: 'hidden', - }); - } else if (!openCount) { - setStyle(cacheOverflow); - cacheOverflow = {}; - switchScrollingEffect(true); - } - }; + const instance = getCurrentInstance(); + + useScrollLocker( + computed(() => { + return ( + props.autoLock && + props.visible && + canUseDom() && + (container.value === document.body || container.value === defaultContainer) + ); + }), + ); onMounted(() => { let init = false; watch( [() => props.visible, () => props.getContainer], ([visible, getContainer], [prevVisible, prevGetContainer]) => { // Update count - if (supportDom && getParent(props.getContainer) === document.body) { - if (visible && !prevVisible) { - openCount += 1; - } else if (init) { - openCount -= 1; + if (supportDom) { + parent = getParent(props.getContainer); + if (parent === document.body) { + if (visible && !prevVisible) { + openCount += 1; + } else if (init) { + openCount -= 1; + } } } @@ -157,17 +146,6 @@ export default defineComponent({ ) { removeCurrentContainer(); } - // updateScrollLocker - if ( - visible && - visible !== prevVisible && - supportDom && - getParent(getContainer) !== scrollLocker.getContainer() - ) { - scrollLocker.reLock({ - container: getParent(getContainer) as HTMLElement, - }); - } } init = true; }, @@ -184,30 +162,27 @@ export default defineComponent({ }); onBeforeUnmount(() => { - const { visible, getContainer } = props; - if (supportDom && getParent(getContainer) === document.body) { + const { visible } = props; + if (supportDom && parent === document.body) { // 离开时不会 render, 导到离开时数值不变,改用 func 。。 openCount = visible && openCount ? openCount - 1 : openCount; } removeCurrentContainer(); raf.cancel(rafId.value); }); - return () => { const { forceRender, visible } = props; let portal = null; const childProps = { getOpenCount: () => openCount, getContainer, - switchScrollingEffect: switchScrolling, - scrollLocker, }; - if (forceRender || visible || componentRef.value) { portal = ( slots.default?.(childProps) }} > ); diff --git a/components/_util/colors.ts b/components/_util/colors.ts index bb3c2ca2c4..8bb5a90ab1 100644 --- a/components/_util/colors.ts +++ b/components/_util/colors.ts @@ -1,23 +1,34 @@ -import type { ElementOf } from './type'; -import { tuple } from './type'; +import type { PresetColorKey } from '../theme/interface'; +import { PresetColors } from '../theme/interface'; -export const PresetStatusColorTypes = tuple('success', 'processing', 'error', 'default', 'warning'); +type InverseColor = `${PresetColorKey}-inverse`; +const inverseColors = PresetColors.map(color => `${color}-inverse`); -export const PresetColorTypes = tuple( - 'pink', - 'red', - 'yellow', - 'orange', - 'cyan', - 'green', - 'blue', - 'purple', - 'geekblue', - 'magenta', - 'volcano', - 'gold', - 'lime', -); +export const PresetStatusColorTypes = [ + 'success', + 'processing', + 'error', + 'default', + 'warning', +] as const; -export type PresetColorType = ElementOf; -export type PresetStatusColorType = ElementOf; +export type PresetColorType = PresetColorKey | InverseColor; + +export type PresetStatusColorType = (typeof PresetStatusColorTypes)[number]; + +/** + * determine if the color keyword belongs to the `Ant Design` {@link PresetColors}. + * @param color color to be judged + * @param includeInverse whether to include reversed colors + */ +export function isPresetColor(color?: any, includeInverse = true) { + if (includeInverse) { + return [...inverseColors, ...PresetColors].includes(color); + } + + return PresetColors.includes(color); +} + +export function isPresetStatusColor(color?: any): color is PresetStatusColorType { + return PresetStatusColorTypes.includes(color); +} diff --git a/components/_util/createContext.ts b/components/_util/createContext.ts new file mode 100644 index 0000000000..2a666d3f6d --- /dev/null +++ b/components/_util/createContext.ts @@ -0,0 +1,22 @@ +import { inject, provide, reactive, watchEffect } from 'vue'; + +function createContext>(defaultValue?: T) { + const contextKey = Symbol('contextKey'); + const useProvide = (props: T, newProps?: T) => { + const mergedProps = reactive({} as T); + provide(contextKey, mergedProps); + watchEffect(() => { + Object.assign(mergedProps, props, newProps || {}); + }); + return mergedProps; + }; + const useInject = () => { + return inject(contextKey, defaultValue as T) || ({} as T); + }; + return { + useProvide, + useInject, + }; +} + +export default createContext; diff --git a/components/_util/cssinjs/Cache.ts b/components/_util/cssinjs/Cache.ts new file mode 100644 index 0000000000..007e4a8672 --- /dev/null +++ b/components/_util/cssinjs/Cache.ts @@ -0,0 +1,25 @@ +export type KeyType = string | number; +type ValueType = [number, any]; // [times, realValue] + +class Entity { + /** @private Internal cache map. Do not access this directly */ + cache = new Map(); + + get(keys: KeyType[] | string): ValueType | null { + return this.cache.get(Array.isArray(keys) ? keys.join('%') : keys) || null; + } + + update(keys: KeyType[] | string, valueFn: (origin: ValueType | null) => ValueType | null) { + const path = Array.isArray(keys) ? keys.join('%') : keys; + const prevValue = this.cache.get(path)!; + const nextValue = valueFn(prevValue); + + if (nextValue === null) { + this.cache.delete(path); + } else { + this.cache.set(path, nextValue); + } + } +} + +export default Entity; diff --git a/components/_util/cssinjs/Keyframes.ts b/components/_util/cssinjs/Keyframes.ts new file mode 100644 index 0000000000..64b99e27c3 --- /dev/null +++ b/components/_util/cssinjs/Keyframes.ts @@ -0,0 +1,19 @@ +import type { CSSInterpolation } from './hooks/useStyleRegister'; + +class Keyframe { + private name: string; + style: CSSInterpolation; + + constructor(name: string, style: CSSInterpolation) { + this.name = name; + this.style = style; + } + + getName(hashId = ''): string { + return hashId ? `${hashId}-${this.name}` : this.name; + } + + _keyframe = true; +} + +export default Keyframe; diff --git a/components/_util/cssinjs/StyleContext.tsx b/components/_util/cssinjs/StyleContext.tsx new file mode 100644 index 0000000000..c1abe883e5 --- /dev/null +++ b/components/_util/cssinjs/StyleContext.tsx @@ -0,0 +1,157 @@ +import type { ShallowRef, ExtractPropTypes, InjectionKey, Ref } from 'vue'; +import { provide, defineComponent, unref, inject, watch, shallowRef } from 'vue'; +import CacheEntity from './Cache'; +import type { Linter } from './linters/interface'; +import type { Transformer } from './transformers/interface'; +import { arrayType, booleanType, objectType, someType, stringType, withInstall } from '../type'; +import initDefaultProps from '../props-util/initDefaultProps'; +export const ATTR_TOKEN = 'data-token-hash'; +export const ATTR_MARK = 'data-css-hash'; +export const ATTR_DEV_CACHE_PATH = 'data-dev-cache-path'; + +// Mark css-in-js instance in style element +export const CSS_IN_JS_INSTANCE = '__cssinjs_instance__'; +export const CSS_IN_JS_INSTANCE_ID = Math.random().toString(12).slice(2); + +export function createCache() { + if (typeof document !== 'undefined' && document.head && document.body) { + const styles = document.body.querySelectorAll(`style[${ATTR_MARK}]`) || []; + const { firstChild } = document.head; + + Array.from(styles).forEach(style => { + (style as any)[CSS_IN_JS_INSTANCE] = + (style as any)[CSS_IN_JS_INSTANCE] || CSS_IN_JS_INSTANCE_ID; + + // Not force move if no head + document.head.insertBefore(style, firstChild); + }); + + // Deduplicate of moved styles + const styleHash: Record = {}; + Array.from(document.querySelectorAll(`style[${ATTR_MARK}]`)).forEach(style => { + const hash = style.getAttribute(ATTR_MARK)!; + if (styleHash[hash]) { + if ((style as any)[CSS_IN_JS_INSTANCE] === CSS_IN_JS_INSTANCE_ID) { + style.parentNode?.removeChild(style); + } + } else { + styleHash[hash] = true; + } + }); + } + + return new CacheEntity(); +} + +export type HashPriority = 'low' | 'high'; + +export interface StyleContextProps { + autoClear?: boolean; + /** @private Test only. Not work in production. */ + mock?: 'server' | 'client'; + /** + * Only set when you need ssr to extract style on you own. + * If not provided, it will auto create `; + }); + + return styleText; +} diff --git a/components/_util/cssinjs/index.ts b/components/_util/cssinjs/index.ts new file mode 100644 index 0000000000..49d0443b53 --- /dev/null +++ b/components/_util/cssinjs/index.ts @@ -0,0 +1,67 @@ +import useCacheToken from './hooks/useCacheToken'; +import type { CSSInterpolation, CSSObject } from './hooks/useStyleRegister'; +import useStyleRegister, { extractStyle } from './hooks/useStyleRegister'; +import Keyframes from './Keyframes'; +import type { Linter } from './linters'; +import { legacyNotSelectorLinter, logicalPropertiesLinter } from './linters'; +import type { StyleContextProps, StyleProviderProps } from './StyleContext'; +import { createCache, useStyleInject, useStyleProvider, StyleProvider } from './StyleContext'; +import type { DerivativeFunc, TokenType } from './theme'; +import { createTheme, Theme } from './theme'; +import type { Transformer } from './transformers/interface'; +import legacyLogicalPropertiesTransformer from './transformers/legacyLogicalProperties'; + +const cssinjs = { + Theme, + createTheme, + useStyleRegister, + useCacheToken, + createCache, + useStyleInject, + useStyleProvider, + Keyframes, + extractStyle, + + // Transformer + legacyLogicalPropertiesTransformer, + + // Linters + logicalPropertiesLinter, + legacyNotSelectorLinter, + + // cssinjs + StyleProvider, +}; +export { + Theme, + createTheme, + useStyleRegister, + useCacheToken, + createCache, + useStyleInject, + useStyleProvider, + Keyframes, + extractStyle, + + // Transformer + legacyLogicalPropertiesTransformer, + + // Linters + logicalPropertiesLinter, + legacyNotSelectorLinter, + + // cssinjs + StyleProvider, +}; +export type { + TokenType, + CSSObject, + CSSInterpolation, + DerivativeFunc, + Transformer, + Linter, + StyleContextProps, + StyleProviderProps, +}; + +export default cssinjs; diff --git a/components/_util/cssinjs/linters/contentQuotesLinter.ts b/components/_util/cssinjs/linters/contentQuotesLinter.ts new file mode 100644 index 0000000000..b1e60f08ce --- /dev/null +++ b/components/_util/cssinjs/linters/contentQuotesLinter.ts @@ -0,0 +1,25 @@ +import type { Linter } from './interface'; +import { lintWarning } from './utils'; + +const linter: Linter = (key, value, info) => { + if (key === 'content') { + // From emotion: https://github.com/emotion-js/emotion/blob/main/packages/serialize/src/index.js#L63 + const contentValuePattern = + /(attr|counters?|url|(((repeating-)?(linear|radial))|conic)-gradient)\(|(no-)?(open|close)-quote/; + const contentValues = ['normal', 'none', 'initial', 'inherit', 'unset']; + if ( + typeof value !== 'string' || + (contentValues.indexOf(value) === -1 && + !contentValuePattern.test(value) && + (value.charAt(0) !== value.charAt(value.length - 1) || + (value.charAt(0) !== '"' && value.charAt(0) !== "'"))) + ) { + lintWarning( + `You seem to be using a value for 'content' without quotes, try replacing it with \`content: '"${value}"'\`.`, + info, + ); + } + } +}; + +export default linter; diff --git a/components/_util/cssinjs/linters/hashedAnimationLinter.ts b/components/_util/cssinjs/linters/hashedAnimationLinter.ts new file mode 100644 index 0000000000..4c6fc948b3 --- /dev/null +++ b/components/_util/cssinjs/linters/hashedAnimationLinter.ts @@ -0,0 +1,15 @@ +import type { Linter } from './interface'; +import { lintWarning } from './utils'; + +const linter: Linter = (key, value, info) => { + if (key === 'animation') { + if (info.hashId && value !== 'none') { + lintWarning( + `You seem to be using hashed animation '${value}', in which case 'animationName' with Keyframe as value is recommended.`, + info, + ); + } + } +}; + +export default linter; diff --git a/components/_util/cssinjs/linters/index.ts b/components/_util/cssinjs/linters/index.ts new file mode 100644 index 0000000000..a7a3ee1009 --- /dev/null +++ b/components/_util/cssinjs/linters/index.ts @@ -0,0 +1,5 @@ +export { default as contentQuotesLinter } from './contentQuotesLinter'; +export { default as hashedAnimationLinter } from './hashedAnimationLinter'; +export type { Linter } from './interface'; +export { default as legacyNotSelectorLinter } from './legacyNotSelectorLinter'; +export { default as logicalPropertiesLinter } from './logicalPropertiesLinter'; diff --git a/components/_util/cssinjs/linters/interface.ts b/components/_util/cssinjs/linters/interface.ts new file mode 100644 index 0000000000..2df3b6bc2f --- /dev/null +++ b/components/_util/cssinjs/linters/interface.ts @@ -0,0 +1,9 @@ +export interface LinterInfo { + path?: string; + hashId?: string; + parentSelectors: string[]; +} + +export interface Linter { + (key: string, value: string | number, info: LinterInfo): void; +} diff --git a/components/_util/cssinjs/linters/legacyNotSelectorLinter.ts b/components/_util/cssinjs/linters/legacyNotSelectorLinter.ts new file mode 100644 index 0000000000..f38bf5a331 --- /dev/null +++ b/components/_util/cssinjs/linters/legacyNotSelectorLinter.ts @@ -0,0 +1,33 @@ +import type { Linter, LinterInfo } from './interface'; +import { lintWarning } from './utils'; + +function isConcatSelector(selector: string) { + const notContent = selector.match(/:not\(([^)]*)\)/)?.[1] || ''; + + // split selector. e.g. + // `h1#a.b` => ['h1', #a', '.b'] + const splitCells = notContent.split(/(\[[^[]*])|(?=[.#])/).filter(str => str); + + return splitCells.length > 1; +} + +function parsePath(info: LinterInfo) { + return info.parentSelectors.reduce((prev, cur) => { + if (!prev) { + return cur; + } + + return cur.includes('&') ? cur.replace(/&/g, prev) : `${prev} ${cur}`; + }, ''); +} + +const linter: Linter = (_key, _value, info) => { + const parentSelectorPath = parsePath(info); + const notList = parentSelectorPath.match(/:not\([^)]*\)/g) || []; + + if (notList.length > 0 && notList.some(isConcatSelector)) { + lintWarning(`Concat ':not' selector not support in legacy browsers.`, info); + } +}; + +export default linter; diff --git a/components/_util/cssinjs/linters/logicalPropertiesLinter.ts b/components/_util/cssinjs/linters/logicalPropertiesLinter.ts new file mode 100644 index 0000000000..bdddcf73f4 --- /dev/null +++ b/components/_util/cssinjs/linters/logicalPropertiesLinter.ts @@ -0,0 +1,88 @@ +import type { Linter } from './interface'; +import { lintWarning } from './utils'; + +const linter: Linter = (key, value, info) => { + switch (key) { + case 'marginLeft': + case 'marginRight': + case 'paddingLeft': + case 'paddingRight': + case 'left': + case 'right': + case 'borderLeft': + case 'borderLeftWidth': + case 'borderLeftStyle': + case 'borderLeftColor': + case 'borderRight': + case 'borderRightWidth': + case 'borderRightStyle': + case 'borderRightColor': + case 'borderTopLeftRadius': + case 'borderTopRightRadius': + case 'borderBottomLeftRadius': + case 'borderBottomRightRadius': + lintWarning( + `You seem to be using non-logical property '${key}' which is not compatible with RTL mode. Please use logical properties and values instead. For more information: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties.`, + info, + ); + return; + case 'margin': + case 'padding': + case 'borderWidth': + case 'borderStyle': + // case 'borderColor': + if (typeof value === 'string') { + const valueArr = value.split(' ').map(item => item.trim()); + if (valueArr.length === 4 && valueArr[1] !== valueArr[3]) { + lintWarning( + `You seem to be using '${key}' property with different left ${key} and right ${key}, which is not compatible with RTL mode. Please use logical properties and values instead. For more information: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties.`, + info, + ); + } + } + return; + case 'clear': + case 'textAlign': + if (value === 'left' || value === 'right') { + lintWarning( + `You seem to be using non-logical value '${value}' of ${key}, which is not compatible with RTL mode. Please use logical properties and values instead. For more information: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties.`, + info, + ); + } + return; + case 'borderRadius': + if (typeof value === 'string') { + const radiusGroups = value.split('/').map(item => item.trim()); + const invalid = radiusGroups.reduce((result, group) => { + if (result) { + return result; + } + const radiusArr = group.split(' ').map(item => item.trim()); + // borderRadius: '2px 4px' + if (radiusArr.length >= 2 && radiusArr[0] !== radiusArr[1]) { + return true; + } + // borderRadius: '4px 4px 2px' + if (radiusArr.length === 3 && radiusArr[1] !== radiusArr[2]) { + return true; + } + // borderRadius: '4px 4px 2px 4px' + if (radiusArr.length === 4 && radiusArr[2] !== radiusArr[3]) { + return true; + } + return result; + }, false); + + if (invalid) { + lintWarning( + `You seem to be using non-logical value '${value}' of ${key}, which is not compatible with RTL mode. Please use logical properties and values instead. For more information: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties.`, + info, + ); + } + } + return; + default: + } +}; + +export default linter; diff --git a/components/_util/cssinjs/linters/utils.ts b/components/_util/cssinjs/linters/utils.ts new file mode 100644 index 0000000000..5b0853ff2f --- /dev/null +++ b/components/_util/cssinjs/linters/utils.ts @@ -0,0 +1,13 @@ +import devWarning from '../../../vc-util/warning'; +import type { LinterInfo } from './interface'; + +export function lintWarning(message: string, info: LinterInfo) { + const { path, parentSelectors } = info; + + devWarning( + false, + `[Ant Design Vue CSS-in-JS] ${path ? `Error in '${path}': ` : ''}${message}${ + parentSelectors.length ? ` Selector info: ${parentSelectors.join(' -> ')}` : '' + }`, + ); +} diff --git a/components/_util/cssinjs/theme/Theme.ts b/components/_util/cssinjs/theme/Theme.ts new file mode 100644 index 0000000000..608bdde6cd --- /dev/null +++ b/components/_util/cssinjs/theme/Theme.ts @@ -0,0 +1,38 @@ +import warning from '../../warning'; +import type { DerivativeFunc, TokenType } from './interface'; + +let uuid = 0; + +/** + * Theme with algorithms to derive tokens from design tokens. + * Use `createTheme` first which will help to manage the theme instance cache. + */ +export default class Theme { + private derivatives: DerivativeFunc[]; + public readonly id: number; + + constructor( + derivatives: + | DerivativeFunc + | DerivativeFunc[], + ) { + this.derivatives = Array.isArray(derivatives) ? derivatives : [derivatives]; + this.id = uuid; + + if (derivatives.length === 0) { + warning( + derivatives.length > 0, + '[Ant Design Vue CSS-in-JS] Theme should have at least one derivative function.', + ); + } + + uuid += 1; + } + + getDerivativeToken(token: DesignToken): DerivativeToken { + return this.derivatives.reduce( + (result, derivative) => derivative(token, result), + undefined as any, + ); + } +} diff --git a/components/_util/cssinjs/theme/ThemeCache.ts b/components/_util/cssinjs/theme/ThemeCache.ts new file mode 100644 index 0000000000..db76ffa6a4 --- /dev/null +++ b/components/_util/cssinjs/theme/ThemeCache.ts @@ -0,0 +1,135 @@ +import type Theme from './Theme'; +import type { DerivativeFunc } from './interface'; + +// ================================== Cache ================================== +type ThemeCacheMap = Map< + DerivativeFunc, + { + map?: ThemeCacheMap; + value?: [Theme, number]; + } +>; + +type DerivativeOptions = DerivativeFunc[]; + +export function sameDerivativeOption(left: DerivativeOptions, right: DerivativeOptions) { + if (left.length !== right.length) { + return false; + } + for (let i = 0; i < left.length; i++) { + if (left[i] !== right[i]) { + return false; + } + } + return true; +} + +export default class ThemeCache { + public static MAX_CACHE_SIZE = 20; + public static MAX_CACHE_OFFSET = 5; + + private readonly cache: ThemeCacheMap; + private keys: DerivativeOptions[]; + private cacheCallTimes: number; + + constructor() { + this.cache = new Map(); + this.keys = []; + this.cacheCallTimes = 0; + } + + public size(): number { + return this.keys.length; + } + + private internalGet( + derivativeOption: DerivativeOptions, + updateCallTimes = false, + ): [Theme, number] | undefined { + let cache: ReturnType = { map: this.cache }; + derivativeOption.forEach(derivative => { + if (!cache) { + cache = undefined; + } else { + cache = cache?.map?.get(derivative); + } + }); + if (cache?.value && updateCallTimes) { + cache.value[1] = this.cacheCallTimes++; + } + return cache?.value; + } + + public get(derivativeOption: DerivativeOptions): Theme | undefined { + return this.internalGet(derivativeOption, true)?.[0]; + } + + public has(derivativeOption: DerivativeOptions): boolean { + return !!this.internalGet(derivativeOption); + } + + public set(derivativeOption: DerivativeOptions, value: Theme): void { + // New cache + if (!this.has(derivativeOption)) { + if (this.size() + 1 > ThemeCache.MAX_CACHE_SIZE + ThemeCache.MAX_CACHE_OFFSET) { + const [targetKey] = this.keys.reduce<[DerivativeOptions, number]>( + (result, key) => { + const [, callTimes] = result; + if (this.internalGet(key)![1] < callTimes) { + return [key, this.internalGet(key)![1]]; + } + return result; + }, + [this.keys[0], this.cacheCallTimes], + ); + this.delete(targetKey); + } + + this.keys.push(derivativeOption); + } + + let cache = this.cache; + derivativeOption.forEach((derivative, index) => { + if (index === derivativeOption.length - 1) { + cache.set(derivative, { value: [value, this.cacheCallTimes++] }); + } else { + const cacheValue = cache.get(derivative); + if (!cacheValue) { + cache.set(derivative, { map: new Map() }); + } else if (!cacheValue.map) { + cacheValue.map = new Map(); + } + cache = cache.get(derivative)!.map!; + } + }); + } + + private deleteByPath( + currentCache: ThemeCacheMap, + derivatives: DerivativeFunc[], + ): Theme | undefined { + const cache = currentCache.get(derivatives[0])!; + if (derivatives.length === 1) { + if (!cache.map) { + currentCache.delete(derivatives[0]); + } else { + currentCache.set(derivatives[0], { map: cache.map }); + } + return cache.value?.[0]; + } + const result = this.deleteByPath(cache.map!, derivatives.slice(1)); + if ((!cache.map || cache.map.size === 0) && !cache.value) { + currentCache.delete(derivatives[0]); + } + return result; + } + + public delete(derivativeOption: DerivativeOptions): Theme | undefined { + // If cache exists + if (this.has(derivativeOption)) { + this.keys = this.keys.filter(item => !sameDerivativeOption(item, derivativeOption)); + return this.deleteByPath(this.cache, derivativeOption); + } + return undefined; + } +} diff --git a/components/_util/cssinjs/theme/createTheme.ts b/components/_util/cssinjs/theme/createTheme.ts new file mode 100644 index 0000000000..9f73f58a16 --- /dev/null +++ b/components/_util/cssinjs/theme/createTheme.ts @@ -0,0 +1,26 @@ +import ThemeCache from './ThemeCache'; +import Theme from './Theme'; +import type { DerivativeFunc, TokenType } from './interface'; + +const cacheThemes = new ThemeCache(); + +/** + * Same as new Theme, but will always return same one if `derivative` not changed. + */ +export default function createTheme< + DesignToken extends TokenType, + DerivativeToken extends TokenType, +>( + derivatives: + | DerivativeFunc[] + | DerivativeFunc, +) { + const derivativeArr = Array.isArray(derivatives) ? derivatives : [derivatives]; + // Create new theme if not exist + if (!cacheThemes.has(derivativeArr)) { + cacheThemes.set(derivativeArr, new Theme(derivativeArr)); + } + + // Get theme from cache and return + return cacheThemes.get(derivativeArr)!; +} diff --git a/components/_util/cssinjs/theme/index.ts b/components/_util/cssinjs/theme/index.ts new file mode 100644 index 0000000000..b3c2ff4b1e --- /dev/null +++ b/components/_util/cssinjs/theme/index.ts @@ -0,0 +1,4 @@ +export { default as createTheme } from './createTheme'; +export { default as Theme } from './Theme'; +export { default as ThemeCache } from './ThemeCache'; +export type { TokenType, DerivativeFunc } from './interface'; diff --git a/components/_util/cssinjs/theme/interface.ts b/components/_util/cssinjs/theme/interface.ts new file mode 100644 index 0000000000..827706ce2c --- /dev/null +++ b/components/_util/cssinjs/theme/interface.ts @@ -0,0 +1,5 @@ +export type TokenType = object; +export type DerivativeFunc = ( + designToken: DesignToken, + derivativeToken?: DerivativeToken, +) => DerivativeToken; diff --git a/components/_util/cssinjs/transformers/interface.ts b/components/_util/cssinjs/transformers/interface.ts new file mode 100644 index 0000000000..a7120e8148 --- /dev/null +++ b/components/_util/cssinjs/transformers/interface.ts @@ -0,0 +1,5 @@ +import type { CSSObject } from '..'; + +export interface Transformer { + visit?: (cssObj: CSSObject) => CSSObject; +} diff --git a/components/_util/cssinjs/transformers/legacyLogicalProperties.ts b/components/_util/cssinjs/transformers/legacyLogicalProperties.ts new file mode 100644 index 0000000000..58e00c89f4 --- /dev/null +++ b/components/_util/cssinjs/transformers/legacyLogicalProperties.ts @@ -0,0 +1,162 @@ +import type { CSSObject } from '..'; +import type { Transformer } from './interface'; + +function splitValues(value: string | number) { + if (typeof value === 'number') { + return [value]; + } + + const splitStyle = String(value).split(/\s+/); + + // Combine styles split in brackets, like `calc(1px + 2px)` + let temp = ''; + let brackets = 0; + return splitStyle.reduce((list, item) => { + if (item.includes('(')) { + temp += item; + brackets += item.split('(').length - 1; + } else if (item.includes(')')) { + temp += ` ${item}`; + brackets -= item.split(')').length - 1; + if (brackets === 0) { + list.push(temp); + temp = ''; + } + } else if (brackets > 0) { + temp += ` ${item}`; + } else { + list.push(item); + } + return list; + }, []); +} + +type MatchValue = string[] & { + notSplit?: boolean; +}; + +function noSplit(list: MatchValue): MatchValue { + list.notSplit = true; + return list; +} + +const keyMap: Record = { + // Inset + inset: ['top', 'right', 'bottom', 'left'], + insetBlock: ['top', 'bottom'], + insetBlockStart: ['top'], + insetBlockEnd: ['bottom'], + insetInline: ['left', 'right'], + insetInlineStart: ['left'], + insetInlineEnd: ['right'], + + // Margin + marginBlock: ['marginTop', 'marginBottom'], + marginBlockStart: ['marginTop'], + marginBlockEnd: ['marginBottom'], + marginInline: ['marginLeft', 'marginRight'], + marginInlineStart: ['marginLeft'], + marginInlineEnd: ['marginRight'], + + // Padding + paddingBlock: ['paddingTop', 'paddingBottom'], + paddingBlockStart: ['paddingTop'], + paddingBlockEnd: ['paddingBottom'], + paddingInline: ['paddingLeft', 'paddingRight'], + paddingInlineStart: ['paddingLeft'], + paddingInlineEnd: ['paddingRight'], + + // Border + borderBlock: noSplit(['borderTop', 'borderBottom']), + borderBlockStart: noSplit(['borderTop']), + borderBlockEnd: noSplit(['borderBottom']), + borderInline: noSplit(['borderLeft', 'borderRight']), + borderInlineStart: noSplit(['borderLeft']), + borderInlineEnd: noSplit(['borderRight']), + + // Border width + borderBlockWidth: ['borderTopWidth', 'borderBottomWidth'], + borderBlockStartWidth: ['borderTopWidth'], + borderBlockEndWidth: ['borderBottomWidth'], + borderInlineWidth: ['borderLeftWidth', 'borderRightWidth'], + borderInlineStartWidth: ['borderLeftWidth'], + borderInlineEndWidth: ['borderRightWidth'], + + // Border style + borderBlockStyle: ['borderTopStyle', 'borderBottomStyle'], + borderBlockStartStyle: ['borderTopStyle'], + borderBlockEndStyle: ['borderBottomStyle'], + borderInlineStyle: ['borderLeftStyle', 'borderRightStyle'], + borderInlineStartStyle: ['borderLeftStyle'], + borderInlineEndStyle: ['borderRightStyle'], + + // Border color + borderBlockColor: ['borderTopColor', 'borderBottomColor'], + borderBlockStartColor: ['borderTopColor'], + borderBlockEndColor: ['borderBottomColor'], + borderInlineColor: ['borderLeftColor', 'borderRightColor'], + borderInlineStartColor: ['borderLeftColor'], + borderInlineEndColor: ['borderRightColor'], + + // Border radius + borderStartStartRadius: ['borderTopLeftRadius'], + borderStartEndRadius: ['borderTopRightRadius'], + borderEndStartRadius: ['borderBottomLeftRadius'], + borderEndEndRadius: ['borderBottomRightRadius'], +}; + +function skipCheck(value: string | number) { + return { _skip_check_: true, value }; +} + +/** + * Convert css logical properties to legacy properties. + * Such as: `margin-block-start` to `margin-top`. + * Transform list: + * - inset + * - margin + * - padding + * - border + */ +const transform: Transformer = { + visit: cssObj => { + const clone: CSSObject = {}; + + Object.keys(cssObj).forEach(key => { + const value = cssObj[key]; + const matchValue = keyMap[key]; + + if (matchValue && (typeof value === 'number' || typeof value === 'string')) { + const values = splitValues(value); + + if (matchValue.length && matchValue.notSplit) { + // not split means always give same value like border + matchValue.forEach(matchKey => { + clone[matchKey] = skipCheck(value); + }); + } else if (matchValue.length === 1) { + // Handle like `marginBlockStart` => `marginTop` + clone[matchValue[0]] = skipCheck(value); + } else if (matchValue.length === 2) { + // Handle like `marginBlock` => `marginTop` & `marginBottom` + matchValue.forEach((matchKey, index) => { + clone[matchKey] = skipCheck(values[index] ?? values[0]); + }); + } else if (matchValue.length === 4) { + // Handle like `inset` => `top` & `right` & `bottom` & `left` + matchValue.forEach((matchKey, index) => { + clone[matchKey] = skipCheck(values[index] ?? values[index - 2] ?? values[0]); + }); + } else { + clone[key] = value; + } + } else { + clone[key] = value; + } + }); + + return clone; + }, +}; + +export default transform; diff --git a/components/_util/cssinjs/util.ts b/components/_util/cssinjs/util.ts new file mode 100644 index 0000000000..b4115a37df --- /dev/null +++ b/components/_util/cssinjs/util.ts @@ -0,0 +1,68 @@ +import hash from '@emotion/hash'; +import { removeCSS, updateCSS } from '../../vc-util/Dom/dynamicCSS'; +import canUseDom from '../canUseDom'; + +export function flattenToken(token: any) { + let str = ''; + Object.keys(token).forEach(key => { + const value = token[key]; + str += key; + if (value && typeof value === 'object') { + str += flattenToken(value); + } else { + str += value; + } + }); + return str; +} + +/** + * Convert derivative token to key string + */ +export function token2key(token: any, salt: string): string { + return hash(`${salt}_${flattenToken(token)}`); +} + +const layerKey = `layer-${Date.now()}-${Math.random()}`.replace(/\./g, ''); +const layerWidth = '903px'; + +function supportSelector(styleStr: string, handleElement?: (ele: HTMLElement) => void): boolean { + if (canUseDom()) { + updateCSS(styleStr, layerKey); + + const ele = document.createElement('div'); + ele.style.position = 'fixed'; + ele.style.left = '0'; + ele.style.top = '0'; + handleElement?.(ele); + document.body.appendChild(ele); + + if (process.env.NODE_ENV !== 'production') { + ele.innerHTML = 'Test'; + ele.style.zIndex = '9999999'; + } + + const support = getComputedStyle(ele).width === layerWidth; + + ele.parentNode?.removeChild(ele); + removeCSS(layerKey); + + return support; + } + + return false; +} + +let canLayer: boolean | undefined = undefined; +export function supportLayer(): boolean { + if (canLayer === undefined) { + canLayer = supportSelector( + `@layer ${layerKey} { .${layerKey} { width: ${layerWidth}!important; } }`, + ele => { + ele.className = layerKey; + }, + ); + } + + return canLayer!; +} diff --git a/components/_util/extendsObject.ts b/components/_util/extendsObject.ts new file mode 100644 index 0000000000..3f6959ce4e --- /dev/null +++ b/components/_util/extendsObject.ts @@ -0,0 +1,21 @@ +type RecordType = Record; + +function extendsObject(...list: T[]) { + const result: RecordType = { ...list[0] }; + + for (let i = 1; i < list.length; i++) { + const obj = list[i]; + if (obj) { + Object.keys(obj).forEach(key => { + const val = obj[key]; + if (val !== undefined) { + result[key] = val; + } + }); + } + } + + return result; +} + +export default extendsObject; diff --git a/components/_util/getLocale.js b/components/_util/getLocale.js deleted file mode 100644 index 03aeca95bb..0000000000 --- a/components/_util/getLocale.js +++ /dev/null @@ -1,30 +0,0 @@ -export function getComponentLocale(props, context, componentName, getDefaultLocale) { - let locale = {}; - if (context && context.antLocale && context.antLocale[componentName]) { - locale = context.antLocale[componentName]; - } else { - const defaultLocale = getDefaultLocale(); - // TODO: make default lang of antd be English - // https://github.com/ant-design/ant-design/issues/6334 - locale = defaultLocale.default || defaultLocale; - } - - const result = { - ...locale, - ...props.locale, - }; - result.lang = { - ...locale.lang, - ...props.locale.lang, - }; - return result; -} - -export function getLocaleCode(context) { - const localeCode = context.antLocale && context.antLocale.locale; - // Had use LocaleProvide but didn't set locale - if (context.antLocale && context.antLocale.exist && !localeCode) { - return 'zh-cn'; - } - return localeCode; -} diff --git a/components/_util/getRequestAnimationFrame.js b/components/_util/getRequestAnimationFrame.ts similarity index 100% rename from components/_util/getRequestAnimationFrame.js rename to components/_util/getRequestAnimationFrame.ts diff --git a/components/_util/getScroll.ts b/components/_util/getScroll.ts index 70b50141d1..2889b43ed7 100644 --- a/components/_util/getScroll.ts +++ b/components/_util/getScroll.ts @@ -1,4 +1,4 @@ -export function isWindow(obj: any) { +export function isWindow(obj: any): obj is Window { return obj !== null && obj !== undefined && obj === obj.window; } @@ -12,16 +12,22 @@ export default function getScroll( const method = top ? 'scrollTop' : 'scrollLeft'; let result = 0; if (isWindow(target)) { - result = (target as Window)[top ? 'pageYOffset' : 'pageXOffset']; + result = target[top ? 'pageYOffset' : 'pageXOffset']; } else if (target instanceof Document) { result = target.documentElement[method]; + } else if (target instanceof HTMLElement) { + result = target[method]; } else if (target) { - result = (target as HTMLElement)[method]; + // According to the type inference, the `target` is `never` type. + // Since we configured the loose mode type checking, and supports mocking the target with such shape below:: + // `{ documentElement: { scrollLeft: 200, scrollTop: 400 } }`, + // the program may falls into this branch. + // Check the corresponding tests for details. Don't sure what is the real scenario this happens. + result = target[method]; } + if (target && !isWindow(target) && typeof result !== 'number') { - result = ((target as HTMLElement).ownerDocument || (target as Document)).documentElement?.[ - method - ]; + result = ((target.ownerDocument ?? target) as any).documentElement?.[method]; } return result; } diff --git a/components/_util/hooks/_vueuse/useElementSize.ts b/components/_util/hooks/_vueuse/useElementSize.ts index 90beea16bd..bc90e9a06b 100644 --- a/components/_util/hooks/_vueuse/useElementSize.ts +++ b/components/_util/hooks/_vueuse/useElementSize.ts @@ -1,4 +1,4 @@ -import { ref, watch } from 'vue'; +import { shallowRef, watch } from 'vue'; import type { MaybeComputedElementRef } from './unrefElement'; import type { UseResizeObserverOptions } from './useResizeObserver'; import { useResizeObserver } from './useResizeObserver'; @@ -23,8 +23,8 @@ export function useElementSize( options: UseResizeObserverOptions = {}, ) { const { box = 'content-box' } = options; - const width = ref(initialSize.width); - const height = ref(initialSize.height); + const width = shallowRef(initialSize.width); + const height = shallowRef(initialSize.height); useResizeObserver( target, diff --git a/components/_util/hooks/_vueuse/useMutationObserver.ts b/components/_util/hooks/_vueuse/useMutationObserver.ts new file mode 100644 index 0000000000..3a191d396f --- /dev/null +++ b/components/_util/hooks/_vueuse/useMutationObserver.ts @@ -0,0 +1,62 @@ +import { tryOnScopeDispose } from './tryOnScopeDispose'; +import { watch } from 'vue'; +import type { MaybeElementRef } from './unrefElement'; +import { unrefElement } from './unrefElement'; +import { useSupported } from './useSupported'; +import type { ConfigurableWindow } from './_configurable'; +import { defaultWindow } from './_configurable'; + +export interface UseMutationObserverOptions extends MutationObserverInit, ConfigurableWindow {} + +/** + * Watch for changes being made to the DOM tree. + * + * @see https://vueuse.org/useMutationObserver + * @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver MutationObserver MDN + * @param target + * @param callback + * @param options + */ +export function useMutationObserver( + target: MaybeElementRef, + callback: MutationCallback, + options: UseMutationObserverOptions = {}, +) { + const { window = defaultWindow, ...mutationOptions } = options; + let observer: MutationObserver | undefined; + const isSupported = useSupported(() => window && 'MutationObserver' in window); + + const cleanup = () => { + if (observer) { + observer.disconnect(); + observer = undefined; + } + }; + + const stopWatch = watch( + () => unrefElement(target), + el => { + cleanup(); + + if (isSupported.value && window && el) { + observer = new MutationObserver(callback); + observer!.observe(el, mutationOptions); + } + }, + { immediate: true }, + ); + + const stop = () => { + cleanup(); + stopWatch(); + }; + + tryOnScopeDispose(stop); + + return { + isSupported, + stop, + }; +} + +export type UseMutationObserverReturn = ReturnType; diff --git a/components/_util/hooks/_vueuse/useSupported.ts b/components/_util/hooks/_vueuse/useSupported.ts index 7705bf63d2..360e8e613f 100644 --- a/components/_util/hooks/_vueuse/useSupported.ts +++ b/components/_util/hooks/_vueuse/useSupported.ts @@ -1,9 +1,8 @@ import { tryOnMounted } from './tryOnMounted'; -import type { Ref } from 'vue'; -import { ref } from 'vue'; +import { shallowRef } from 'vue'; export function useSupported(callback: () => unknown, sync = false) { - const isSupported = ref() as Ref; + const isSupported = shallowRef(); const update = () => (isSupported.value = Boolean(callback())); diff --git a/components/_util/hooks/useBreakpoint.ts b/components/_util/hooks/useBreakpoint.ts index 932c22f5e3..1c20a996db 100644 --- a/components/_util/hooks/useBreakpoint.ts +++ b/components/_util/hooks/useBreakpoint.ts @@ -1,19 +1,21 @@ import type { Ref } from 'vue'; -import { onMounted, onUnmounted, ref } from 'vue'; +import { onMounted, onUnmounted, shallowRef } from 'vue'; import type { ScreenMap } from '../../_util/responsiveObserve'; -import ResponsiveObserve from '../../_util/responsiveObserve'; +import useResponsiveObserve from '../../_util/responsiveObserve'; function useBreakpoint(): Ref { - const screens = ref({}); + const screens = shallowRef({}); let token = null; + const responsiveObserve = useResponsiveObserve(); + onMounted(() => { - token = ResponsiveObserve.subscribe(supportScreens => { + token = responsiveObserve.value.subscribe(supportScreens => { screens.value = supportScreens; }); }); onUnmounted(() => { - ResponsiveObserve.unsubscribe(token); + responsiveObserve.value.unsubscribe(token); }); return screens; diff --git a/components/_util/hooks/useConfigInject.ts b/components/_util/hooks/useConfigInject.ts deleted file mode 100644 index be02d92418..0000000000 --- a/components/_util/hooks/useConfigInject.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { RequiredMark } from '../../form/Form'; -import type { ComputedRef, UnwrapRef } from 'vue'; -import { computed, inject } from 'vue'; -import type { ConfigProviderProps, CSPConfig, Direction, SizeType } from '../../config-provider'; -import { defaultConfigProvider } from '../../config-provider'; -import type { VueNode } from '../type'; -import type { ValidateMessages } from '../../form/interface'; - -export default ( - name: string, - props: Record, -): { - configProvider: UnwrapRef; - prefixCls: ComputedRef; - rootPrefixCls: ComputedRef; - direction: ComputedRef; - size: ComputedRef; - getTargetContainer: ComputedRef<() => HTMLElement>; - space: ComputedRef<{ size: SizeType | number }>; - pageHeader: ComputedRef<{ ghost: boolean }>; - form?: ComputedRef<{ - requiredMark?: RequiredMark; - colon?: boolean; - validateMessages?: ValidateMessages; - }>; - autoInsertSpaceInButton: ComputedRef; - renderEmpty?: ComputedRef<(componentName?: string) => VueNode>; - virtual: ComputedRef; - dropdownMatchSelectWidth: ComputedRef; - getPopupContainer: ComputedRef; - getPrefixCls: ConfigProviderProps['getPrefixCls']; - autocomplete: ComputedRef; - csp: ComputedRef; -} => { - const configProvider = inject>( - 'configProvider', - defaultConfigProvider, - ); - const prefixCls = computed(() => configProvider.getPrefixCls(name, props.prefixCls)); - const direction = computed(() => props.direction ?? configProvider.direction); - const rootPrefixCls = computed(() => configProvider.getPrefixCls()); - const autoInsertSpaceInButton = computed(() => configProvider.autoInsertSpaceInButton); - const renderEmpty = computed(() => configProvider.renderEmpty); - const space = computed(() => configProvider.space); - const pageHeader = computed(() => configProvider.pageHeader); - const form = computed(() => configProvider.form); - const getTargetContainer = computed( - () => props.getTargetContainer || configProvider.getTargetContainer, - ); - const getPopupContainer = computed( - () => props.getPopupContainer || configProvider.getPopupContainer, - ); - - const dropdownMatchSelectWidth = computed( - () => props.dropdownMatchSelectWidth ?? configProvider.dropdownMatchSelectWidth, - ); - const virtual = computed( - () => - (props.virtual === undefined ? configProvider.virtual !== false : props.virtual !== false) && - dropdownMatchSelectWidth.value !== false, - ); - const size = computed(() => props.size || configProvider.componentSize); - const autocomplete = computed(() => props.autocomplete || configProvider.input?.autocomplete); - const csp = computed(() => configProvider.csp); - return { - configProvider, - prefixCls, - direction, - size, - getTargetContainer, - getPopupContainer, - space, - pageHeader, - form, - autoInsertSpaceInButton, - renderEmpty, - virtual, - dropdownMatchSelectWidth, - rootPrefixCls, - getPrefixCls: configProvider.getPrefixCls, - autocomplete, - csp, - }; -}; diff --git a/components/_util/hooks/useDestroyed.ts b/components/_util/hooks/useDestroyed.ts index 57cc172302..7fa1f50228 100644 --- a/components/_util/hooks/useDestroyed.ts +++ b/components/_util/hooks/useDestroyed.ts @@ -1,7 +1,7 @@ -import { onBeforeUnmount, ref } from 'vue'; +import { onBeforeUnmount, shallowRef } from 'vue'; const useDestroyed = () => { - const destroyed = ref(false); + const destroyed = shallowRef(false); onBeforeUnmount(() => { destroyed.value = true; }); diff --git a/components/_util/hooks/useFlexGapSupport.ts b/components/_util/hooks/useFlexGapSupport.ts index eb3c100ec7..592cc762a0 100644 --- a/components/_util/hooks/useFlexGapSupport.ts +++ b/components/_util/hooks/useFlexGapSupport.ts @@ -1,8 +1,8 @@ -import { onMounted, ref } from 'vue'; +import { onMounted, shallowRef } from 'vue'; import { detectFlexGapSupported } from '../styleChecker'; export default () => { - const flexible = ref(false); + const flexible = shallowRef(false); onMounted(() => { flexible.value = detectFlexGapSupported(); }); diff --git a/components/_util/hooks/useId.ts b/components/_util/hooks/useId.ts new file mode 100644 index 0000000000..fea54f9082 --- /dev/null +++ b/components/_util/hooks/useId.ts @@ -0,0 +1,30 @@ +import { ref } from 'vue'; +import canUseDom from '../../_util/canUseDom'; + +let uuid = 0; + +/** Is client side and not jsdom */ +export const isBrowserClient = process.env.NODE_ENV !== 'test' && canUseDom(); + +/** Get unique id for accessibility usage */ +export function getUUID(): number | string { + let retId: string | number; + + // Test never reach + /* istanbul ignore if */ + if (isBrowserClient) { + retId = uuid; + uuid += 1; + } else { + retId = 'TEST_OR_SSR'; + } + + return retId; +} + +export default function useId(id = ref('')) { + // Inner id for accessibility usage. Only work in client side + const innerId = `vc_unique_${getUUID()}`; + + return id.value || innerId; +} diff --git a/components/_util/hooks/useLayoutState.ts b/components/_util/hooks/useLayoutState.ts index 78630a8825..95189fccdf 100644 --- a/components/_util/hooks/useLayoutState.ts +++ b/components/_util/hooks/useLayoutState.ts @@ -1,5 +1,5 @@ import type { Ref } from 'vue'; -import { onBeforeUnmount, ref } from 'vue'; +import { onBeforeUnmount, shallowRef } from 'vue'; import raf from '../raf'; export type Updater = (prev: State) => State; @@ -9,11 +9,11 @@ export type Updater = (prev: State) => State; export function useLayoutState( defaultState: State, ): [Ref, (updater: Updater) => void] { - const stateRef = ref(defaultState); + const stateRef = shallowRef(defaultState); let tempState = stateRef.value; let updateBatchRef = []; - const rafRef = ref(); + const rafRef = shallowRef(); function setFrameState(updater: Updater) { raf.cancel(rafRef.value); updateBatchRef.push(updater); diff --git a/components/_util/hooks/useScrollLocker.ts b/components/_util/hooks/useScrollLocker.ts new file mode 100644 index 0000000000..993ff7d1ca --- /dev/null +++ b/components/_util/hooks/useScrollLocker.ts @@ -0,0 +1,48 @@ +import type { Ref } from 'vue'; +import { computed, watchEffect } from 'vue'; +import { updateCSS, removeCSS } from '../../vc-util/Dom/dynamicCSS'; +import getScrollBarSize from '../../_util/getScrollBarSize'; + +const UNIQUE_ID = `vc-util-locker-${Date.now()}`; + +let uuid = 0; + +/**../vc-util/Dom/dynam + * Test usage export. Do not use in your production + */ +export function isBodyOverflowing() { + return ( + document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight) && + window.innerWidth > document.body.offsetWidth + ); +} + +export default function useScrollLocker(lock?: Ref) { + const mergedLock = computed(() => !!lock && !!lock.value); + uuid += 1; + const id = `${UNIQUE_ID}_${uuid}`; + + watchEffect( + onClear => { + if (mergedLock.value) { + const scrollbarSize = getScrollBarSize(); + const isOverflow = isBodyOverflowing(); + + updateCSS( + ` +html body { + overflow-y: hidden; + ${isOverflow ? `width: calc(100% - ${scrollbarSize}px);` : ''} +}`, + id, + ); + } else { + removeCSS(id); + } + onClear(() => { + removeCSS(id); + }); + }, + { flush: 'post' }, + ); +} diff --git a/components/_util/hooks/useSize.ts b/components/_util/hooks/useSize.ts deleted file mode 100644 index 62b392e603..0000000000 --- a/components/_util/hooks/useSize.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ComputedRef, UnwrapRef } from 'vue'; -import { computed, inject, provide } from 'vue'; -import type { ConfigProviderProps, SizeType } from '../../config-provider'; -import { defaultConfigProvider } from '../../config-provider'; - -const sizeProvider = Symbol('SizeProvider'); - -const useProvideSize = (props: Record): ComputedRef => { - const configProvider = inject>( - 'configProvider', - defaultConfigProvider, - ); - const size = computed(() => props.size || configProvider.componentSize); - provide(sizeProvider, size); - return size; -}; - -const useInjectSize = (props?: Record): ComputedRef => { - const size: ComputedRef = props - ? computed(() => props.size) - : inject( - sizeProvider, - computed(() => 'default' as unknown as T), - ); - return size; -}; - -export { useInjectSize, sizeProvider, useProvideSize }; - -export default useProvideSize; diff --git a/components/_util/isMobile.js b/components/_util/isMobile.js deleted file mode 100644 index 96927fd3b6..0000000000 --- a/components/_util/isMobile.js +++ /dev/null @@ -1,110 +0,0 @@ -// MIT License from https://github.com/kaimallea/isMobile - -const applePhone = /iPhone/i; -const appleIpod = /iPod/i; -const appleTablet = /iPad/i; -const androidPhone = /\bAndroid(?:.+)Mobile\b/i; // Match 'Android' AND 'Mobile' -const androidTablet = /Android/i; -const amazonPhone = /\bAndroid(?:.+)SD4930UR\b/i; -const amazonTablet = /\bAndroid(?:.+)(?:KF[A-Z]{2,4})\b/i; -const windowsPhone = /Windows Phone/i; -const windowsTablet = /\bWindows(?:.+)ARM\b/i; // Match 'Windows' AND 'ARM' -const otherBlackberry = /BlackBerry/i; -const otherBlackberry10 = /BB10/i; -const otherOpera = /Opera Mini/i; -const otherChrome = /\b(CriOS|Chrome)(?:.+)Mobile/i; -const otherFirefox = /Mobile(?:.+)Firefox\b/i; // Match 'Mobile' AND 'Firefox' - -function match(regex, userAgent) { - return regex.test(userAgent); -} - -function isMobile(userAgent) { - let ua = userAgent || (typeof navigator !== 'undefined' ? navigator.userAgent : ''); - - // Facebook mobile app's integrated browser adds a bunch of strings that - // match everything. Strip it out if it exists. - let tmp = ua.split('[FBAN'); - if (typeof tmp[1] !== 'undefined') { - [ua] = tmp; - } - - // Twitter mobile app's integrated browser on iPad adds a "Twitter for - // iPhone" string. Same probably happens on other tablet platforms. - // This will confuse detection so strip it out if it exists. - tmp = ua.split('Twitter'); - if (typeof tmp[1] !== 'undefined') { - [ua] = tmp; - } - - const result = { - apple: { - phone: match(applePhone, ua) && !match(windowsPhone, ua), - ipod: match(appleIpod, ua), - tablet: !match(applePhone, ua) && match(appleTablet, ua) && !match(windowsPhone, ua), - device: - (match(applePhone, ua) || match(appleIpod, ua) || match(appleTablet, ua)) && - !match(windowsPhone, ua), - }, - amazon: { - phone: match(amazonPhone, ua), - tablet: !match(amazonPhone, ua) && match(amazonTablet, ua), - device: match(amazonPhone, ua) || match(amazonTablet, ua), - }, - android: { - phone: - (!match(windowsPhone, ua) && match(amazonPhone, ua)) || - (!match(windowsPhone, ua) && match(androidPhone, ua)), - tablet: - !match(windowsPhone, ua) && - !match(amazonPhone, ua) && - !match(androidPhone, ua) && - (match(amazonTablet, ua) || match(androidTablet, ua)), - device: - (!match(windowsPhone, ua) && - (match(amazonPhone, ua) || - match(amazonTablet, ua) || - match(androidPhone, ua) || - match(androidTablet, ua))) || - match(/\bokhttp\b/i, ua), - }, - windows: { - phone: match(windowsPhone, ua), - tablet: match(windowsTablet, ua), - device: match(windowsPhone, ua) || match(windowsTablet, ua), - }, - other: { - blackberry: match(otherBlackberry, ua), - blackberry10: match(otherBlackberry10, ua), - opera: match(otherOpera, ua), - firefox: match(otherFirefox, ua), - chrome: match(otherChrome, ua), - device: - match(otherBlackberry, ua) || - match(otherBlackberry10, ua) || - match(otherOpera, ua) || - match(otherFirefox, ua) || - match(otherChrome, ua), - }, - - // Additional - any: null, - phone: null, - tablet: null, - }; - result.any = - result.apple.device || result.android.device || result.windows.device || result.other.device; - - // excludes 'other' devices and ipods, targeting touchscreen phones - result.phone = result.apple.phone || result.android.phone || result.windows.phone; - result.tablet = result.apple.tablet || result.android.tablet || result.windows.tablet; - - return result; -} - -const defaultResult = { - ...isMobile(), - isMobile, -}; - -export default defaultResult; diff --git a/components/tooltip/placements.ts b/components/_util/placements.ts similarity index 100% rename from components/tooltip/placements.ts rename to components/_util/placements.ts diff --git a/components/_util/props-util/index.js b/components/_util/props-util/index.js index d67820cd9e..38b0c92523 100644 --- a/components/_util/props-util/index.js +++ b/components/_util/props-util/index.js @@ -71,6 +71,7 @@ const getSlots = ele => { return { ...slots, ...getScopedSlots(ele) }; }; +export const skipFlattenKey = Symbol('skipFlatten'); const flattenChildren = (children = [], filterEmpty = true) => { const temp = Array.isArray(children) ? children : [children]; const res = []; @@ -78,7 +79,11 @@ const flattenChildren = (children = [], filterEmpty = true) => { if (Array.isArray(child)) { res.push(...flattenChildren(child, filterEmpty)); } else if (child && child.type === Fragment) { - res.push(...flattenChildren(child.children, filterEmpty)); + if (child.key === skipFlattenKey) { + res.push(child); + } else { + res.push(...flattenChildren(child.children, filterEmpty)); + } } else if (child && isVNode(child)) { if (filterEmpty && !isEmptyElement(child)) { res.push(child); diff --git a/components/_util/requestAnimationTimeout.js b/components/_util/requestAnimationTimeout.ts similarity index 100% rename from components/_util/requestAnimationTimeout.js rename to components/_util/requestAnimationTimeout.ts diff --git a/components/_util/responsiveObserve.ts b/components/_util/responsiveObserve.ts index 497f01bb9b..11bd47db26 100644 --- a/components/_util/responsiveObserve.ts +++ b/components/_util/responsiveObserve.ts @@ -1,75 +1,85 @@ +import { computed } from 'vue'; +import type { GlobalToken } from '../theme/interface'; +import { useToken } from '../theme/internal'; + export type Breakpoint = 'xxxl' | 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs'; export type BreakpointMap = Record; export type ScreenMap = Partial>; export type ScreenSizeMap = Partial>; export const responsiveArray: Breakpoint[] = ['xxxl', 'xxl', 'xl', 'lg', 'md', 'sm', 'xs']; +type SubscribeFunc = (screens: ScreenMap) => void; -export const responsiveMap: BreakpointMap = { - xs: '(max-width: 575px)', - sm: '(min-width: 576px)', - md: '(min-width: 768px)', - lg: '(min-width: 992px)', - xl: '(min-width: 1200px)', - xxl: '(min-width: 1600px)', - xxxl: '(min-width: 2000px)', -}; +const getResponsiveMap = (token: GlobalToken): BreakpointMap => ({ + xs: `(max-width: ${token.screenXSMax}px)`, + sm: `(min-width: ${token.screenSM}px)`, + md: `(min-width: ${token.screenMD}px)`, + lg: `(min-width: ${token.screenLG}px)`, + xl: `(min-width: ${token.screenXL}px)`, + xxl: `(min-width: ${token.screenXXL}px)`, + xxxl: `{min-width: ${token.screenXXXL}px}`, +}); -type SubscribeFunc = (screens: ScreenMap) => void; -const subscribers = new Map(); -let subUid = -1; -let screens = {}; +export default function useResponsiveObserver() { + const [, token] = useToken(); -const responsiveObserve = { - matchHandlers: {} as { - [prop: string]: { - mql: MediaQueryList; - listener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null; - }; - }, - dispatch(pointMap: ScreenMap) { - screens = pointMap; - subscribers.forEach(func => func(screens)); - return subscribers.size >= 1; - }, - subscribe(func: SubscribeFunc): number { - if (!subscribers.size) this.register(); - subUid += 1; - subscribers.set(subUid, func); - func(screens); - return subUid; - }, - unsubscribe(token: number) { - subscribers.delete(token); - if (!subscribers.size) this.unregister(); - }, - unregister() { - Object.keys(responsiveMap).forEach((screen: string) => { - const matchMediaQuery = responsiveMap[screen]; - const handler = this.matchHandlers[matchMediaQuery]; - handler?.mql.removeListener(handler?.listener); - }); - subscribers.clear(); - }, - register() { - Object.keys(responsiveMap).forEach((screen: string) => { - const matchMediaQuery = responsiveMap[screen]; - const listener = ({ matches }: { matches: boolean }) => { - this.dispatch({ - ...screens, - [screen]: matches, - }); - }; - const mql = window.matchMedia(matchMediaQuery); - mql.addListener(listener); - this.matchHandlers[matchMediaQuery] = { - mql, - listener, - }; + return computed(() => { + const responsiveMap: BreakpointMap = getResponsiveMap(token.value); + const subscribers = new Map(); + let subUid = -1; + let screens = {}; - listener(mql); - }); - }, -}; + return { + matchHandlers: {} as { + [prop: string]: { + mql: MediaQueryList; + listener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null; + }; + }, + dispatch(pointMap: ScreenMap) { + screens = pointMap; + subscribers.forEach(func => func(screens)); + return subscribers.size >= 1; + }, + subscribe(func: SubscribeFunc): number { + if (!subscribers.size) this.register(); + subUid += 1; + subscribers.set(subUid, func); + func(screens); + return subUid; + }, + unsubscribe(paramToken: number) { + subscribers.delete(paramToken); + if (!subscribers.size) this.unregister(); + }, + unregister() { + Object.keys(responsiveMap).forEach((screen: string) => { + const matchMediaQuery = responsiveMap[screen]; + const handler = this.matchHandlers[matchMediaQuery]; + handler?.mql.removeListener(handler?.listener); + }); + subscribers.clear(); + }, + register() { + Object.keys(responsiveMap).forEach((screen: string) => { + const matchMediaQuery = responsiveMap[screen]; + const listener = ({ matches }: { matches: boolean }) => { + this.dispatch({ + ...screens, + [screen]: matches, + }); + }; + const mql = window.matchMedia(matchMediaQuery); + mql.addListener(listener); + this.matchHandlers[matchMediaQuery] = { + mql, + listener, + }; -export default responsiveObserve; + listener(mql); + }); + }, + responsiveMap, + }; + }); +} diff --git a/components/_util/scrollTo.ts b/components/_util/scrollTo.ts index f41c1b649e..c9dbb89104 100644 --- a/components/_util/scrollTo.ts +++ b/components/_util/scrollTo.ts @@ -1,6 +1,6 @@ import raf from './raf'; -import getScroll, { isWindow } from './getScroll'; import { easeInOutCubic } from './easings'; +import getScroll, { isWindow } from './getScroll'; interface ScrollToOptions { /** Scroll container, default as window */ @@ -23,8 +23,8 @@ export default function scrollTo(y: number, options: ScrollToOptions = {}) { const nextScrollTop = easeInOutCubic(time > duration ? duration : time, scrollTop, y, duration); if (isWindow(container)) { (container as Window).scrollTo(window.pageXOffset, nextScrollTop); - } else if (container instanceof HTMLDocument || container.constructor.name === 'HTMLDocument') { - (container as HTMLDocument).documentElement.scrollTop = nextScrollTop; + } else if (container instanceof Document || container.constructor.name === 'HTMLDocument') { + (container as Document).documentElement.scrollTop = nextScrollTop; } else { (container as HTMLElement).scrollTop = nextScrollTop; } diff --git a/components/_util/shallowequal.js b/components/_util/shallowequal.js deleted file mode 100644 index b06a5ea60e..0000000000 --- a/components/_util/shallowequal.js +++ /dev/null @@ -1,50 +0,0 @@ -import { toRaw } from 'vue'; - -function shallowEqual(objA, objB, compare, compareContext) { - let ret = compare ? compare.call(compareContext, objA, objB) : void 0; - - if (ret !== void 0) { - return !!ret; - } - - if (objA === objB) { - return true; - } - - if (typeof objA !== 'object' || !objA || typeof objB !== 'object' || !objB) { - return false; - } - - const keysA = Object.keys(objA); - const keysB = Object.keys(objB); - - if (keysA.length !== keysB.length) { - return false; - } - - const bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB); - - // Test for A's keys different from B. - for (let idx = 0; idx < keysA.length; idx++) { - const key = keysA[idx]; - - if (!bHasOwnProperty(key)) { - return false; - } - - const valueA = objA[key]; - const valueB = objB[key]; - - ret = compare ? compare.call(compareContext, valueA, valueB, key) : void 0; - - if (ret === false || (ret === void 0 && valueA !== valueB)) { - return false; - } - } - - return true; -} - -export default function (value, other, customizer, thisArg) { - return shallowEqual(toRaw(value), toRaw(other), customizer, thisArg); -} diff --git a/components/_util/shallowequal.ts b/components/_util/shallowequal.ts new file mode 100644 index 0000000000..9a09cc9b6e --- /dev/null +++ b/components/_util/shallowequal.ts @@ -0,0 +1,50 @@ +import { toRaw } from 'vue'; + +function shallowEqual(objA: any, objB: any, compare?: any, compareContext?: any) { + let ret = compare ? compare.call(compareContext, objA, objB) : void 0; + + if (ret !== void 0) { + return !!ret; + } + + if (objA === objB) { + return true; + } + + if (typeof objA !== 'object' || !objA || typeof objB !== 'object' || !objB) { + return false; + } + + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); + + if (keysA.length !== keysB.length) { + return false; + } + + const bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB); + + // Test for A's keys different from B. + for (let idx = 0; idx < keysA.length; idx++) { + const key = keysA[idx]; + + if (!bHasOwnProperty(key)) { + return false; + } + + const valueA = objA[key]; + const valueB = objB[key]; + + ret = compare ? compare.call(compareContext, valueA, valueB, key) : void 0; + + if (ret === false || (ret === void 0 && valueA !== valueB)) { + return false; + } + } + + return true; +} + +export default function (value: any, other: any) { + return shallowEqual(toRaw(value), toRaw(other)); +} diff --git a/components/_util/statusUtils.tsx b/components/_util/statusUtils.tsx new file mode 100644 index 0000000000..a9cb68aaad --- /dev/null +++ b/components/_util/statusUtils.tsx @@ -0,0 +1,23 @@ +import type { ValidateStatus } from '../form/FormItem'; +import classNames from './classNames'; + +const InputStatuses = ['warning', 'error', ''] as const; + +export type InputStatus = (typeof InputStatuses)[number]; + +export function getStatusClassNames( + prefixCls: string, + status?: ValidateStatus, + hasFeedback?: boolean, +) { + return classNames({ + [`${prefixCls}-status-success`]: status === 'success', + [`${prefixCls}-status-warning`]: status === 'warning', + [`${prefixCls}-status-error`]: status === 'error', + [`${prefixCls}-status-validating`]: status === 'validating', + [`${prefixCls}-has-feedback`]: hasFeedback, + }); +} + +export const getMergedStatus = (contextStatus?: ValidateStatus, customStatus?: InputStatus) => + customStatus || contextStatus; diff --git a/components/_util/switchScrollingEffect.ts b/components/_util/switchScrollingEffect.ts deleted file mode 100644 index e87985bdda..0000000000 --- a/components/_util/switchScrollingEffect.ts +++ /dev/null @@ -1,42 +0,0 @@ -import getScrollBarSize from './getScrollBarSize'; -import setStyle from './setStyle'; - -function isBodyOverflowing() { - return ( - document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight) && - window.innerWidth > document.body.offsetWidth - ); -} - -let cacheStyle = {}; - -export default (close?: boolean) => { - if (!isBodyOverflowing() && !close) { - return; - } - - // https://github.com/ant-design/ant-design/issues/19729 - const scrollingEffectClassName = 'ant-scrolling-effect'; - const scrollingEffectClassNameReg = new RegExp(`${scrollingEffectClassName}`, 'g'); - const bodyClassName = document.body.className; - - if (close) { - if (!scrollingEffectClassNameReg.test(bodyClassName)) return; - setStyle(cacheStyle); - cacheStyle = {}; - document.body.className = bodyClassName.replace(scrollingEffectClassNameReg, '').trim(); - return; - } - - const scrollBarSize = getScrollBarSize(); - if (scrollBarSize) { - cacheStyle = setStyle({ - position: 'relative', - width: `calc(100% - ${scrollBarSize}px)`, - }); - if (!scrollingEffectClassNameReg.test(bodyClassName)) { - const addClassName = `${bodyClassName} ${scrollingEffectClassName}`; - document.body.className = addClassName.trim(); - } - } -}; diff --git a/components/_util/throttleByAnimationFrame.ts b/components/_util/throttleByAnimationFrame.ts index dd84132a4f..ce469a3f0a 100644 --- a/components/_util/throttleByAnimationFrame.ts +++ b/components/_util/throttleByAnimationFrame.ts @@ -1,47 +1,29 @@ import raf from './raf'; -export default function throttleByAnimationFrame(fn: (...args: any[]) => void) { - let requestId: number; +type throttledFn = (...args: any[]) => void; - const later = (args: any[]) => () => { +type throttledCancelFn = { cancel: () => void }; + +function throttleByAnimationFrame(fn: (...args: T) => void) { + let requestId: number | null; + + const later = (args: T) => () => { requestId = null; fn(...args); }; - const throttled = (...args: any[]) => { + const throttled: throttledFn & throttledCancelFn = (...args: T) => { if (requestId == null) { requestId = raf(later(args)); } }; - (throttled as any).cancel = () => raf.cancel(requestId!); + throttled.cancel = () => { + raf.cancel(requestId!); + requestId = null; + }; return throttled; } -export function throttleByAnimationFrameDecorator() { - // eslint-disable-next-line func-names - return function (target: any, key: string, descriptor: any) { - const fn = descriptor.value; - let definingProperty = false; - return { - configurable: true, - get() { - // eslint-disable-next-line no-prototype-builtins - if (definingProperty || this === target.prototype || this.hasOwnProperty(key)) { - return fn; - } - - const boundFn = throttleByAnimationFrame(fn.bind(this)); - definingProperty = true; - Object.defineProperty(this, key, { - value: boundFn, - configurable: true, - writable: true, - }); - definingProperty = false; - return boundFn; - }, - }; - }; -} +export default throttleByAnimationFrame; diff --git a/components/_util/transButton.tsx b/components/_util/transButton.tsx index caced47cda..548948d553 100644 --- a/components/_util/transButton.tsx +++ b/components/_util/transButton.tsx @@ -1,5 +1,5 @@ import type { CSSProperties } from 'vue'; -import { defineComponent, ref, onMounted } from 'vue'; +import { defineComponent, shallowRef, onMounted } from 'vue'; /** * Wrap of sub component which need use as Button capacity (like Icon component). * This helps accessibility reader to tread as a interactive button to operation. @@ -25,7 +25,7 @@ const TransButton = defineComponent({ autofocus: { type: Boolean, default: undefined }, }, setup(props, { slots, emit, attrs, expose }) { - const domRef = ref(); + const domRef = shallowRef(); const onKeyDown = (event: KeyboardEvent) => { const { keyCode } = event; if (keyCode === KeyCode.ENTER) { diff --git a/components/_util/transKeys.ts b/components/_util/transKeys.ts new file mode 100644 index 0000000000..d196b11373 --- /dev/null +++ b/components/_util/transKeys.ts @@ -0,0 +1,17 @@ +export const groupKeysMap = (keys: string[]) => { + const map = new Map(); + keys.forEach((key, index) => { + map.set(key, index); + }); + return map; +}; + +export const groupDisabledKeysMap = (dataSource: RecordType) => { + const map = new Map(); + dataSource.forEach(({ disabled, key }, index) => { + if (disabled) { + map.set(key, index); + } + }); + return map; +}; diff --git a/components/_util/transition.tsx b/components/_util/transition.tsx index f0fe0eb183..fd6ebc4904 100644 --- a/components/_util/transition.tsx +++ b/components/_util/transition.tsx @@ -9,7 +9,7 @@ import { nextTick, Transition, TransitionGroup } from 'vue'; import { tuple } from './type'; const SelectPlacements = tuple('bottomLeft', 'bottomRight', 'topLeft', 'topRight'); -export type SelectCommonPlacement = typeof SelectPlacements[number]; +export type SelectCommonPlacement = (typeof SelectPlacements)[number]; const getTransitionDirection = (placement: SelectCommonPlacement | undefined) => { if (placement !== undefined && (placement === 'topLeft' || placement === 'topRight')) { @@ -27,7 +27,7 @@ export const getTransitionProps = (transitionName: string, opt: TransitionProps // appearFromClass: `${transitionName}-appear ${transitionName}-appear-prepare`, // appearActiveClass: `antdv-base-transtion`, // appearToClass: `${transitionName}-appear ${transitionName}-appear-active`, - enterFromClass: `${transitionName}-enter ${transitionName}-enter-prepare`, + enterFromClass: `${transitionName}-enter ${transitionName}-enter-prepare ${transitionName}-enter-start`, enterActiveClass: `${transitionName}-enter ${transitionName}-enter-prepare`, enterToClass: `${transitionName}-enter ${transitionName}-enter-active`, leaveFromClass: ` ${transitionName}-leave`, diff --git a/components/_util/type.ts b/components/_util/type.ts index 20c7e30c32..bc1cb530e1 100644 --- a/components/_util/type.ts +++ b/components/_util/type.ts @@ -15,7 +15,7 @@ export type ElementOf = T extends (infer E)[] ? E : T extends readonly (infer /** * https://github.com/Microsoft/TypeScript/issues/29729 */ -export type LiteralUnion = T | (U & {}); +export type LiteralUnion = T | (string & {}); export type Data = Record; @@ -44,4 +44,49 @@ export const withInstall = (comp: T) => { export type MaybeRef = T | Ref; +export function eventType() { + return { type: [Function, Array] as PropType }; +} + +export function objectType(defaultVal?: T) { + return { type: Object as PropType, default: defaultVal as T }; +} + +export function booleanType(defaultVal?: boolean) { + return { type: Boolean, default: defaultVal as boolean }; +} + +export function functionType {}>(defaultVal?: T) { + return { type: Function as PropType, default: defaultVal as T }; +} + +export function anyType(defaultVal?: T, required?: boolean) { + const type = { validator: () => true, default: defaultVal as T } as unknown; + return required + ? (type as { + type: PropType; + default: T; + required: true; + }) + : (type as { + default: T; + type: PropType; + }); +} +export function vNodeType() { + return { validator: () => true } as unknown as { type: PropType }; +} + +export function arrayType(defaultVal?: T) { + return { type: Array as unknown as PropType, default: defaultVal as T }; +} + +export function stringType(defaultVal?: T) { + return { type: String as unknown as PropType, default: defaultVal as T }; +} + +export function someType(types?: any[], defaultVal?: T) { + return types ? { type: types as PropType, default: defaultVal as T } : anyType(defaultVal); +} + export type CustomSlotsType = SlotsType; diff --git a/components/_util/util.ts b/components/_util/util.ts index 1adfa26efd..aff3788321 100644 --- a/components/_util/util.ts +++ b/components/_util/util.ts @@ -56,7 +56,7 @@ function resolvePropValue(options, props, key, value) { export function getDataAndAriaProps(props) { return Object.keys(props).reduce((memo, key) => { - if (key.substr(0, 5) === 'data-' || key.substr(0, 5) === 'aria-') { + if (key.startsWith('data-') || key.startsWith('aria-')) { memo[key] = props[key]; } return memo; @@ -78,5 +78,24 @@ export function renderHelper>( } return v ?? defaultV; } +export function wrapPromiseFn(openFn: (resolve: VoidFunction) => VoidFunction) { + let closeFn: VoidFunction; + + const closePromise = new Promise(resolve => { + closeFn = openFn(() => { + resolve(true); + }); + }); + + const result: any = () => { + closeFn?.(); + }; + + result.then = (filled: VoidFunction, rejected: VoidFunction) => + closePromise.then(filled, rejected); + result.promise = closePromise; + + return result; +} export { isOn, cacheStringFunction, camelize, hyphenate, capitalize, resolvePropValue }; diff --git a/components/_util/warning.js b/components/_util/warning.js deleted file mode 100644 index 59681e40a4..0000000000 --- a/components/_util/warning.js +++ /dev/null @@ -1,7 +0,0 @@ -import warning, { resetWarned } from '../vc-util/warning'; - -export { resetWarned }; - -export default (valid, component, message = '') => { - warning(valid, `[antdv: ${component}] ${message}`); -}; diff --git a/components/_util/warning.ts b/components/_util/warning.ts new file mode 100644 index 0000000000..b4819faa22 --- /dev/null +++ b/components/_util/warning.ts @@ -0,0 +1,21 @@ +import vcWarning, { resetWarned } from '../vc-util/warning'; + +export { resetWarned }; +export function noop() {} + +type Warning = (valid: boolean, component: string, message?: string) => void; + +// eslint-disable-next-line import/no-mutable-exports +let warning: Warning = noop; +if (process.env.NODE_ENV !== 'production') { + warning = (valid, component, message) => { + vcWarning(valid, `[ant-design-vue: ${component}] ${message}`); + + // StrictMode will inject console which will not throw warning in React 17. + if (process.env.NODE_ENV === 'test') { + resetWarned(); + } + }; +} + +export default warning; diff --git a/components/_util/wave.tsx b/components/_util/wave.tsx deleted file mode 100644 index ef69ef66cd..0000000000 --- a/components/_util/wave.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { nextTick, defineComponent, getCurrentInstance, onMounted, onBeforeUnmount } from 'vue'; -import TransitionEvents from './css-animation/Event'; -import raf from './raf'; -import { findDOMNode } from './props-util'; -import useConfigInject from './hooks/useConfigInject'; -let styleForPesudo: HTMLStyleElement; - -// Where el is the DOM element you'd like to test for visibility -function isHidden(element: HTMLElement) { - if (process.env.NODE_ENV === 'test') { - return false; - } - return !element || element.offsetParent === null; -} -function isNotGrey(color: string) { - // eslint-disable-next-line no-useless-escape - const match = (color || '').match(/rgba?\((\d*), (\d*), (\d*)(, [\.\d]*)?\)/); - if (match && match[1] && match[2] && match[3]) { - return !(match[1] === match[2] && match[2] === match[3]); - } - return true; -} -export default defineComponent({ - compatConfig: { MODE: 3 }, - name: 'Wave', - props: { - insertExtraNode: Boolean, - disabled: Boolean, - }, - setup(props, { slots, expose }) { - const instance = getCurrentInstance(); - const { csp, prefixCls } = useConfigInject('', props); - expose({ - csp, - }); - let eventIns = null; - let clickWaveTimeoutId = null; - let animationStartId = null; - let animationStart = false; - let extraNode = null; - let isUnmounted = false; - const onTransitionStart = e => { - if (isUnmounted) return; - - const node = findDOMNode(instance); - if (!e || e.target !== node) { - return; - } - - if (!animationStart) { - resetEffect(node); - } - }; - const onTransitionEnd = (e: any) => { - if (!e || e.animationName !== 'fadeEffect') { - return; - } - resetEffect(e.target); - }; - const getAttributeName = () => { - const { insertExtraNode } = props; - return insertExtraNode - ? `${prefixCls.value}-click-animating` - : `${prefixCls.value}-click-animating-without-extra-node`; - }; - const onClick = (node: HTMLElement, waveColor: string) => { - const { insertExtraNode, disabled } = props; - if (disabled || !node || isHidden(node) || node.className.indexOf('-leave') >= 0) { - return; - } - - extraNode = document.createElement('div'); - extraNode.className = `${prefixCls.value}-click-animating-node`; - const attributeName = getAttributeName(); - node.removeAttribute(attributeName); - node.setAttribute(attributeName, 'true'); - // Not white or transparent or grey - styleForPesudo = styleForPesudo || document.createElement('style'); - if ( - waveColor && - waveColor !== '#ffffff' && - waveColor !== 'rgb(255, 255, 255)' && - isNotGrey(waveColor) && - !/rgba\(\d*, \d*, \d*, 0\)/.test(waveColor) && // any transparent rgba color - waveColor !== 'transparent' - ) { - // Add nonce if CSP exist - if (csp.value?.nonce) { - styleForPesudo.nonce = csp.value.nonce; - } - extraNode.style.borderColor = waveColor; - styleForPesudo.innerHTML = ` - [${prefixCls.value}-click-animating-without-extra-node='true']::after, .${prefixCls.value}-click-animating-node { - --antd-wave-shadow-color: ${waveColor}; - }`; - if (!document.body.contains(styleForPesudo)) { - document.body.appendChild(styleForPesudo); - } - } - if (insertExtraNode) { - node.appendChild(extraNode); - } - TransitionEvents.addStartEventListener(node, onTransitionStart); - TransitionEvents.addEndEventListener(node, onTransitionEnd); - }; - const resetEffect = (node: HTMLElement) => { - if (!node || node === extraNode || !(node instanceof Element)) { - return; - } - const { insertExtraNode } = props; - const attributeName = getAttributeName(); - node.setAttribute(attributeName, 'false'); // edge has bug on `removeAttribute` #14466 - if (styleForPesudo) { - styleForPesudo.innerHTML = ''; - } - if (insertExtraNode && extraNode && node.contains(extraNode)) { - node.removeChild(extraNode); - } - TransitionEvents.removeStartEventListener(node, onTransitionStart); - TransitionEvents.removeEndEventListener(node, onTransitionEnd); - }; - const bindAnimationEvent = (node: HTMLElement) => { - if ( - !node || - !node.getAttribute || - node.getAttribute('disabled') || - node.className.indexOf('disabled') >= 0 - ) { - return; - } - const newClick = (e: MouseEvent) => { - // Fix radio button click twice - if ((e.target as any).tagName === 'INPUT' || isHidden(e.target as HTMLElement)) { - return; - } - resetEffect(node); - // Get wave color from target - const waveColor = - getComputedStyle(node).getPropertyValue('border-top-color') || // Firefox Compatible - getComputedStyle(node).getPropertyValue('border-color') || - getComputedStyle(node).getPropertyValue('background-color'); - clickWaveTimeoutId = setTimeout(() => onClick(node, waveColor), 0); - raf.cancel(animationStartId); - animationStart = true; - - // Render to trigger transition event cost 3 frames. Let's delay 10 frames to reset this. - animationStartId = raf(() => { - animationStart = false; - }, 10); - }; - node.addEventListener('click', newClick, true); - return { - cancel: () => { - node.removeEventListener('click', newClick, true); - }, - }; - }; - onMounted(() => { - nextTick(() => { - const node = findDOMNode(instance); - if (node.nodeType !== 1) { - return; - } - eventIns = bindAnimationEvent(node); - }); - }); - onBeforeUnmount(() => { - if (eventIns) { - eventIns.cancel(); - } - clearTimeout(clickWaveTimeoutId); - isUnmounted = true; - }); - return () => { - return slots.default?.()[0]; - }; - }, -}); diff --git a/components/_util/wave/WaveEffect.tsx b/components/_util/wave/WaveEffect.tsx new file mode 100644 index 0000000000..1f3ce23d17 --- /dev/null +++ b/components/_util/wave/WaveEffect.tsx @@ -0,0 +1,164 @@ +import type { CSSProperties } from 'vue'; +import { onBeforeUnmount, onMounted, Transition, render, defineComponent, shallowRef } from 'vue'; +import useState from '../hooks/useState'; +import { objectType } from '../type'; +import { getTargetWaveColor } from './util'; +import wrapperRaf from '../raf'; +function validateNum(value: number) { + return Number.isNaN(value) ? 0 : value; +} + +export interface WaveEffectProps { + className: string; + target: HTMLElement; +} + +const WaveEffect = defineComponent({ + props: { + target: objectType(), + className: String, + }, + setup(props) { + const divRef = shallowRef(null); + + const [color, setWaveColor] = useState(null); + const [borderRadius, setBorderRadius] = useState([]); + const [left, setLeft] = useState(0); + const [top, setTop] = useState(0); + const [width, setWidth] = useState(0); + const [height, setHeight] = useState(0); + const [enabled, setEnabled] = useState(false); + + function syncPos() { + const { target } = props; + const nodeStyle = getComputedStyle(target); + + // Get wave color from target + setWaveColor(getTargetWaveColor(target)); + + const isStatic = nodeStyle.position === 'static'; + + // Rect + const { borderLeftWidth, borderTopWidth } = nodeStyle; + setLeft(isStatic ? target.offsetLeft : validateNum(-parseFloat(borderLeftWidth))); + setTop(isStatic ? target.offsetTop : validateNum(-parseFloat(borderTopWidth))); + setWidth(target.offsetWidth); + setHeight(target.offsetHeight); + + // Get border radius + const { + borderTopLeftRadius, + borderTopRightRadius, + borderBottomLeftRadius, + borderBottomRightRadius, + } = nodeStyle; + + setBorderRadius( + [ + borderTopLeftRadius, + borderTopRightRadius, + borderBottomRightRadius, + borderBottomLeftRadius, + ].map(radius => validateNum(parseFloat(radius))), + ); + } + // Add resize observer to follow size + let resizeObserver: ResizeObserver; + let rafId: number; + let timeoutId: any; + const clear = () => { + clearTimeout(timeoutId); + wrapperRaf.cancel(rafId); + resizeObserver?.disconnect(); + }; + const removeDom = () => { + const holder = divRef.value?.parentElement; + if (holder) { + render(null, holder); + if (holder.parentElement) { + holder.parentElement.removeChild(holder); + } + } + }; + + onMounted(() => { + clear(); + timeoutId = setTimeout(() => { + removeDom(); + }, 5000); + const { target } = props; + if (target) { + // We need delay to check position here + // since UI may change after click + rafId = wrapperRaf(() => { + syncPos(); + + setEnabled(true); + }); + + if (typeof ResizeObserver !== 'undefined') { + resizeObserver = new ResizeObserver(syncPos); + + resizeObserver.observe(target); + } + } + }); + onBeforeUnmount(() => { + clear(); + }); + + const onTransitionend = (e: TransitionEvent) => { + if (e.propertyName === 'opacity') { + removeDom(); + } + }; + return () => { + if (!enabled.value) { + return null; + } + const waveStyle = { + left: `${left.value}px`, + top: `${top.value}px`, + width: `${width.value}px`, + height: `${height.value}px`, + borderRadius: borderRadius.value.map(radius => `${radius}px`).join(' '), + } as CSSProperties & { + [name: string]: number | string; + }; + + if (color) { + waveStyle['--wave-color'] = color.value as string; + } + + return ( + +
+ + ); + }; + }, +}); + +function showWaveEffect(node: HTMLElement, className: string) { + // Create holder + const holder = document.createElement('div'); + holder.style.position = 'absolute'; + holder.style.left = `0px`; + holder.style.top = `0px`; + node?.insertBefore(holder, node?.firstChild); + + render(, holder); +} + +export default showWaveEffect; diff --git a/components/_util/wave/index.tsx b/components/_util/wave/index.tsx new file mode 100644 index 0000000000..d816e426a5 --- /dev/null +++ b/components/_util/wave/index.tsx @@ -0,0 +1,96 @@ +import { + computed, + defineComponent, + getCurrentInstance, + nextTick, + onBeforeUnmount, + onMounted, + watch, +} from 'vue'; +import useConfigInject from '../../config-provider/hooks/useConfigInject'; +import isVisible from '../../vc-util/Dom/isVisible'; +import classNames from '../classNames'; +import { findDOMNode } from '../props-util'; +import useStyle from './style'; +import useWave from './useWave'; + +export interface WaveProps { + disabled?: boolean; +} + +export default defineComponent({ + compatConfig: { MODE: 3 }, + name: 'Wave', + props: { + disabled: Boolean, + }, + setup(props, { slots }) { + const instance = getCurrentInstance(); + const { prefixCls } = useConfigInject('wave', props); + + // ============================== Style =============================== + const [, hashId] = useStyle(prefixCls); + + // =============================== Wave =============================== + const showWave = useWave( + instance, + computed(() => classNames(prefixCls.value, hashId.value)), + ); + let onClick: (e: MouseEvent) => void; + const clear = () => { + const node = findDOMNode(instance); + node.removeEventListener('click', onClick, true); + }; + + onMounted(() => { + watch( + () => props.disabled, + () => { + clear(); + nextTick(() => { + const node = findDOMNode(instance); + + if (!node || node.nodeType !== 1 || props.disabled) { + return; + } + + // Click handler + const onClick = (e: MouseEvent) => { + // Fix radio button click twice + if ( + (e.target as HTMLElement).tagName === 'INPUT' || + !isVisible(e.target as HTMLElement) || + // No need wave + !node.getAttribute || + node.getAttribute('disabled') || + (node as HTMLInputElement).disabled || + node.className.includes('disabled') || + node.className.includes('-leave') + ) { + return; + } + + showWave(); + }; + + // Bind events + node.addEventListener('click', onClick, true); + }); + }, + { + immediate: true, + flush: 'post', + }, + ); + }); + onBeforeUnmount(() => { + clear(); + }); + + return () => { + // ============================== Render ============================== + const children = slots.default?.()[0]; + return children; + }; + }, +}); diff --git a/components/_util/wave/style.ts b/components/_util/wave/style.ts new file mode 100644 index 0000000000..63f75a5577 --- /dev/null +++ b/components/_util/wave/style.ts @@ -0,0 +1,38 @@ +import { genComponentStyleHook } from '../../theme/internal'; +import type { FullToken, GenerateStyle } from '../../theme/internal'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ComponentToken {} + +export type WaveToken = FullToken<'Wave'>; + +const genWaveStyle: GenerateStyle = token => { + const { componentCls, colorPrimary } = token; + return { + [componentCls]: { + position: 'absolute', + background: 'transparent', + pointerEvents: 'none', + boxSizing: 'border-box', + color: `var(--wave-color, ${colorPrimary})`, + + boxShadow: `0 0 0 0 currentcolor`, + opacity: 0.2, + + // =================== Motion =================== + '&.wave-motion-appear': { + transition: [ + `box-shadow 0.4s ${token.motionEaseOutCirc}`, + `opacity 2s ${token.motionEaseOutCirc}`, + ].join(','), + + '&-active': { + boxShadow: `0 0 0 6px currentcolor`, + opacity: 0, + }, + }, + }, + }; +}; + +export default genComponentStyleHook('Wave', token => [genWaveStyle(token)]); diff --git a/components/_util/wave/useWave.ts b/components/_util/wave/useWave.ts new file mode 100644 index 0000000000..7a2c23188b --- /dev/null +++ b/components/_util/wave/useWave.ts @@ -0,0 +1,16 @@ +import type { ComponentInternalInstance, Ref } from 'vue'; +import { findDOMNode } from '../props-util'; +import showWaveEffect from './WaveEffect'; + +export default function useWave( + instance: ComponentInternalInstance | null, + className: Ref, +): VoidFunction { + function showWave() { + const node = findDOMNode(instance); + + showWaveEffect(node, className.value); + } + + return showWave; +} diff --git a/components/_util/wave/util.ts b/components/_util/wave/util.ts new file mode 100644 index 0000000000..cd5bf63770 --- /dev/null +++ b/components/_util/wave/util.ts @@ -0,0 +1,35 @@ +export function isNotGrey(color: string) { + // eslint-disable-next-line no-useless-escape + const match = (color || '').match(/rgba?\((\d*), (\d*), (\d*)(, [\d.]*)?\)/); + if (match && match[1] && match[2] && match[3]) { + return !(match[1] === match[2] && match[2] === match[3]); + } + return true; +} + +export function isValidWaveColor(color: string) { + return ( + color && + color !== '#fff' && + color !== '#ffffff' && + color !== 'rgb(255, 255, 255)' && + color !== 'rgba(255, 255, 255, 1)' && + isNotGrey(color) && + !/rgba\((?:\d*, ){3}0\)/.test(color) && // any transparent rgba color + color !== 'transparent' + ); +} + +export function getTargetWaveColor(node: HTMLElement) { + const { borderTopColor, borderColor, backgroundColor } = getComputedStyle(node); + if (isValidWaveColor(borderTopColor)) { + return borderTopColor; + } + if (isValidWaveColor(borderColor)) { + return borderColor; + } + if (isValidWaveColor(backgroundColor)) { + return backgroundColor; + } + return null; +} diff --git a/components/affix/__tests__/__snapshots__/demo.test.js.snap b/components/affix/__tests__/__snapshots__/demo.test.js.snap index 65a2e4b0d2..6086561877 100644 --- a/components/affix/__tests__/__snapshots__/demo.test.js.snap +++ b/components/affix/__tests__/__snapshots__/demo.test.js.snap @@ -2,12 +2,14 @@ exports[`renders ./components/affix/demo/basic.vue correctly 1`] = `
+

+
@@ -16,7 +18,8 @@ exports[`renders ./components/affix/demo/basic.vue correctly 1`] = ` exports[`renders ./components/affix/demo/on-change.vue correctly 1`] = `
-
@@ -26,6 +29,7 @@ exports[`renders ./components/affix/demo/target.vue correctly 1`] = `
+
diff --git a/components/affix/demo/basic.vue b/components/affix/demo/basic.vue index a1baee459d..cab12bdd33 100644 --- a/components/affix/demo/basic.vue +++ b/components/affix/demo/basic.vue @@ -26,16 +26,8 @@ The simplest usage. - diff --git a/components/affix/demo/on-change.vue b/components/affix/demo/on-change.vue index ab9df60764..2bd905c446 100644 --- a/components/affix/demo/on-change.vue +++ b/components/affix/demo/on-change.vue @@ -21,17 +21,8 @@ Callback with affixed state. 120px to affix top - diff --git a/components/affix/demo/target.vue b/components/affix/demo/target.vue index 8256c26b1c..852d46f28e 100644 --- a/components/affix/demo/target.vue +++ b/components/affix/demo/target.vue @@ -25,23 +25,16 @@ Set a `target` for 'Affix', which is listen to scroll event of target element (d
- - diff --git a/components/back-top/demo/index.vue b/components/back-top/demo/index.vue deleted file mode 100644 index d966a0d884..0000000000 --- a/components/back-top/demo/index.vue +++ /dev/null @@ -1,27 +0,0 @@ - - - diff --git a/components/back-top/index.en-US.md b/components/back-top/index.en-US.md deleted file mode 100644 index 1fa126df26..0000000000 --- a/components/back-top/index.en-US.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -category: Components -type: Other -title: BackTop -cover: https://gw.alipayobjects.com/zos/alicdn/tJZ5jbTwX/BackTop.svg ---- - -`BackTop` makes it easy to go back to the top of the page. - -## When To Use - -- When the page content is very long. -- When you need to go back to the top very frequently in order to view the contents. - -## API - -> The distance to the bottom is set to `50px` by default, which is overridable. -> -> If you decide to use custom styles, please note the size limit: no more than `40px * 40px`. - -| Property | Description | Type | Default | Version | -| --- | --- | --- | --- | --- | -| target | specifies the scrollable area dom node | () => HTMLElement | () => window | | -| visibilityHeight | the `BackTop` button will not show until the scroll height reaches this value | number | 400 | | - -### events - -| Events Name | Description | Arguments | Version | -| --- | --- | --- | --- | -| click | a callback function, which can be executed when you click the button | Function | | diff --git a/components/back-top/index.tsx b/components/back-top/index.tsx deleted file mode 100644 index fd00085c3a..0000000000 --- a/components/back-top/index.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import type { ExtractPropTypes, PropType } from 'vue'; -import { - defineComponent, - nextTick, - onActivated, - onBeforeUnmount, - onMounted, - reactive, - ref, - watch, - onDeactivated, -} from 'vue'; -import VerticalAlignTopOutlined from '@ant-design/icons-vue/VerticalAlignTopOutlined'; -import addEventListener from '../vc-util/Dom/addEventListener'; -import getScroll from '../_util/getScroll'; -import { getTransitionProps, Transition } from '../_util/transition'; -import scrollTo from '../_util/scrollTo'; -import { withInstall } from '../_util/type'; -import throttleByAnimationFrame from '../_util/throttleByAnimationFrame'; -import useConfigInject from '../_util/hooks/useConfigInject'; -import type { MouseEventHandler } from '../_util/EventInterface'; - -export const backTopProps = () => ({ - visibilityHeight: { type: Number, default: 400 }, - duration: { type: Number, default: 450 }, - target: Function as PropType<() => HTMLElement | Window | Document>, - prefixCls: String, - onClick: Function as PropType, - // visible: { type: Boolean, default: undefined }, // Only for test. Don't use it. -}); - -export type BackTopProps = Partial>; - -const BackTop = defineComponent({ - compatConfig: { MODE: 3 }, - name: 'ABackTop', - inheritAttrs: false, - props: backTopProps(), - // emits: ['click'], - setup(props, { slots, attrs, emit }) { - const { prefixCls, direction } = useConfigInject('back-top', props); - const domRef = ref(); - const state = reactive({ - visible: false, - scrollEvent: null, - }); - - const getDefaultTarget = () => - domRef.value && domRef.value.ownerDocument ? domRef.value.ownerDocument : window; - - const scrollToTop = (e: Event) => { - const { target = getDefaultTarget, duration } = props; - scrollTo(0, { - getContainer: target, - duration, - }); - emit('click', e); - }; - - const handleScroll = throttleByAnimationFrame((e: Event | { target: any }) => { - const { visibilityHeight } = props; - const scrollTop = getScroll(e.target, true); - state.visible = scrollTop > visibilityHeight; - }); - - const bindScrollEvent = () => { - const { target } = props; - const getTarget = target || getDefaultTarget; - const container = getTarget(); - state.scrollEvent = addEventListener(container, 'scroll', (e: Event) => { - handleScroll(e); - }); - handleScroll({ - target: container, - }); - }; - - const scrollRemove = () => { - if (state.scrollEvent) { - state.scrollEvent.remove(); - } - (handleScroll as any).cancel(); - }; - - watch( - () => props.target, - () => { - scrollRemove(); - nextTick(() => { - bindScrollEvent(); - }); - }, - ); - - onMounted(() => { - nextTick(() => { - bindScrollEvent(); - }); - }); - - onActivated(() => { - nextTick(() => { - bindScrollEvent(); - }); - }); - - onDeactivated(() => { - scrollRemove(); - }); - - onBeforeUnmount(() => { - scrollRemove(); - }); - - return () => { - const defaultElement = ( -
-
- -
-
- ); - const divProps = { - ...attrs, - onClick: scrollToTop, - class: { - [`${prefixCls.value}`]: true, - [`${attrs.class}`]: attrs.class, - [`${prefixCls.value}-rtl`]: direction.value === 'rtl', - }, - }; - - const transitionProps = getTransitionProps('fade'); - return ( - -
- {slots.default?.() || defaultElement} -
-
- ); - }; - }, -}); - -export default withInstall(BackTop); diff --git a/components/back-top/index.zh-CN.md b/components/back-top/index.zh-CN.md deleted file mode 100644 index 743e546fc3..0000000000 --- a/components/back-top/index.zh-CN.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -category: Components -type: 其他 -subtitle: 回到顶部 -title: BackTop -cover: https://gw.alipayobjects.com/zos/alicdn/tJZ5jbTwX/BackTop.svg ---- - -返回页面顶部的操作按钮。 - -## 何时使用 - -- 当页面内容区域比较长时; -- 当用户需要频繁返回顶部查看相关内容时。 - -## API - -> 有默认样式,距离底部 `50px`,可覆盖。 -> -> 自定义样式宽高不大于 40px \* 40px。 - -| 参数 | 说明 | 类型 | 默认值 | 版本 | -| --- | --- | --- | --- | --- | -| target | 设置需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 | Function | () => window | | -| visibilityHeight | 滚动高度达到此参数值才出现 `BackTop` | number | 400 | | - -### 事件 - -| 事件名称 | 说明 | 回调参数 | 版本 | -| -------- | ------------------ | -------- | ---- | -| click | 点击按钮的回调函数 | Function | | diff --git a/components/back-top/style/index.less b/components/back-top/style/index.less deleted file mode 100644 index 60a3da5759..0000000000 --- a/components/back-top/style/index.less +++ /dev/null @@ -1,49 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@backtop-prefix-cls: ~'@{ant-prefix}-back-top'; - -.@{backtop-prefix-cls} { - .reset-component(); - - position: fixed; - right: 100px; - bottom: 50px; - z-index: @zindex-back-top; - width: 40px; - height: 40px; - cursor: pointer; - - &:empty { - display: none; - } - - &-rtl { - right: auto; - left: 100px; - direction: rtl; - } - - &-content { - width: 40px; - height: 40px; - overflow: hidden; - color: @back-top-color; - text-align: center; - background-color: @back-top-bg; - border-radius: 20px; - transition: all 0.3s; - - &:hover { - background-color: @back-top-hover-bg; - transition: all 0.3s; - } - } - - &-icon { - font-size: 24px; - line-height: 40px; - } -} - -@import './responsive'; diff --git a/components/back-top/style/index.tsx b/components/back-top/style/index.tsx deleted file mode 100644 index 3a3ab0de59..0000000000 --- a/components/back-top/style/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import '../../style/index.less'; -import './index.less'; diff --git a/components/back-top/style/responsive.less b/components/back-top/style/responsive.less deleted file mode 100644 index 7b21a85009..0000000000 --- a/components/back-top/style/responsive.less +++ /dev/null @@ -1,11 +0,0 @@ -@media screen and (max-width: @screen-md) { - .@{backtop-prefix-cls} { - right: 60px; - } -} - -@media screen and (max-width: @screen-xs) { - .@{backtop-prefix-cls} { - right: 20px; - } -} diff --git a/components/badge/Badge.tsx b/components/badge/Badge.tsx index d7c0f151db..e1d8a9e6df 100644 --- a/components/badge/Badge.tsx +++ b/components/badge/Badge.tsx @@ -7,15 +7,17 @@ import { getTransitionProps, Transition } from '../_util/transition'; import type { ExtractPropTypes, CSSProperties, PropType } from 'vue'; import { defineComponent, computed, ref, watch } from 'vue'; import Ribbon from './Ribbon'; -import { isPresetColor } from './utils'; -import useConfigInject from '../_util/hooks/useConfigInject'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; import isNumeric from '../_util/isNumeric'; +import useStyle from './style'; +import type { PresetColorKey } from '../theme/interface'; +import type { LiteralUnion, CustomSlotsType } from '../_util/type'; import type { PresetStatusColorType } from '../_util/colors'; -import type { CustomSlotsType } from '../_util/type'; +import { isPresetColor } from '../_util/colors'; export const badgeProps = () => ({ /** Number to show in badge */ - count: PropTypes.any, + count: PropTypes.any.def(null), showZero: { type: Boolean, default: undefined }, /** Max count to show */ overflowCount: { type: Number, default: 99 }, @@ -25,7 +27,7 @@ export const badgeProps = () => ({ scrollNumberPrefixCls: String, status: { type: String as PropType }, size: { type: String as PropType<'default' | 'small'>, default: 'default' }, - color: String, + color: String as PropType>, text: PropTypes.any, offset: Array as unknown as PropType<[number | string, number | string]>, numberStyle: { type: Object as PropType, default: undefined as CSSProperties }, @@ -47,6 +49,7 @@ export default defineComponent({ }>, setup(props, { slots, attrs }) { const { prefixCls, direction } = useConfigInject('badge', props); + const [wrapSSR, hashId] = useStyle(prefixCls); // ================================ Misc ================================ const numberedDisplayCount = computed(() => { @@ -57,15 +60,16 @@ export default defineComponent({ ) as string | number | null; }); - const hasStatus = computed( - () => - (props.status !== null && props.status !== undefined) || - (props.color !== null && props.color !== undefined), - ); - const isZero = computed( () => numberedDisplayCount.value === '0' || numberedDisplayCount.value === 0, ); + const ignoreCount = computed(() => props.count === null || (isZero.value && !props.showZero)); + const hasStatus = computed( + () => + ((props.status !== null && props.status !== undefined) || + (props.color !== null && props.color !== undefined)) && + ignoreCount.value, + ); const showAsDot = computed(() => props.dot && !isZero.value); @@ -97,17 +101,18 @@ export default defineComponent({ }, { immediate: true }, ); - + // InternalColor + const isInternalColor = computed(() => isPresetColor(props.color, false)); // Shared styles const statusCls = computed(() => ({ [`${prefixCls.value}-status-dot`]: hasStatus.value, [`${prefixCls.value}-status-${props.status}`]: !!props.status, - [`${prefixCls.value}-status-${props.color}`]: isPresetColor(props.color), + [`${prefixCls.value}-status-${props.color}`]: isInternalColor.value, })); const statusStyle = computed(() => { - if (props.color && !isPresetColor(props.color)) { - return { background: props.color }; + if (props.color && !isInternalColor.value) { + return { background: props.color, color: props.color }; } else { return {}; } @@ -120,7 +125,7 @@ export default defineComponent({ [`${prefixCls.value}-multiple-words`]: !isDotRef.value && displayCount.value && displayCount.value.toString().length > 1, [`${prefixCls.value}-status-${props.status}`]: !!props.status, - [`${prefixCls.value}-status-${props.color}`]: isPresetColor(props.color), + [`${prefixCls.value}-status-${props.color}`]: isInternalColor.value, })); return () => { @@ -184,18 +189,19 @@ export default defineComponent({ [`${pre}-rtl`]: direction.value === 'rtl', }, attrs.class, + hashId.value, ); // if (!children && hasStatus.value) { const statusTextColor = mergedStyle.color; - return ( + return wrapSSR( {text} - + , ); } @@ -203,12 +209,12 @@ export default defineComponent({ appear: false, }); let scrollNumberStyle: CSSProperties = { ...mergedStyle, ...(props.numberStyle as object) }; - if (color && !isPresetColor(color)) { + if (color && !isInternalColor.value) { scrollNumberStyle = scrollNumberStyle || {}; scrollNumberStyle.background = color; } - return ( + return wrapSSR( {children} @@ -226,7 +232,7 @@ export default defineComponent({ {statusTextNode} - + , ); }; }, diff --git a/components/badge/Ribbon.tsx b/components/badge/Ribbon.tsx index b7a0aafbc6..6725d833b2 100644 --- a/components/badge/Ribbon.tsx +++ b/components/badge/Ribbon.tsx @@ -1,14 +1,15 @@ import type { CustomSlotsType, LiteralUnion } from '../_util/type'; import type { PresetColorType } from '../_util/colors'; -import { isPresetColor } from './utils'; +import useStyle from './style'; +import { isPresetColor } from '../_util/colors'; import type { CSSProperties, PropType, ExtractPropTypes } from 'vue'; import { defineComponent, computed } from 'vue'; import PropTypes from '../_util/vue-types'; -import useConfigInject from '../_util/hooks/useConfigInject'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; export const ribbonProps = () => ({ prefix: String, - color: { type: String as PropType> }, + color: { type: String as PropType> }, text: PropTypes.any, placement: { type: String as PropType<'start' | 'end'>, default: 'end' }, }); @@ -26,7 +27,8 @@ export default defineComponent({ }>, setup(props, { attrs, slots }) { const { prefixCls, direction } = useConfigInject('ribbon', props); - const colorInPreset = computed(() => isPresetColor(props.color)); + const [wrapSSR, hashId] = useStyle(prefixCls); + const colorInPreset = computed(() => isPresetColor(props.color, false)); const ribbonCls = computed(() => [ prefixCls.value, `${prefixCls.value}-placement-${props.placement}`, @@ -43,17 +45,17 @@ export default defineComponent({ colorStyle.background = props.color; cornerColorStyle.color = props.color; } - return ( -
+ return wrapSSR( +
{slots.default?.()}
{props.text || slots.text?.()}
-
+
, ); }; }, diff --git a/components/badge/ScrollNumber.tsx b/components/badge/ScrollNumber.tsx index 37c3b2c4d8..cba1e29c8b 100644 --- a/components/badge/ScrollNumber.tsx +++ b/components/badge/ScrollNumber.tsx @@ -3,7 +3,7 @@ import PropTypes from '../_util/vue-types'; import { cloneElement } from '../_util/vnode'; import type { ExtractPropTypes, CSSProperties, DefineComponent, HTMLAttributes } from 'vue'; import { defineComponent } from 'vue'; -import useConfigInject from '../_util/hooks/useConfigInject'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; import SingleNumber from './SingleNumber'; import { filterEmpty } from '../_util/props-util'; diff --git a/components/badge/__tests__/__snapshots__/demo.test.js.snap b/components/badge/__tests__/__snapshots__/demo.test.js.snap index 7739dcaa47..b08e26cb09 100644 --- a/components/badge/__tests__/__snapshots__/demo.test.js.snap +++ b/components/badge/__tests__/__snapshots__/demo.test.js.snap @@ -12,9 +12,9 @@ exports[`renders ./components/badge/demo/basic.vue correctly 1`] = ` exports[`renders ./components/badge/demo/change.vue correctly 1`] = `

5

-
@@ -26,7 +26,7 @@ exports[`renders ./components/badge/demo/change.vue correctly 1`] = `
`; @@ -48,13 +48,13 @@ exports[`renders ./components/badge/demo/colors.vue correctly 1`] = `
lime
-#f50 +#f50
-#2db7f5 +#2db7f5
-#87d068 +#87d068
-#108ee9 +#108ee9 `; exports[`renders ./components/badge/demo/dot.vue correctly 1`] = ` @@ -89,8 +89,8 @@ exports[`renders ./components/badge/demo/overflow.vue correctly 1`] = ` `; exports[`renders ./components/badge/demo/ribbon.vue correctly 1`] = ` -
-
+
+
Pushes open the window
@@ -106,8 +106,8 @@ exports[`renders ./components/badge/demo/ribbon.vue correctly 1`] = `
-
-
+
+
Pushes open the window
@@ -123,8 +123,8 @@ exports[`renders ./components/badge/demo/ribbon.vue correctly 1`] = `
-
-
+
+
Pushes open the window
@@ -140,8 +140,8 @@ exports[`renders ./components/badge/demo/ribbon.vue correctly 1`] = `
-
-
+
+
Pushes open the window
@@ -157,8 +157,8 @@ exports[`renders ./components/badge/demo/ribbon.vue correctly 1`] = `
-
-
+
+
Pushes open the window
@@ -174,8 +174,8 @@ exports[`renders ./components/badge/demo/ribbon.vue correctly 1`] = `
-
-
+
+
Pushes open the window
@@ -191,8 +191,8 @@ exports[`renders ./components/badge/demo/ribbon.vue correctly 1`] = `
-
-
+
+
Pushes open the window
@@ -208,8 +208,8 @@ exports[`renders ./components/badge/demo/ribbon.vue correctly 1`] = `
-
-
+
+
Pushes open the window
diff --git a/components/badge/demo/basic.vue b/components/badge/demo/basic.vue index 883813466a..1dba258435 100644 --- a/components/badge/demo/basic.vue +++ b/components/badge/demo/basic.vue @@ -30,12 +30,6 @@ Simplest Usage. Badge will be hidden when `count` is `0`, but we can use `showZe - diff --git a/components/badge/demo/change.vue b/components/badge/demo/change.vue index 996b70d3fe..55c10dae2b 100644 --- a/components/badge/demo/change.vue +++ b/components/badge/demo/change.vue @@ -35,31 +35,18 @@ The count will be animated as it changes. - diff --git a/components/badge/demo/colors.vue b/components/badge/demo/colors.vue index dc6d47d71d..00bd146d4c 100644 --- a/components/badge/demo/colors.vue +++ b/components/badge/demo/colors.vue @@ -32,9 +32,7 @@ New feature after 3.16.0. We preset a series of colorful Badge styles for use in
- diff --git a/components/badge/demo/dot.vue b/components/badge/demo/dot.vue index 48998a7529..5aeae1cd91 100644 --- a/components/badge/demo/dot.vue +++ b/components/badge/demo/dot.vue @@ -24,12 +24,6 @@ If count equals 0, it won't display the dot. Link something - diff --git a/components/badge/index.en_US.md b/components/badge/index.en_US.md index 402b5eb589..778c065ce8 100644 --- a/components/badge/index.en_US.md +++ b/components/badge/index.en_US.md @@ -2,7 +2,8 @@ category: Components type: Data Display title: Badge -cover: https://gw.alipayobjects.com/zos/antfincdn/6%26GF9WHwvY/Badge.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*e0qITYqF394AAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*v8EQT7KoGbcAAAAAAAAAAAAADrJ8AQ/original --- Small numerical value or status descriptor for UI elements. diff --git a/components/badge/index.zh-CN.md b/components/badge/index.zh-CN.md index 7c4a7fd42c..89df9cc5a7 100644 --- a/components/badge/index.zh-CN.md +++ b/components/badge/index.zh-CN.md @@ -3,7 +3,8 @@ category: Components type: 数据展示 title: Badge subtitle: 徽标数 -cover: https://gw.alipayobjects.com/zos/antfincdn/6%26GF9WHwvY/Badge.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*e0qITYqF394AAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*v8EQT7KoGbcAAAAAAAAAAAAADrJ8AQ/original --- 图标右上角的圆形徽标数字。 diff --git a/components/badge/style/index.less b/components/badge/style/index.less deleted file mode 100644 index 8059ae5816..0000000000 --- a/components/badge/style/index.less +++ /dev/null @@ -1,281 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@badge-prefix-cls: ~'@{ant-prefix}-badge'; -@number-prefix-cls: ~'@{ant-prefix}-scroll-number'; - -.@{badge-prefix-cls} { - .reset-component(); - - position: relative; - display: inline-block; - line-height: 1; - - &-count { - z-index: @zindex-badge; - min-width: @badge-height; - height: @badge-height; - padding: 0 6px; - color: @badge-text-color; - font-weight: @badge-font-weight; - font-size: @badge-font-size; - line-height: @badge-height; - white-space: nowrap; - text-align: center; - background: @badge-color; - border-radius: (@badge-height / 2); - box-shadow: 0 0 0 1px @shadow-color-inverse; - - a, - a:hover { - color: @badge-text-color; - } - } - - &-count-sm { - min-width: @badge-height-sm; - height: @badge-height-sm; - padding: 0; - font-size: @badge-font-size-sm; - line-height: @badge-height-sm; - border-radius: (@badge-height-sm / 2); - } - - &-multiple-words { - padding: 0 8px; - } - - &-dot { - z-index: @zindex-badge; - width: @badge-dot-size; - min-width: @badge-dot-size; - height: @badge-dot-size; - background: @highlight-color; - border-radius: 100%; - box-shadow: 0 0 0 1px @shadow-color-inverse; - } - - // Tricky way to resolve https://github.com/ant-design/ant-design/issues/30088 - &-dot.@{number-prefix-cls} { - transition: background 1.5s; - } - - &-count, - &-dot, - .@{number-prefix-cls}-custom-component { - position: absolute; - top: 0; - right: 0; - transform: translate(50%, -50%); - transform-origin: 100% 0%; - - &.@{iconfont-css-prefix}-spin { - animation: antBadgeLoadingCircle 1s infinite linear; - } - } - - &-status { - line-height: inherit; - vertical-align: baseline; - - &-dot { - position: relative; - top: -1px; - display: inline-block; - width: @badge-status-size; - height: @badge-status-size; - vertical-align: middle; - border-radius: 50%; - } - - &-success { - background-color: @success-color; - } - - &-processing { - position: relative; - background-color: @processing-color; - - &::after { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - border: 1px solid @processing-color; - border-radius: 50%; - animation: antStatusProcessing 1.2s infinite ease-in-out; - content: ''; - } - } - - &-default { - background-color: @normal-color; - } - - &-error { - background-color: @error-color; - } - - &-warning { - background-color: @warning-color; - } - - // mixin to iterate over colors and create CSS class for each one - .make-color-classes(@i: length(@preset-colors)) when (@i > 0) { - .make-color-classes(@i - 1); - @color: extract(@preset-colors, @i); - @darkColor: '@{color}-6'; - &-@{color} { - background: @@darkColor; - } - } - .make-color-classes(); - - &-text { - margin-left: 8px; - color: @text-color; - font-size: @font-size-base; - } - } - - &-zoom-appear, - &-zoom-enter { - animation: antZoomBadgeIn @animation-duration-slow @ease-out-back; - animation-fill-mode: both; - } - - &-zoom-leave { - animation: antZoomBadgeOut @animation-duration-slow @ease-in-back; - animation-fill-mode: both; - } - - &-not-a-wrapper { - .@{badge-prefix-cls}-zoom-appear, - .@{badge-prefix-cls}-zoom-enter { - animation: antNoWrapperZoomBadgeIn @animation-duration-slow @ease-out-back; - } - - .@{badge-prefix-cls}-zoom-leave { - animation: antNoWrapperZoomBadgeOut @animation-duration-slow @ease-in-back; - } - - &:not(.@{badge-prefix-cls}-status) { - vertical-align: middle; - } - - .@{number-prefix-cls}-custom-component, - .@{badge-prefix-cls}-count { - transform: none; - } - - .@{number-prefix-cls}-custom-component, - .@{number-prefix-cls} { - position: relative; - top: auto; - display: block; - transform-origin: 50% 50%; - } - } -} - -@keyframes antStatusProcessing { - 0% { - transform: scale(0.8); - opacity: 0.5; - } - - 100% { - transform: scale(2.4); - opacity: 0; - } -} - -// Safari will blink with transform when inner element has absolute style. -.safari-fix-motion() { - /* stylelint-disable property-no-vendor-prefix */ - -webkit-transform-style: preserve-3d; - -webkit-backface-visibility: hidden; - /* stylelint-enable property-no-vendor-prefix */ -} - -.@{number-prefix-cls} { - overflow: hidden; - direction: ltr; - - &-only { - position: relative; - display: inline-block; - height: @badge-height; - transition: all @animation-duration-slow @ease-in-out; - .safari-fix-motion; - - > p.@{number-prefix-cls}-only-unit { - height: @badge-height; - margin: 0; - .safari-fix-motion; - } - } - - &-symbol { - vertical-align: top; - } -} - -@keyframes antZoomBadgeIn { - 0% { - transform: scale(0) translate(50%, -50%); - opacity: 0; - } - - 100% { - transform: scale(1) translate(50%, -50%); - } -} - -@keyframes antZoomBadgeOut { - 0% { - transform: scale(1) translate(50%, -50%); - } - - 100% { - transform: scale(0) translate(50%, -50%); - opacity: 0; - } -} - -@keyframes antNoWrapperZoomBadgeIn { - 0% { - transform: scale(0); - opacity: 0; - } - - 100% { - transform: scale(1); - } -} - -@keyframes antNoWrapperZoomBadgeOut { - 0% { - transform: scale(1); - } - - 100% { - transform: scale(0); - opacity: 0; - } -} - -@keyframes antBadgeLoadingCircle { - 0% { - transform-origin: 50%; - } - - 100% { - transform: translate(50%, -50%) rotate(360deg); - transform-origin: 50%; - } -} - -@import './ribbon'; -@import './rtl'; diff --git a/components/badge/style/index.ts b/components/badge/style/index.ts new file mode 100644 index 0000000000..1e46c60079 --- /dev/null +++ b/components/badge/style/index.ts @@ -0,0 +1,376 @@ +import type { CSSObject } from '../../_util/cssinjs'; +import { Keyframes } from '../../_util/cssinjs'; +import type { FullToken, GenerateStyle } from '../../theme/internal'; +import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import { genPresetColor, resetComponent } from '../../style'; + +interface BadgeToken extends FullToken<'Badge'> { + badgeFontHeight: number; + badgeZIndex: number | string; + badgeHeight: number; + badgeHeightSm: number; + badgeTextColor: string; + badgeFontWeight: string; + badgeFontSize: number; + badgeColor: string; + badgeColorHover: string; + badgeDotSize: number; + badgeFontSizeSm: number; + badgeStatusSize: number; + badgeShadowSize: number; + badgeShadowColor: string; + badgeProcessingDuration: string; + badgeRibbonOffset: number; + badgeRibbonCornerTransform: string; + badgeRibbonCornerFilter: string; +} + +const antStatusProcessing = new Keyframes('antStatusProcessing', { + '0%': { transform: 'scale(0.8)', opacity: 0.5 }, + '100%': { transform: 'scale(2.4)', opacity: 0 }, +}); + +const antZoomBadgeIn = new Keyframes('antZoomBadgeIn', { + '0%': { transform: 'scale(0) translate(50%, -50%)', opacity: 0 }, + '100%': { transform: 'scale(1) translate(50%, -50%)' }, +}); + +const antZoomBadgeOut = new Keyframes('antZoomBadgeOut', { + '0%': { transform: 'scale(1) translate(50%, -50%)' }, + '100%': { transform: 'scale(0) translate(50%, -50%)', opacity: 0 }, +}); + +const antNoWrapperZoomBadgeIn = new Keyframes('antNoWrapperZoomBadgeIn', { + '0%': { transform: 'scale(0)', opacity: 0 }, + '100%': { transform: 'scale(1)' }, +}); +const antNoWrapperZoomBadgeOut = new Keyframes('antNoWrapperZoomBadgeOut', { + '0%': { transform: 'scale(1)' }, + '100%': { transform: 'scale(0)', opacity: 0 }, +}); +const antBadgeLoadingCircle = new Keyframes('antBadgeLoadingCircle', { + '0%': { transformOrigin: '50%' }, + '100%': { + transform: 'translate(50%, -50%) rotate(360deg)', + transformOrigin: '50%', + }, +}); + +const genSharedBadgeStyle: GenerateStyle = (token: BadgeToken): CSSObject => { + const { + componentCls, + iconCls, + antCls, + badgeFontHeight, + badgeShadowSize, + badgeHeightSm, + motionDurationSlow, + badgeStatusSize, + marginXS, + badgeRibbonOffset, + } = token; + const numberPrefixCls = `${antCls}-scroll-number`; + const ribbonPrefixCls = `${antCls}-ribbon`; + const ribbonWrapperPrefixCls = `${antCls}-ribbon-wrapper`; + + const statusPreset = genPresetColor(token, (colorKey, { darkColor }) => ({ + [`${componentCls}-status-${colorKey}`]: { + background: darkColor, + }, + })); + + const statusRibbonPreset = genPresetColor(token, (colorKey, { darkColor }) => ({ + [`&${ribbonPrefixCls}-color-${colorKey}`]: { + background: darkColor, + color: darkColor, + }, + })); + + return { + [componentCls]: { + ...resetComponent(token), + position: 'relative', + display: 'inline-block', + width: 'fit-content', + lineHeight: 1, + + [`${componentCls}-count`]: { + zIndex: token.badgeZIndex, + minWidth: token.badgeHeight, + height: token.badgeHeight, + color: token.badgeTextColor, + fontWeight: token.badgeFontWeight, + fontSize: token.badgeFontSize, + lineHeight: `${token.badgeHeight}px`, + whiteSpace: 'nowrap', + textAlign: 'center', + background: token.badgeColor, + borderRadius: token.badgeHeight / 2, + boxShadow: `0 0 0 ${badgeShadowSize}px ${token.badgeShadowColor}`, + transition: `background ${token.motionDurationMid}`, + + a: { + color: token.badgeTextColor, + }, + 'a:hover': { + color: token.badgeTextColor, + }, + + 'a:hover &': { + background: token.badgeColorHover, + }, + }, + [`${componentCls}-count-sm`]: { + minWidth: badgeHeightSm, + height: badgeHeightSm, + fontSize: token.badgeFontSizeSm, + lineHeight: `${badgeHeightSm}px`, + borderRadius: badgeHeightSm / 2, + }, + + [`${componentCls}-multiple-words`]: { + padding: `0 ${token.paddingXS}px`, + }, + + [`${componentCls}-dot`]: { + zIndex: token.badgeZIndex, + width: token.badgeDotSize, + minWidth: token.badgeDotSize, + height: token.badgeDotSize, + background: token.badgeColor, + borderRadius: '100%', + boxShadow: `0 0 0 ${badgeShadowSize}px ${token.badgeShadowColor}`, + }, + [`${componentCls}-dot${numberPrefixCls}`]: { + transition: `background ${motionDurationSlow}`, + }, + [`${componentCls}-count, ${componentCls}-dot, ${numberPrefixCls}-custom-component`]: { + position: 'absolute', + top: 0, + insetInlineEnd: 0, + transform: 'translate(50%, -50%)', + transformOrigin: '100% 0%', + [`${iconCls}-spin`]: { + animationName: antBadgeLoadingCircle, + animationDuration: token.motionDurationMid, + animationIterationCount: 'infinite', + animationTimingFunction: 'linear', + }, + }, + [`&${componentCls}-status`]: { + lineHeight: 'inherit', + verticalAlign: 'baseline', + + [`${componentCls}-status-dot`]: { + position: 'relative', + top: -1, // Magic number, but seems better experience + display: 'inline-block', + width: badgeStatusSize, + height: badgeStatusSize, + verticalAlign: 'middle', + borderRadius: '50%', + }, + + [`${componentCls}-status-success`]: { + backgroundColor: token.colorSuccess, + }, + [`${componentCls}-status-processing`]: { + position: 'relative', + color: token.colorPrimary, + backgroundColor: token.colorPrimary, + + '&::after': { + position: 'absolute', + top: 0, + insetInlineStart: 0, + width: '100%', + height: '100%', + borderWidth: badgeShadowSize, + borderStyle: 'solid', + borderColor: 'inherit', + borderRadius: '50%', + animationName: antStatusProcessing, + animationDuration: token.badgeProcessingDuration, + animationIterationCount: 'infinite', + animationTimingFunction: 'ease-in-out', + content: '""', + }, + }, + [`${componentCls}-status-default`]: { + backgroundColor: token.colorTextPlaceholder, + }, + + [`${componentCls}-status-error`]: { + backgroundColor: token.colorError, + }, + + [`${componentCls}-status-warning`]: { + backgroundColor: token.colorWarning, + }, + ...statusPreset, + [`${componentCls}-status-text`]: { + marginInlineStart: marginXS, + color: token.colorText, + fontSize: token.fontSize, + }, + }, + [`${componentCls}-zoom-appear, ${componentCls}-zoom-enter`]: { + animationName: antZoomBadgeIn, + animationDuration: token.motionDurationSlow, + animationTimingFunction: token.motionEaseOutBack, + animationFillMode: 'both', + }, + [`${componentCls}-zoom-leave`]: { + animationName: antZoomBadgeOut, + animationDuration: token.motionDurationSlow, + animationTimingFunction: token.motionEaseOutBack, + animationFillMode: 'both', + }, + [`&${componentCls}-not-a-wrapper`]: { + [`${componentCls}-zoom-appear, ${componentCls}-zoom-enter`]: { + animationName: antNoWrapperZoomBadgeIn, + animationDuration: token.motionDurationSlow, + animationTimingFunction: token.motionEaseOutBack, + }, + + [`${componentCls}-zoom-leave`]: { + animationName: antNoWrapperZoomBadgeOut, + animationDuration: token.motionDurationSlow, + animationTimingFunction: token.motionEaseOutBack, + }, + [`&:not(${componentCls}-status)`]: { + verticalAlign: 'middle', + }, + [`${numberPrefixCls}-custom-component, ${componentCls}-count`]: { + transform: 'none', + }, + [`${numberPrefixCls}-custom-component, ${numberPrefixCls}`]: { + position: 'relative', + top: 'auto', + display: 'block', + transformOrigin: '50% 50%', + }, + }, + [`${numberPrefixCls}`]: { + overflow: 'hidden', + [`${numberPrefixCls}-only`]: { + position: 'relative', + display: 'inline-block', + height: token.badgeHeight, + transition: `all ${token.motionDurationSlow} ${token.motionEaseOutBack}`, + WebkitTransformStyle: 'preserve-3d', + WebkitBackfaceVisibility: 'hidden', + [`> p${numberPrefixCls}-only-unit`]: { + height: token.badgeHeight, + margin: 0, + WebkitTransformStyle: 'preserve-3d', + WebkitBackfaceVisibility: 'hidden', + }, + }, + [`${numberPrefixCls}-symbol`]: { verticalAlign: 'top' }, + }, + + // ====================== RTL ======================= + '&-rtl': { + direction: 'rtl', + + [`${componentCls}-count, ${componentCls}-dot, ${numberPrefixCls}-custom-component`]: { + transform: 'translate(-50%, -50%)', + }, + }, + }, + [`${ribbonWrapperPrefixCls}`]: { position: 'relative' }, + [`${ribbonPrefixCls}`]: { + ...resetComponent(token), + position: 'absolute', + top: marginXS, + height: badgeFontHeight, + padding: `0 ${token.paddingXS}px`, + color: token.colorPrimary, + lineHeight: `${badgeFontHeight}px`, + whiteSpace: 'nowrap', + backgroundColor: token.colorPrimary, + borderRadius: token.borderRadiusSM, + [`${ribbonPrefixCls}-text`]: { color: token.colorTextLightSolid }, + [`${ribbonPrefixCls}-corner`]: { + position: 'absolute', + top: '100%', + width: badgeRibbonOffset, + height: badgeRibbonOffset, + color: 'currentcolor', + border: `${badgeRibbonOffset / 2}px solid`, + transform: token.badgeRibbonCornerTransform, + transformOrigin: 'top', + filter: token.badgeRibbonCornerFilter, + }, + ...statusRibbonPreset, + [`&${ribbonPrefixCls}-placement-end`]: { + insetInlineEnd: -badgeRibbonOffset, + borderEndEndRadius: 0, + [`${ribbonPrefixCls}-corner`]: { + insetInlineEnd: 0, + borderInlineEndColor: 'transparent', + borderBlockEndColor: 'transparent', + }, + }, + [`&${ribbonPrefixCls}-placement-start`]: { + insetInlineStart: -badgeRibbonOffset, + borderEndStartRadius: 0, + [`${ribbonPrefixCls}-corner`]: { + insetInlineStart: 0, + borderBlockEndColor: 'transparent', + borderInlineStartColor: 'transparent', + }, + }, + + // ====================== RTL ======================= + '&-rtl': { + direction: 'rtl', + }, + }, + }; +}; + +// ============================== Export ============================== +export default genComponentStyleHook('Badge', token => { + const { fontSize, lineHeight, fontSizeSM, lineWidth, marginXS, colorBorderBg } = token; + + const badgeFontHeight = Math.round(fontSize * lineHeight); + const badgeShadowSize = lineWidth; + const badgeZIndex = 'auto'; + const badgeHeight = badgeFontHeight - 2 * badgeShadowSize; + const badgeTextColor = token.colorBgContainer; + const badgeFontWeight = 'normal'; + const badgeFontSize = fontSizeSM; + const badgeColor = token.colorError; + const badgeColorHover = token.colorErrorHover; + const badgeHeightSm = fontSize; + const badgeDotSize = fontSizeSM / 2; + const badgeFontSizeSm = fontSizeSM; + const badgeStatusSize = fontSizeSM / 2; + + const badgeToken = mergeToken(token, { + badgeFontHeight, + badgeShadowSize, + badgeZIndex, + badgeHeight, + badgeTextColor, + badgeFontWeight, + badgeFontSize, + badgeColor, + badgeColorHover, + badgeShadowColor: colorBorderBg, + badgeHeightSm, + badgeDotSize, + badgeFontSizeSm, + badgeStatusSize, + badgeProcessingDuration: '1.2s', + badgeRibbonOffset: marginXS, + + // Follow token just by Design. Not related with token + badgeRibbonCornerTransform: 'scaleY(0.75)', + badgeRibbonCornerFilter: `brightness(75%)`, + }); + + return [genSharedBadgeStyle(badgeToken)]; +}); diff --git a/components/badge/style/index.tsx b/components/badge/style/index.tsx deleted file mode 100644 index 3a3ab0de59..0000000000 --- a/components/badge/style/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import '../../style/index.less'; -import './index.less'; diff --git a/components/badge/style/ribbon.less b/components/badge/style/ribbon.less deleted file mode 100644 index 6a6e366bfe..0000000000 --- a/components/badge/style/ribbon.less +++ /dev/null @@ -1,81 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@ribbon-prefix-cls: ~'@{ant-prefix}-ribbon'; -@ribbon-wrapper-prefix-cls: ~'@{ant-prefix}-ribbon-wrapper'; - -.@{ribbon-wrapper-prefix-cls} { - position: relative; -} - -.@{ribbon-prefix-cls} { - .reset-component(); - - position: absolute; - top: 8px; - height: 22px; - padding: 0 8px; - color: @badge-text-color; - line-height: 22px; - white-space: nowrap; - background-color: @primary-color; - border-radius: @border-radius-sm; - - &-text { - color: @white; - } - - &-corner { - position: absolute; - top: 100%; - width: 8px; - height: 8px; - color: currentcolor; - border: 4px solid; - transform: scaleY(0.75); - transform-origin: top; - // If not support IE 11, use filter: brightness(75%) instead - &::after { - position: absolute; - top: -4px; - left: -4px; - width: inherit; - height: inherit; - color: rgba(0, 0, 0, 0.25); - border: inherit; - content: ''; - } - } - - // colors - // mixin to iterate over colors and create CSS class for each one - .make-color-classes(@i: length(@preset-colors)) when (@i > 0) { - .make-color-classes(@i - 1); - @color: extract(@preset-colors, @i); - @darkColor: '@{color}-6'; - &-color-@{color} { - color: @@darkColor; - background: @@darkColor; - } - } - .make-color-classes(); - - // placement - &.@{ribbon-prefix-cls}-placement-end { - right: -8px; - border-bottom-right-radius: 0; - .@{ribbon-prefix-cls}-corner { - right: 0; - border-color: currentcolor transparent transparent currentcolor; - } - } - - &.@{ribbon-prefix-cls}-placement-start { - left: -8px; - border-bottom-left-radius: 0; - .@{ribbon-prefix-cls}-corner { - left: 0; - border-color: currentcolor currentcolor transparent transparent; - } - } -} diff --git a/components/badge/style/rtl.less b/components/badge/style/rtl.less deleted file mode 100644 index 7d7c02867c..0000000000 --- a/components/badge/style/rtl.less +++ /dev/null @@ -1,67 +0,0 @@ -.@{badge-prefix-cls} { - &-rtl { - direction: rtl; - } - - &:not(&-not-a-wrapper) &-count, - &:not(&-not-a-wrapper) &-dot, - &:not(&-not-a-wrapper) .@{number-prefix-cls}-custom-component { - .@{badge-prefix-cls}-rtl & { - right: auto; - left: 0; - direction: ltr; - transform: translate(-50%, -50%); - transform-origin: 0% 0%; - } - } - - &-rtl&:not(&-not-a-wrapper) .@{number-prefix-cls}-custom-component { - right: auto; - left: 0; - transform: translate(-50%, -50%); - transform-origin: 0% 0%; - } - - &-status { - &-text { - .@{badge-prefix-cls}-rtl & { - margin-right: 8px; - margin-left: 0; - } - } - } -} - -.@{ribbon-prefix-cls}-rtl { - direction: rtl; - &.@{ribbon-prefix-cls}-placement-end { - right: unset; - left: -8px; - border-bottom-right-radius: @border-radius-sm; - border-bottom-left-radius: 0; - .@{ribbon-prefix-cls}-corner { - right: unset; - left: 0; - border-color: currentcolor currentcolor transparent transparent; - - &::after { - border-color: currentcolor currentcolor transparent transparent; - } - } - } - &.@{ribbon-prefix-cls}-placement-start { - right: -8px; - left: unset; - border-bottom-right-radius: 0; - border-bottom-left-radius: @border-radius-sm; - .@{ribbon-prefix-cls}-corner { - right: 0; - left: unset; - border-color: currentcolor transparent transparent currentcolor; - - &::after { - border-color: currentcolor transparent transparent currentcolor; - } - } - } -} diff --git a/components/badge/utils.ts b/components/badge/utils.ts deleted file mode 100644 index 21bebac2e4..0000000000 --- a/components/badge/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PresetColorTypes } from '../_util/colors'; - -export function isPresetColor(color?: string): boolean { - return (PresetColorTypes as any[]).indexOf(color) !== -1; -} diff --git a/components/breadcrumb/Breadcrumb.tsx b/components/breadcrumb/Breadcrumb.tsx index c14eaa1d55..ad9e24b0de 100644 --- a/components/breadcrumb/Breadcrumb.tsx +++ b/components/breadcrumb/Breadcrumb.tsx @@ -3,10 +3,12 @@ import { cloneVNode, defineComponent } from 'vue'; import PropTypes from '../_util/vue-types'; import { flattenChildren, getPropsSlot } from '../_util/props-util'; import warning from '../_util/warning'; +import type { BreadcrumbItemProps } from './BreadcrumbItem'; import BreadcrumbItem from './BreadcrumbItem'; import Menu from '../menu'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; +import useStyle from './style'; import type { CustomSlotsType, VueNode } from '../_util/type'; -import useConfigInject from '../_util/hooks/useConfigInject'; export interface Route { path: string; @@ -54,15 +56,16 @@ function defaultItemRender(opt: { export default defineComponent({ compatConfig: { MODE: 3 }, name: 'ABreadcrumb', + inheritAttrs: false, props: breadcrumbProps(), slots: Object as CustomSlotsType<{ separator: any; itemRender: { route: Route; params: any; routes: Route[]; paths: string[] }; default: any; }>, - setup(props, { slots }) { + setup(props, { slots, attrs }) { const { prefixCls, direction } = useConfigInject('breadcrumb', props); - + const [wrapSSR, hashId] = useStyle(prefixCls); const getPath = (path: string, params: unknown) => { path = (path || '').replace(/^\//, ''); Object.keys(params).forEach(key => { @@ -98,27 +101,25 @@ export default defineComponent({ let overlay = null; if (route.children && route.children.length) { overlay = ( - - {route.children.map(child => ( - - {itemRender({ - route: child, - params, - routes, - paths: addChildPath(tempPaths, child.path, params), - })} - - ))} - + ({ + key: child.path || child.breadcrumbName, + label: itemRender({ + route: child, + params, + routes, + paths: addChildPath(tempPaths, child.path, params), + }), + }))} + > ); } - + const itemProps: BreadcrumbItemProps = { separator }; + if (overlay) { + itemProps.overlay = overlay; + } return ( - + {itemRender({ route, params, routes, paths: tempPaths })} ); @@ -156,8 +157,15 @@ export default defineComponent({ const breadcrumbClassName = { [prefixCls.value]: true, [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + [`${attrs.class}`]: !!attrs.class, + [hashId.value]: true, }; - return
{crumbs}
; + + return wrapSSR( + , + ); }; }, }); diff --git a/components/breadcrumb/BreadcrumbItem.tsx b/components/breadcrumb/BreadcrumbItem.tsx index c7f2fa7a39..194114c983 100644 --- a/components/breadcrumb/BreadcrumbItem.tsx +++ b/components/breadcrumb/BreadcrumbItem.tsx @@ -1,19 +1,22 @@ -import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'; +import type { CSSProperties, ExtractPropTypes } from 'vue'; import { defineComponent } from 'vue'; import PropTypes from '../_util/vue-types'; import { getPropsSlot } from '../_util/props-util'; -import DropDown from '../dropdown/dropdown'; +import type { DropdownProps } from '../dropdown/dropdown'; +import Dropdown from '../dropdown/dropdown'; import DownOutlined from '@ant-design/icons-vue/DownOutlined'; -import useConfigInject from '../_util/hooks/useConfigInject'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; import type { MouseEventHandler } from '../_util/EventInterface'; +import { eventType, objectType } from '../_util/type'; import type { CustomSlotsType } from '../_util/type'; export const breadcrumbItemProps = () => ({ prefixCls: String, href: String, separator: PropTypes.any, + dropdownProps: objectType(), overlay: PropTypes.any, - onClick: Function as PropType, + onClick: eventType(), }); export type BreadcrumbItemProps = Partial>>; @@ -29,27 +32,29 @@ export default defineComponent({ overlay: any; default: any; }>, - setup(props, { slots, attrs }) { + setup(props, { slots, attrs, emit }) { const { prefixCls } = useConfigInject('breadcrumb', props); /** * if overlay is have - * Wrap a DropDown + * Wrap a Dropdown */ const renderBreadcrumbNode = (breadcrumbItem: JSX.Element, prefixCls: string) => { const overlay = getPropsSlot(slots, props, 'overlay'); if (overlay) { return ( - + {breadcrumbItem} - + ); } return breadcrumbItem; }; - + const handleClick = (e: MouseEvent) => { + emit('click', e); + }; return () => { const separator = getPropsSlot(slots, props, 'separator') ?? '/'; const children = getPropsSlot(slots, props); @@ -57,25 +62,25 @@ export default defineComponent({ let link: JSX.Element; if (props.href !== undefined) { link = ( - + {children} ); } else { link = ( - + {children} ); } // wrap to dropDown link = renderBreadcrumbNode(link, prefixCls.value); - if (children) { + if (children !== undefined && children !== null) { return ( - +
  • {link} {separator && {separator}} - +
  • ); } return null; diff --git a/components/breadcrumb/BreadcrumbSeparator.tsx b/components/breadcrumb/BreadcrumbSeparator.tsx index 91e90732d8..6f981dff43 100644 --- a/components/breadcrumb/BreadcrumbSeparator.tsx +++ b/components/breadcrumb/BreadcrumbSeparator.tsx @@ -1,7 +1,7 @@ import type { ExtractPropTypes } from 'vue'; import { defineComponent } from 'vue'; import { flattenChildren } from '../_util/props-util'; -import useConfigInject from '../_util/hooks/useConfigInject'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; export const breadcrumbSeparatorProps = () => ({ prefixCls: String, diff --git a/components/breadcrumb/__tests__/Breadcrumb.test.js b/components/breadcrumb/__tests__/Breadcrumb.test.js index 4375e885d2..0af7f08e03 100644 --- a/components/breadcrumb/__tests__/Breadcrumb.test.js +++ b/components/breadcrumb/__tests__/Breadcrumb.test.js @@ -25,7 +25,7 @@ describe('Breadcrumb', () => { }); expect(errorSpy.mock.calls).toHaveLength(1); expect(errorSpy.mock.calls[0][0]).toMatch( - "Warning: [antdv: Breadcrumb] Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children", + "Warning: [ant-design-vue: Breadcrumb] Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children", ); }); diff --git a/components/breadcrumb/__tests__/__snapshots__/Breadcrumb.test.js.snap b/components/breadcrumb/__tests__/__snapshots__/Breadcrumb.test.js.snap index 8192190693..2da2de9edf 100644 --- a/components/breadcrumb/__tests__/__snapshots__/Breadcrumb.test.js.snap +++ b/components/breadcrumb/__tests__/__snapshots__/Breadcrumb.test.js.snap @@ -1,15 +1,49 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Breadcrumb should allow Breadcrumb.Item is null or undefined 1`] = `
    Home/
    `; +exports[`Breadcrumb should allow Breadcrumb.Item is null or undefined 1`] = ` + +`; exports[`Breadcrumb should not display Breadcrumb Item when its children is falsy 1`] = ` -
    - xxx/yyy/ -
    + `; -exports[`Breadcrumb should render a menu 1`] = `
    home/first/second/
    `; +exports[`Breadcrumb should render a menu 1`] = ` + +`; -exports[`Breadcrumb should support Breadcrumb.Item default separator 1`] = `
    Location/Mock Node/Application Center/
    `; +exports[`Breadcrumb should support Breadcrumb.Item default separator 1`] = ` + +`; -exports[`Breadcrumb should support custom attribute 1`] = `
    xxx/yyy/
    `; +exports[`Breadcrumb should support custom attribute 1`] = ` + +`; diff --git a/components/breadcrumb/__tests__/__snapshots__/demo.test.js.snap b/components/breadcrumb/__tests__/__snapshots__/demo.test.js.snap index eb22264cf5..1a0c70eb23 100644 --- a/components/breadcrumb/__tests__/__snapshots__/demo.test.js.snap +++ b/components/breadcrumb/__tests__/__snapshots__/demo.test.js.snap @@ -1,14 +1,65 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders ./components/breadcrumb/demo/basic.vue correctly 1`] = ``; +exports[`renders ./components/breadcrumb/demo/basic.vue correctly 1`] = ` + +`; -exports[`renders ./components/breadcrumb/demo/overlay.vue correctly 1`] = `
    Ant Design Vue/Component/General/Button/
    `; +exports[`renders ./components/breadcrumb/demo/overlay.vue correctly 1`] = ` + +`; exports[`renders ./components/breadcrumb/demo/separator.vue correctly 1`] = ` - - + + `; -exports[`renders ./components/breadcrumb/demo/separator-indepent.vue correctly 1`] = `
    Location:Application Center/Application List/An Application
    `; +exports[`renders ./components/breadcrumb/demo/separator-indepent.vue correctly 1`] = ` + +`; -exports[`renders ./components/breadcrumb/demo/withIcon.vue correctly 1`] = `
    /Application List/Application/
    `; +exports[`renders ./components/breadcrumb/demo/withIcon.vue correctly 1`] = ` + +`; diff --git a/components/breadcrumb/demo/router.vue b/components/breadcrumb/demo/router.vue index c5037485a0..cafd12a63d 100644 --- a/components/breadcrumb/demo/router.vue +++ b/components/breadcrumb/demo/router.vue @@ -32,8 +32,8 @@ Used together with `vue-router` {{ $route.path }}
    - diff --git a/components/breadcrumb/demo/withIcon.vue b/components/breadcrumb/demo/withIcon.vue index 02c28b8cdf..636c7dbeda 100644 --- a/components/breadcrumb/demo/withIcon.vue +++ b/components/breadcrumb/demo/withIcon.vue @@ -28,13 +28,6 @@ The icon should be placed in front of the text. Application - diff --git a/components/breadcrumb/index.en-US.md b/components/breadcrumb/index.en-US.md index 504affdfee..d1f9b219bc 100644 --- a/components/breadcrumb/index.en-US.md +++ b/components/breadcrumb/index.en-US.md @@ -2,7 +2,8 @@ category: Components type: Navigation title: Breadcrumb -cover: https://gw.alipayobjects.com/zos/alicdn/9Ltop8JwH/Breadcrumb.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*I5a2Tpqs3y0AAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*Tr90QKrE_LcAAAAAAAAAAAAADrJ8AQ/original --- A breadcrumb displays the current location within a hierarchy. It allows going back to states higher up in the hierarchy. diff --git a/components/breadcrumb/index.zh-CN.md b/components/breadcrumb/index.zh-CN.md index 548c096757..5e67b0fb40 100644 --- a/components/breadcrumb/index.zh-CN.md +++ b/components/breadcrumb/index.zh-CN.md @@ -3,7 +3,8 @@ category: Components subtitle: 面包屑 type: 导航 title: Breadcrumb -cover: https://gw.alipayobjects.com/zos/alicdn/9Ltop8JwH/Breadcrumb.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*I5a2Tpqs3y0AAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*Tr90QKrE_LcAAAAAAAAAAAAADrJ8AQ/original --- 显示当前页面在系统层级结构中的位置,并能向上返回。 diff --git a/components/breadcrumb/style/index.less b/components/breadcrumb/style/index.less deleted file mode 100644 index ca29afb3a9..0000000000 --- a/components/breadcrumb/style/index.less +++ /dev/null @@ -1,56 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@breadcrumb-prefix-cls: ~'@{ant-prefix}-breadcrumb'; - -.@{breadcrumb-prefix-cls} { - .reset-component(); - - color: @breadcrumb-base-color; - font-size: @breadcrumb-font-size; - - .@{iconfont-css-prefix} { - font-size: @breadcrumb-icon-font-size; - } - - a { - color: @breadcrumb-link-color; - transition: color 0.3s; - - &:hover { - color: @breadcrumb-link-color-hover; - } - } - - & > span:last-child { - color: @breadcrumb-last-item-color; - - a { - color: @breadcrumb-last-item-color; - } - } - - & > span:last-child &-separator { - display: none; - } - - &-separator { - margin: @breadcrumb-separator-margin; - color: @breadcrumb-separator-color; - } - - &-link { - > .@{iconfont-css-prefix} + span, - > .@{iconfont-css-prefix} + a { - margin-left: 4px; - } - } - - &-overlay-link { - > .@{iconfont-css-prefix} { - margin-left: 4px; - } - } -} - -@import './rtl'; diff --git a/components/breadcrumb/style/index.ts b/components/breadcrumb/style/index.ts new file mode 100644 index 0000000000..9d3b24d9cf --- /dev/null +++ b/components/breadcrumb/style/index.ts @@ -0,0 +1,127 @@ +import type { CSSObject } from '../../_util/cssinjs'; +import type { FullToken, GenerateStyle } from '../../theme/internal'; +import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import { genFocusStyle, resetComponent } from '../../style'; + +interface BreadcrumbToken extends FullToken<'Breadcrumb'> { + breadcrumbBaseColor: string; + breadcrumbFontSize: number; + breadcrumbIconFontSize: number; + breadcrumbLinkColor: string; + breadcrumbLinkColorHover: string; + breadcrumbLastItemColor: string; + breadcrumbSeparatorMargin: number; + breadcrumbSeparatorColor: string; +} + +const genBreadcrumbStyle: GenerateStyle = token => { + const { componentCls, iconCls } = token; + + return { + [componentCls]: { + ...resetComponent(token), + color: token.breadcrumbBaseColor, + fontSize: token.breadcrumbFontSize, + + [iconCls]: { + fontSize: token.breadcrumbIconFontSize, + }, + + ol: { + display: 'flex', + flexWrap: 'wrap', + margin: 0, + padding: 0, + listStyle: 'none', + }, + + a: { + color: token.breadcrumbLinkColor, + transition: `color ${token.motionDurationMid}`, + padding: `0 ${token.paddingXXS}px`, + borderRadius: token.borderRadiusSM, + height: token.lineHeight * token.fontSize, + display: 'inline-block', + marginInline: -token.marginXXS, + + '&:hover': { + color: token.breadcrumbLinkColorHover, + backgroundColor: token.colorBgTextHover, + }, + + ...genFocusStyle(token), + }, + + [`li:last-child`]: { + color: token.breadcrumbLastItemColor, + + [`& > ${componentCls}-separator`]: { + display: 'none', + }, + }, + + [`${componentCls}-separator`]: { + marginInline: token.breadcrumbSeparatorMargin, + color: token.breadcrumbSeparatorColor, + }, + + [`${componentCls}-link`]: { + [` + > ${iconCls} + span, + > ${iconCls} + a + `]: { + marginInlineStart: token.marginXXS, + }, + }, + + [`${componentCls}-overlay-link`]: { + borderRadius: token.borderRadiusSM, + height: token.lineHeight * token.fontSize, + display: 'inline-block', + padding: `0 ${token.paddingXXS}px`, + marginInline: -token.marginXXS, + + [`> ${iconCls}`]: { + marginInlineStart: token.marginXXS, + fontSize: token.fontSizeIcon, + }, + + '&:hover': { + color: token.breadcrumbLinkColorHover, + backgroundColor: token.colorBgTextHover, + + a: { + color: token.breadcrumbLinkColorHover, + }, + }, + + a: { + '&:hover': { + backgroundColor: 'transparent', + }, + }, + }, + + // rtl style + [`&${token.componentCls}-rtl`]: { + direction: 'rtl', + }, + }, + }; +}; + +// ============================== Export ============================== +export default genComponentStyleHook('Breadcrumb', token => { + const BreadcrumbToken = mergeToken(token, { + breadcrumbBaseColor: token.colorTextDescription, + breadcrumbFontSize: token.fontSize, + breadcrumbIconFontSize: token.fontSize, + breadcrumbLinkColor: token.colorTextDescription, + breadcrumbLinkColorHover: token.colorText, + breadcrumbLastItemColor: token.colorText, + breadcrumbSeparatorMargin: token.marginXS, + breadcrumbSeparatorColor: token.colorTextDescription, + }); + + return [genBreadcrumbStyle(BreadcrumbToken)]; +}); diff --git a/components/breadcrumb/style/index.tsx b/components/breadcrumb/style/index.tsx deleted file mode 100644 index 3d7084daa2..0000000000 --- a/components/breadcrumb/style/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import '../../style/index.less'; -import './index.less'; - -// style dependencies -import '../../menu/style'; -import '../../dropdown/style'; diff --git a/components/breadcrumb/style/rtl.less b/components/breadcrumb/style/rtl.less deleted file mode 100644 index c1141c9937..0000000000 --- a/components/breadcrumb/style/rtl.less +++ /dev/null @@ -1,29 +0,0 @@ -.@{breadcrumb-prefix-cls} { - &-rtl { - .clearfix(); - direction: rtl; - - > span { - float: right; - } - } - - &-link { - > .@{iconfont-css-prefix} + span, - > .@{iconfont-css-prefix} + a { - .@{breadcrumb-prefix-cls}-rtl & { - margin-right: 4px; - margin-left: 0; - } - } - } - - &-overlay-link { - > .@{iconfont-css-prefix} { - .@{breadcrumb-prefix-cls}-rtl & { - margin-right: 4px; - margin-left: 0; - } - } - } -} diff --git a/components/button/__tests__/__snapshots__/demo.test.js.snap b/components/button/__tests__/__snapshots__/demo.test.js.snap index 25dbe26448..cd54d6e055 100644 --- a/components/button/__tests__/__snapshots__/demo.test.js.snap +++ b/components/button/__tests__/__snapshots__/demo.test.js.snap @@ -1,61 +1,75 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders ./components/button/demo/basic.vue correctly 1`] = ` - - - - - +
    +
    + +
    + +
    + +
    + +
    + +
    `; exports[`renders ./components/button/demo/block.vue correctly 1`] = ` - - - - - +
    +
    + +
    + +
    + +
    + +
    + +
    `; exports[`renders ./components/button/demo/button-group.vue correctly 1`] = `

    Basic

    -
    -
    +
    +
    + +
    + +
    + +
    + +
    + +
    `; exports[`renders ./components/button/demo/disabled.vue correctly 1`] = ` - - -
    - - -
    - - -
    - - -
    - - -
    - - -
    - - -
    - - -
    -
    +
    +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    `; exports[`renders ./components/button/demo/ghost.vue correctly 1`] = ` -
    +
    +
    +
    + +
    + +
    + +
    + +
    +
    `; exports[`renders ./components/button/demo/icon.vue correctly 1`] = ` - - - - - - - - - - -
    -
    - - - - - -
    - - - - - +
    +
    +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    + +
    `; exports[`renders ./components/button/demo/loading.vue correctly 1`] = ` -
    -
    +
    +
    +
    +
    + +
    + +
    +
    -
    +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    + +
    +
    -
    +`; + +exports[`renders ./components/button/demo/multiple.vue correctly 1`] = ` +
    -
    - -
    -
    -
    - -
    +
    -
    +
    + +
    `; -exports[`renders ./components/button/demo/multiple.vue correctly 1`] = ` - - - -`; - exports[`renders ./components/button/demo/size.vue correctly 1`] = ` -
    -
    -
    - - - - - -
    - - - - - -
    +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    + +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    + +
    + +
    + +
    +
    + +
    `; diff --git a/components/button/__tests__/__snapshots__/index.test.js.snap b/components/button/__tests__/__snapshots__/index.test.js.snap index 00b3398d8a..316ca068bb 100644 --- a/components/button/__tests__/__snapshots__/index.test.js.snap +++ b/components/button/__tests__/__snapshots__/index.test.js.snap @@ -1,53 +1,53 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Button fixbug renders {0} , 0 and {false} 1`] = ` - `; exports[`Button fixbug renders {0} , 0 and {false} 2`] = ` - `; exports[`Button fixbug renders {0} , 0 and {false} 3`] = ` - `; exports[`Button renders Chinese characters correctly 1`] = ` - `; exports[`Button renders Chinese characters correctly 2`] = ` - `; exports[`Button renders Chinese characters correctly 3`] = ` - `; -exports[`Button renders Chinese characters correctly 4`] = ``; +exports[`Button renders Chinese characters correctly 4`] = ``; -exports[`Button renders Chinese characters correctly 5`] = ``; +exports[`Button renders Chinese characters correctly 5`] = ``; exports[`Button renders Chinese characters correctly 6`] = ` - `; exports[`Button renders correctly 1`] = ` - `; @@ -59,7 +59,7 @@ exports[`Button should not render as link button when href is undefined 1`] = ` `; exports[`Button should support link button 1`] = ` - + link button `; diff --git a/components/button/__tests__/wave.test.js b/components/button/__tests__/wave.test.js index 0e9300484c..cfe216642a 100644 --- a/components/button/__tests__/wave.test.js +++ b/components/button/__tests__/wave.test.js @@ -13,54 +13,6 @@ describe('click wave effect', () => { await sleep(20); } - it('should have click wave effect for primary button', async () => { - const wrapper = mount({ - render() { - return ; - }, - }); - await clickButton(wrapper); - expect(wrapper.find('.ant-btn').attributes('ant-click-animating-without-extra-node')).toBe( - 'true', - ); - }); - - it('should have click wave effect for default button', async () => { - const wrapper = mount({ - render() { - return ; - }, - }); - await clickButton(wrapper); - expect(wrapper.find('.ant-btn').attributes('ant-click-animating-without-extra-node')).toBe( - 'true', - ); - }); - - it('should not have click wave effect for link type button', async () => { - const wrapper = mount({ - render() { - return ; - }, - }); - await clickButton(wrapper); - expect(wrapper.find('.ant-btn').attributes('ant-click-animating-without-extra-node')).toBe( - undefined, - ); - }); - - it('should not have click wave effect for text type button', async () => { - const wrapper = mount({ - render() { - return ; - }, - }); - await clickButton(wrapper); - expect(wrapper.find('.ant-btn').attributes('ant-click-animating-without-extra-node')).toBe( - undefined, - ); - }); - it('should handle transitionstart', async () => { const wrapper = mount({ render() { @@ -70,9 +22,6 @@ describe('click wave effect', () => { await clickButton(wrapper); const buttonNode = wrapper.find('.ant-btn').element; buttonNode.dispatchEvent(new Event('transitionstart')); - expect(wrapper.find('.ant-btn').attributes('ant-click-animating-without-extra-node')).toBe( - 'true', - ); wrapper.unmount(); buttonNode.dispatchEvent(new Event('transitionstart')); }); diff --git a/components/button/button-group.tsx b/components/button/button-group.tsx index f934ece192..1403382652 100644 --- a/components/button/button-group.tsx +++ b/components/button/button-group.tsx @@ -1,10 +1,11 @@ -import { computed, defineComponent } from 'vue'; +import { computed, defineComponent, reactive } from 'vue'; import { flattenChildren } from '../_util/props-util'; -import useConfigInject from '../_util/hooks/useConfigInject'; - +import useConfigInject from '../config-provider/hooks/useConfigInject'; +import { useToken } from '../theme/internal'; import type { ExtractPropTypes, PropType } from 'vue'; import type { SizeType } from '../config-provider'; -import UnreachableException from '../_util/unreachableException'; +import devWarning from '../vc-util/devWarning'; +import createContext from '../_util/createContext'; export const buttonGroupProps = () => ({ prefixCls: String, @@ -14,17 +15,23 @@ export const buttonGroupProps = () => ({ }); export type ButtonGroupProps = Partial>>; - +export const GroupSizeContext = createContext<{ + size: SizeType; +}>(); export default defineComponent({ compatConfig: { MODE: 3 }, name: 'AButtonGroup', props: buttonGroupProps(), setup(props, { slots }) { const { prefixCls, direction } = useConfigInject('btn-group', props); + const [, , hashId] = useToken(); + GroupSizeContext.useProvide( + reactive({ + size: computed(() => props.size), + }), + ); const classes = computed(() => { const { size } = props; - // large => lg - // small => sm let sizeCls = ''; switch (size) { case 'large': @@ -38,12 +45,13 @@ export default defineComponent({ break; default: // eslint-disable-next-line no-console - console.warn(new UnreachableException(size).error); + devWarning(!size, 'Button.Group', 'Invalid prop `size`.'); } return { [`${prefixCls.value}`]: true, [`${prefixCls.value}-${sizeCls}`]: sizeCls, [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + [hashId.value]: true, }; }); return () => { diff --git a/components/button/button.tsx b/components/button/button.tsx index 951c298d20..32fdf9e21d 100644 --- a/components/button/button.tsx +++ b/components/button/button.tsx @@ -4,7 +4,7 @@ import { onBeforeUnmount, onMounted, onUpdated, - ref, + shallowRef, Text, watch, watchEffect, @@ -12,12 +12,15 @@ import { import Wave from '../_util/wave'; import buttonProps from './buttonTypes'; import { flattenChildren, initDefaultProps } from '../_util/props-util'; -import useConfigInject from '../_util/hooks/useConfigInject'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; +import { useInjectDisabled } from '../config-provider/DisabledContext'; import devWarning from '../vc-util/devWarning'; import LoadingIcon from './LoadingIcon'; - +import useStyle from './style'; import type { ButtonType } from './buttonTypes'; -import type { VNode, Ref } from 'vue'; +import type { VNode } from 'vue'; +import { GroupSizeContext } from './button-group'; +import { useCompactItemContext } from '../space/Compact'; import type { CustomSlotsType } from '../_util/type'; type Loading = boolean | number; @@ -25,7 +28,7 @@ type Loading = boolean | number; const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/; const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar); -function isUnborderedButtonType(type: ButtonType | undefined) { +function isUnBorderedButtonType(type: ButtonType | undefined) { return type === 'text' || type === 'link'; } export { buttonProps }; @@ -42,15 +45,19 @@ export default defineComponent({ // emits: ['click', 'mousedown'], setup(props, { slots, attrs, emit, expose }) { const { prefixCls, autoInsertSpaceInButton, direction, size } = useConfigInject('btn', props); - - const buttonNodeRef = ref(null); - const delayTimeoutRef = ref(undefined); + const [wrapSSR, hashId] = useStyle(prefixCls); + const groupSizeContext = GroupSizeContext.useInject(); + const disabledContext = useInjectDisabled(); + const mergedDisabled = computed(() => props.disabled ?? disabledContext.value); + const buttonNodeRef = shallowRef(null); + const delayTimeoutRef = shallowRef(undefined); let isNeedInserted = false; - const innerLoading: Ref = ref(false); - const hasTwoCNChar = ref(false); + const innerLoading = shallowRef(false); + const hasTwoCNChar = shallowRef(false); const autoInsertSpace = computed(() => autoInsertSpaceInButton.value !== false); + const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction); // =============== Update Loading =============== const loadingOrDelay = computed(() => @@ -81,21 +88,25 @@ export default defineComponent({ const pre = prefixCls.value; const sizeClassNameMap = { large: 'lg', small: 'sm', middle: undefined }; - const sizeFullname = size.value; + const sizeFullname = compactSize.value || groupSizeContext?.size || size.value; const sizeCls = sizeFullname ? sizeClassNameMap[sizeFullname] || '' : ''; - return { - [`${pre}`]: true, - [`${pre}-${type}`]: type, - [`${pre}-${shape}`]: shape !== 'default' && shape, - [`${pre}-${sizeCls}`]: sizeCls, - [`${pre}-loading`]: innerLoading.value, - [`${pre}-background-ghost`]: ghost && !isUnborderedButtonType(type), - [`${pre}-two-chinese-chars`]: hasTwoCNChar.value && autoInsertSpace.value, - [`${pre}-block`]: block, - [`${pre}-dangerous`]: !!danger, - [`${pre}-rtl`]: direction.value === 'rtl', - }; + return [ + compactItemClassnames.value, + { + [hashId.value]: true, + [`${pre}`]: true, + [`${pre}-${shape}`]: shape !== 'default' && shape, + [`${pre}-${type}`]: type, + [`${pre}-${sizeCls}`]: sizeCls, + [`${pre}-loading`]: innerLoading.value, + [`${pre}-background-ghost`]: ghost && !isUnBorderedButtonType(type), + [`${pre}-two-chinese-chars`]: hasTwoCNChar.value && autoInsertSpace.value, + [`${pre}-block`]: block, + [`${pre}-dangerous`]: !!danger, + [`${pre}-rtl`]: direction.value === 'rtl', + }, + ]; }); const fixTwoCNChar = () => { @@ -116,12 +127,15 @@ export default defineComponent({ }; const handleClick = (event: Event) => { // https://github.com/ant-design/ant-design/issues/30207 - if (innerLoading.value || props.disabled) { + if (innerLoading.value || mergedDisabled.value) { event.preventDefault(); return; } emit('click', event); }; + const handleMousedown = (event: Event) => { + emit('mousedown', event); + }; const insertSpace = (child: VNode, needInserted: boolean) => { const SPACE = needInserted ? ' ' : ''; @@ -137,7 +151,7 @@ export default defineComponent({ watchEffect(() => { devWarning( - !(props.ghost && isUnborderedButtonType(props.type)), + !(props.ghost && isUnBorderedButtonType(props.type)), 'Button', "`link` or `text` button can't be a `ghost` button.", ); @@ -165,28 +179,27 @@ export default defineComponent({ const { icon = slots.icon?.() } = props; const children = flattenChildren(slots.default?.()); - isNeedInserted = children.length === 1 && !icon && !isUnborderedButtonType(props.type); + isNeedInserted = children.length === 1 && !icon && !isUnBorderedButtonType(props.type); - const { type, htmlType, disabled, href, title, target, onMousedown } = props; + const { type, htmlType, href, title, target } = props; const iconType = innerLoading.value ? 'loading' : icon; const buttonProps = { ...attrs, title, - disabled, + disabled: mergedDisabled.value, class: [ classes.value, attrs.class, { [`${prefixCls.value}-icon-only`]: children.length === 0 && !!iconType }, ], onClick: handleClick, - onMousedown, + onMousedown: handleMousedown, }; // https://github.com/vueComponent/ant-design-vue/issues/4930 - if (!disabled) { + if (!mergedDisabled.value) { delete buttonProps.disabled; } - const iconNode = icon && !innerLoading.value ? ( icon @@ -203,30 +216,30 @@ export default defineComponent({ ); if (href !== undefined) { - return ( + return wrapSSR( {iconNode} {kids} - + , ); } - const buttonNode = ( + let buttonNode = ( ); - if (isUnborderedButtonType(type)) { - return buttonNode; + if (!isUnBorderedButtonType(type)) { + buttonNode = ( + + {buttonNode} + + ); } - return ( - - {buttonNode} - - ); + return wrapSSR(buttonNode); }; }, }); diff --git a/components/button/buttonTypes.ts b/components/button/buttonTypes.ts index d4e49551d9..4ad33168ad 100644 --- a/components/button/buttonTypes.ts +++ b/components/button/buttonTypes.ts @@ -2,6 +2,8 @@ import PropTypes from '../_util/vue-types'; import type { ExtractPropTypes, PropType } from 'vue'; import type { SizeType } from '../config-provider'; +import { eventType } from '../_util/type'; +import type { MouseEventHandler } from '../_util/EventInterface'; export type ButtonType = 'link' | 'default' | 'primary' | 'ghost' | 'dashed' | 'text'; export type ButtonShape = 'default' | 'circle' | 'round'; @@ -36,12 +38,8 @@ export const buttonProps = () => ({ href: String, target: String, title: String, - onClick: { - type: Function as PropType<(event: MouseEvent) => void>, - }, - onMousedown: { - type: Function as PropType<(event: MouseEvent) => void>, - }, + onClick: eventType(), + onMousedown: eventType(), }); export type ButtonProps = Partial>>; diff --git a/components/button/demo/basic.vue b/components/button/demo/basic.vue index ff7893f34b..c42c6105b1 100644 --- a/components/button/demo/basic.vue +++ b/components/button/demo/basic.vue @@ -17,9 +17,11 @@ There are `primary` button, `default` button, `dashed` button, `text` button and diff --git a/components/button/demo/block.vue b/components/button/demo/block.vue index 1d9a65a65c..888e36d475 100644 --- a/components/button/demo/block.vue +++ b/components/button/demo/block.vue @@ -16,9 +16,11 @@ title: diff --git a/components/button/demo/button-group.vue b/components/button/demo/button-group.vue index 23a750b0d7..8cfffbce17 100644 --- a/components/button/demo/button-group.vue +++ b/components/button/demo/button-group.vue @@ -34,7 +34,6 @@ Debug usage M R -

    With Icon

    @@ -56,23 +55,17 @@ Debug usage
    - - diff --git a/components/carousel/demo/basic.vue b/components/carousel/demo/basic.vue index 78dbddb048..04f86fc361 100644 --- a/components/carousel/demo/basic.vue +++ b/components/carousel/demo/basic.vue @@ -24,24 +24,16 @@ Basic usage.

    4

    - + diff --git a/components/carousel/demo/customArrows.vue b/components/carousel/demo/customArrows.vue index 7e224cbaa6..2054458ca9 100644 --- a/components/carousel/demo/customArrows.vue +++ b/components/carousel/demo/customArrows.vue @@ -34,19 +34,14 @@ Custom arrows display

    4

    - + diff --git a/components/carousel/demo/customPaging.vue b/components/carousel/demo/customPaging.vue index 7b9b4ebdd3..f6a6f69ea7 100644 --- a/components/carousel/demo/customPaging.vue +++ b/components/carousel/demo/customPaging.vue @@ -46,33 +46,33 @@ export default defineComponent({ diff --git a/components/carousel/demo/fade.vue b/components/carousel/demo/fade.vue index bd5fce1117..b4d3a3f546 100644 --- a/components/carousel/demo/fade.vue +++ b/components/carousel/demo/fade.vue @@ -24,9 +24,10 @@ Slides use fade for transition.

    4

    + diff --git a/components/carousel/demo/position.vue b/components/carousel/demo/position.vue index 215565fa70..51a32b267d 100644 --- a/components/carousel/demo/position.vue +++ b/components/carousel/demo/position.vue @@ -29,21 +29,16 @@ There are 4 position options available.

    4

    - + diff --git a/components/carousel/index.en-US.md b/components/carousel/index.en-US.md index b0d3c83804..80df0d676b 100644 --- a/components/carousel/index.en-US.md +++ b/components/carousel/index.en-US.md @@ -2,7 +2,8 @@ category: Components type: Data Display title: Carousel -cover: https://gw.alipayobjects.com/zos/antfincdn/%24C9tmj978R/Carousel.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*bPMSSqbaTMkAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a-58QpYnqOsAAAAAAAAAAAAADrJ8AQ/original --- A carousel component. Scales with its container. diff --git a/components/carousel/index.tsx b/components/carousel/index.tsx index 8417d5c541..6166c3c257 100644 --- a/components/carousel/index.tsx +++ b/components/carousel/index.tsx @@ -1,11 +1,14 @@ -import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'; +import type { CSSProperties, ExtractPropTypes } from 'vue'; import { ref, computed, watchEffect, defineComponent } from 'vue'; import PropTypes from '../_util/vue-types'; import warning from '../_util/warning'; import classNames from '../_util/classNames'; import SlickCarousel from '../vc-slick'; -import { withInstall } from '../_util/type'; -import useConfigInject from '../_util/hooks/useConfigInject'; +import { withInstall, booleanType, functionType, stringType } from '../_util/type'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; + +// CSSINJS +import useStyle from './style'; export type SwipeDirection = 'left' | 'down' | 'right' | 'up' | string; @@ -24,49 +27,49 @@ export interface CarouselRef { // Carousel export const carouselProps = () => ({ - effect: String as PropType, - dots: { type: Boolean, default: true }, - vertical: { type: Boolean, default: undefined }, - autoplay: { type: Boolean, default: undefined }, + effect: stringType(), + dots: booleanType(true), + vertical: booleanType(), + autoplay: booleanType(), easing: String, - beforeChange: Function as PropType<(currentSlide: number, nextSlide: number) => void>, - afterChange: Function as PropType<(currentSlide: number) => void>, + beforeChange: functionType<(currentSlide: number, nextSlide: number) => void>(), + afterChange: functionType<(currentSlide: number) => void>(), // style: PropTypes.React.CSSProperties, prefixCls: String, - accessibility: { type: Boolean, default: undefined }, + accessibility: booleanType(), nextArrow: PropTypes.any, prevArrow: PropTypes.any, - pauseOnHover: { type: Boolean, default: undefined }, + pauseOnHover: booleanType(), // className: String, - adaptiveHeight: { type: Boolean, default: undefined }, - arrows: { type: Boolean, default: false }, + adaptiveHeight: booleanType(), + arrows: booleanType(false), autoplaySpeed: Number, - centerMode: { type: Boolean, default: undefined }, + centerMode: booleanType(), centerPadding: String, cssEase: String, dotsClass: String, - draggable: { type: Boolean, default: false }, - fade: { type: Boolean, default: undefined }, - focusOnSelect: { type: Boolean, default: undefined }, - infinite: { type: Boolean, default: undefined }, + draggable: booleanType(false), + fade: booleanType(), + focusOnSelect: booleanType(), + infinite: booleanType(), initialSlide: Number, - lazyLoad: String as PropType, - rtl: { type: Boolean, default: undefined }, + lazyLoad: stringType(), + rtl: booleanType(), slide: String, slidesToShow: Number, slidesToScroll: Number, speed: Number, - swipe: { type: Boolean, default: undefined }, - swipeToSlide: { type: Boolean, default: undefined }, - swipeEvent: Function as PropType<(swipeDirection: SwipeDirection) => void>, - touchMove: { type: Boolean, default: undefined }, + swipe: booleanType(), + swipeToSlide: booleanType(), + swipeEvent: functionType<(swipeDirection: SwipeDirection) => void>(), + touchMove: booleanType(), touchThreshold: Number, - variableWidth: { type: Boolean, default: undefined }, - useCSS: { type: Boolean, default: undefined }, + variableWidth: booleanType(), + useCSS: booleanType(), slickGoTo: Number, responsive: Array, - dotPosition: { type: String as PropType, default: undefined }, - verticalSwiping: { type: Boolean, default: false }, + dotPosition: stringType(), + verticalSwiping: booleanType(false), }); export type CarouselProps = Partial>>; const Carousel = defineComponent({ @@ -104,6 +107,10 @@ const Carousel = defineComponent({ ); }); const { prefixCls, direction } = useConfigInject('carousel', props); + + // style + const [wrapSSR, hashId] = useStyle(prefixCls); + const dotPosition = computed(() => { if (props.dotPosition) return props.dotPosition; if (props.vertical !== undefined) return props.vertical ? 'right' : 'bottom'; @@ -122,12 +129,16 @@ const Carousel = defineComponent({ const { dots, arrows, draggable, effect } = props; const { class: cls, style, ...restAttrs } = attrs; const fade = effect === 'fade' ? true : props.fade; - const className = classNames(prefixCls.value, { - [`${prefixCls.value}-rtl`]: direction.value === 'rtl', - [`${prefixCls.value}-vertical`]: vertical.value, - [`${cls}`]: !!cls, - }); - return ( + const className = classNames( + prefixCls.value, + { + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + [`${prefixCls.value}-vertical`]: vertical.value, + [`${cls}`]: !!cls, + }, + hashId.value, + ); + return wrapSSR(
    -
    +
    , ); }; }, diff --git a/components/carousel/index.zh-CN.md b/components/carousel/index.zh-CN.md index bb02f2be8b..d40cc435e7 100644 --- a/components/carousel/index.zh-CN.md +++ b/components/carousel/index.zh-CN.md @@ -3,7 +3,8 @@ category: Components type: 数据展示 title: Carousel subtitle: 走马灯 -cover: https://gw.alipayobjects.com/zos/antfincdn/%24C9tmj978R/Carousel.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*bPMSSqbaTMkAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a-58QpYnqOsAAAAAAAAAAAAADrJ8AQ/original --- 旋转木马,一组轮播的区域。 diff --git a/components/carousel/style/index.less b/components/carousel/style/index.less deleted file mode 100644 index 031383b2b6..0000000000 --- a/components/carousel/style/index.less +++ /dev/null @@ -1,294 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@carousel-prefix-cls: ~'@{ant-prefix}-carousel'; - -.@{carousel-prefix-cls} { - .reset-component(); - - .slick-slider { - position: relative; - display: block; - box-sizing: border-box; - touch-action: pan-y; - -webkit-touch-callout: none; - -webkit-tap-highlight-color: transparent; - } - - .slick-list { - position: relative; - display: block; - margin: 0; - padding: 0; - overflow: hidden; - - &:focus { - outline: none; - } - - &.dragging { - cursor: pointer; - } - - .slick-slide { - pointer-events: none; - - // https://github.com/ant-design/ant-design/issues/23294 - input.@{ant-prefix}-radio-input, - input.@{ant-prefix}-checkbox-input { - visibility: hidden; - } - - &.slick-active { - pointer-events: auto; - - input.@{ant-prefix}-radio-input, - input.@{ant-prefix}-checkbox-input { - visibility: visible; - } - } - - // fix Carousel content height not match parent node - // when children is empty node - // https://github.com/ant-design/ant-design/issues/25878 - > div > div { - vertical-align: bottom; - } - } - } - - .slick-slider .slick-track, - .slick-slider .slick-list { - transform: translate3d(0, 0, 0); - touch-action: pan-y; - } - - .slick-track { - position: relative; - top: 0; - left: 0; - display: block; - - &::before, - &::after { - display: table; - content: ''; - } - - &::after { - clear: both; - } - - .slick-loading & { - visibility: hidden; - } - } - - .slick-slide { - display: none; - float: left; - height: 100%; - min-height: 1px; - - img { - display: block; - } - - &.slick-loading img { - display: none; - } - - &.dragging img { - pointer-events: none; - } - } - - .slick-initialized .slick-slide { - display: block; - } - - .slick-loading .slick-slide { - visibility: hidden; - } - - .slick-vertical .slick-slide { - display: block; - height: auto; - } - - .slick-arrow.slick-hidden { - display: none; - } - - // Arrows - .slick-prev, - .slick-next { - position: absolute; - top: 50%; - display: block; - width: 20px; - height: 20px; - margin-top: -10px; - padding: 0; - color: transparent; - font-size: 0; - line-height: 0; - background: transparent; - border: 0; - outline: none; - cursor: pointer; - - &:hover, - &:focus { - color: transparent; - background: transparent; - outline: none; - - &::before { - opacity: 1; - } - } - - &.slick-disabled::before { - opacity: 0.25; - } - } - - .slick-prev { - left: -25px; - - &::before { - content: '←'; - } - } - - .slick-next { - right: -25px; - - &::before { - content: '→'; - } - } - - // Dots - .slick-dots { - position: absolute; - right: 0; - bottom: 0; - left: 0; - z-index: 15; - display: flex !important; - justify-content: center; - margin-right: 15%; - margin-left: 15%; - padding-left: 0; - list-style: none; - - &-bottom { - bottom: 12px; - } - - &-top { - top: 12px; - bottom: auto; - } - - li { - position: relative; - display: inline-block; - flex: 0 1 auto; - box-sizing: content-box; - width: @carousel-dot-width; - height: @carousel-dot-height; - margin: 0 2px; - margin-right: 3px; - margin-left: 3px; - padding: 0; - text-align: center; - text-indent: -999px; - vertical-align: top; - transition: all 0.5s; - - button { - display: block; - width: 100%; - height: @carousel-dot-height; - padding: 0; - color: transparent; - font-size: 0; - background: @component-background; - border: 0; - border-radius: 1px; - outline: none; - cursor: pointer; - opacity: 0.3; - transition: all 0.5s; - - &:hover, - &:focus { - opacity: 0.75; - } - } - - &.slick-active { - width: @carousel-dot-active-width; - - & button { - background: @component-background; - opacity: 1; - } - - &:hover, - &:focus { - opacity: 1; - } - } - } - } -} - -.@{ant-prefix}-carousel-vertical { - .slick-dots { - top: 50%; - bottom: auto; - flex-direction: column; - width: @carousel-dot-height; - height: auto; - margin: 0; - transform: translateY(-50%); - - &-left { - right: auto; - left: 12px; - } - - &-right { - right: 12px; - left: auto; - } - - li { - width: @carousel-dot-height; - height: @carousel-dot-width; - margin: 4px 2px; - vertical-align: baseline; - - button { - width: @carousel-dot-height; - height: @carousel-dot-width; - } - - &.slick-active { - width: @carousel-dot-height; - height: @carousel-dot-active-width; - - button { - width: @carousel-dot-height; - height: @carousel-dot-active-width; - } - } - } - } -} - -@import './rtl'; diff --git a/components/carousel/style/index.tsx b/components/carousel/style/index.tsx index 3a3ab0de59..06e29a3e94 100644 --- a/components/carousel/style/index.tsx +++ b/components/carousel/style/index.tsx @@ -1,2 +1,350 @@ -import '../../style/index.less'; -import './index.less'; +import type { FullToken, GenerateStyle } from '../../theme/internal'; +import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import { resetComponent } from '../../style'; + +export interface ComponentToken { + dotWidth: number; + dotHeight: number; + dotWidthActive: number; +} + +interface CarouselToken extends FullToken<'Carousel'> { + carouselArrowSize: number; + carouselDotOffset: number; + carouselDotInline: number; +} + +const genCarouselStyle: GenerateStyle = token => { + const { componentCls, antCls, carouselArrowSize, carouselDotOffset, marginXXS } = token; + const arrowOffset = -carouselArrowSize * 1.25; + + const carouselDotMargin = marginXXS; + + return { + [componentCls]: { + ...resetComponent(token), + + '.slick-slider': { + position: 'relative', + display: 'block', + boxSizing: 'border-box', + touchAction: 'pan-y', + WebkitTouchCallout: 'none', + WebkitTapHighlightColor: 'transparent', + + '.slick-track, .slick-list': { + transform: 'translate3d(0, 0, 0)', + touchAction: 'pan-y', + }, + }, + + '.slick-list': { + position: 'relative', + display: 'block', + margin: 0, + padding: 0, + overflow: 'hidden', + + '&:focus': { + outline: 'none', + }, + + '&.dragging': { + cursor: 'pointer', + }, + + '.slick-slide': { + pointerEvents: 'none', + + // https://github.com/ant-design/ant-design/issues/23294 + [`input${antCls}-radio-input, input${antCls}-checkbox-input`]: { + visibility: 'hidden', + }, + + '&.slick-active': { + pointerEvents: 'auto', + + [`input${antCls}-radio-input, input${antCls}-checkbox-input`]: { + visibility: 'visible', + }, + }, + + // fix Carousel content height not match parent node + // when children is empty node + // https://github.com/ant-design/ant-design/issues/25878 + '> div > div': { + verticalAlign: 'bottom', + }, + }, + }, + + '.slick-track': { + position: 'relative', + top: 0, + insetInlineStart: 0, + display: 'block', + + '&::before, &::after': { + display: 'table', + content: '""', + }, + + '&::after': { + clear: 'both', + }, + }, + + '.slick-slide': { + display: 'none', + float: 'left', + height: '100%', + minHeight: 1, + + img: { + display: 'block', + }, + + '&.dragging img': { + pointerEvents: 'none', + }, + }, + + '.slick-initialized .slick-slide': { + display: 'block', + }, + + '.slick-vertical .slick-slide': { + display: 'block', + height: 'auto', + }, + + '.slick-arrow.slick-hidden': { + display: 'none', + }, + + // Arrows + '.slick-prev, .slick-next': { + position: 'absolute', + top: '50%', + display: 'block', + width: carouselArrowSize, + height: carouselArrowSize, + marginTop: -carouselArrowSize / 2, + padding: 0, + color: 'transparent', + fontSize: 0, + lineHeight: 0, + background: 'transparent', + border: 0, + outline: 'none', + cursor: 'pointer', + + '&:hover, &:focus': { + color: 'transparent', + background: 'transparent', + outline: 'none', + + '&::before': { + opacity: 1, + }, + }, + + '&.slick-disabled::before': { + opacity: 0.25, + }, + }, + + '.slick-prev': { + insetInlineStart: arrowOffset, + + '&::before': { + content: '"←"', + }, + }, + + '.slick-next': { + insetInlineEnd: arrowOffset, + + '&::before': { + content: '"→"', + }, + }, + + // Dots + '.slick-dots': { + position: 'absolute', + insetInlineEnd: 0, + bottom: 0, + insetInlineStart: 0, + zIndex: 15, + display: 'flex !important', + justifyContent: 'center', + paddingInlineStart: 0, + listStyle: 'none', + + '&-bottom': { + bottom: carouselDotOffset, + }, + + '&-top': { + top: carouselDotOffset, + bottom: 'auto', + }, + + li: { + position: 'relative', + display: 'inline-block', + flex: '0 1 auto', + boxSizing: 'content-box', + width: token.dotWidth, + height: token.dotHeight, + marginInline: carouselDotMargin, + padding: 0, + textAlign: 'center', + textIndent: -999, + verticalAlign: 'top', + transition: `all ${token.motionDurationSlow}`, + + button: { + position: 'relative', + display: 'block', + width: '100%', + height: token.dotHeight, + padding: 0, + color: 'transparent', + fontSize: 0, + background: token.colorBgContainer, + border: 0, + borderRadius: 1, + outline: 'none', + cursor: 'pointer', + opacity: 0.3, + transition: `all ${token.motionDurationSlow}`, + + '&: hover, &:focus': { + opacity: 0.75, + }, + + '&::after': { + position: 'absolute', + inset: -carouselDotMargin, + content: '""', + }, + }, + + '&.slick-active': { + width: token.dotWidthActive, + + '& button': { + background: token.colorBgContainer, + opacity: 1, + }, + + '&: hover, &:focus': { + opacity: 1, + }, + }, + }, + }, + }, + }; +}; + +const genCarouselVerticalStyle: GenerateStyle = token => { + const { componentCls, carouselDotOffset, marginXXS } = token; + + const reverseSizeOfDot = { + width: token.dotHeight, + height: token.dotWidth, + }; + + return { + [`${componentCls}-vertical`]: { + '.slick-dots': { + top: '50%', + bottom: 'auto', + flexDirection: 'column', + width: token.dotHeight, + height: 'auto', + margin: 0, + transform: 'translateY(-50%)', + + '&-left': { + insetInlineEnd: 'auto', + insetInlineStart: carouselDotOffset, + }, + + '&-right': { + insetInlineEnd: carouselDotOffset, + insetInlineStart: 'auto', + }, + + li: { + // reverse width and height in vertical situation + ...reverseSizeOfDot, + margin: `${marginXXS}px 0`, + verticalAlign: 'baseline', + + button: reverseSizeOfDot, + + '&.slick-active': { + ...reverseSizeOfDot, + + button: reverseSizeOfDot, + }, + }, + }, + }, + }; +}; + +const genCarouselRtlStyle: GenerateStyle = token => { + const { componentCls } = token; + + return [ + { + [`${componentCls}-rtl`]: { + direction: 'rtl', + + // Dots + '.slick-dots': { + [`${componentCls}-rtl&`]: { + flexDirection: 'row-reverse', + }, + }, + }, + }, + { + [`${componentCls}-vertical`]: { + '.slick-dots': { + [`${componentCls}-rtl&`]: { + flexDirection: 'column', + }, + }, + }, + }, + ]; +}; + +// ============================== Export ============================== +export default genComponentStyleHook( + 'Carousel', + token => { + const { controlHeightLG, controlHeightSM } = token; + const carouselToken = mergeToken(token, { + carouselArrowSize: controlHeightLG / 2, + carouselDotOffset: controlHeightSM / 2, + }); + + return [ + genCarouselStyle(carouselToken), + genCarouselVerticalStyle(carouselToken), + genCarouselRtlStyle(carouselToken), + ]; + }, + { + dotWidth: 16, + dotHeight: 3, + dotWidthActive: 24, + }, +); diff --git a/components/carousel/style/rtl.less b/components/carousel/style/rtl.less deleted file mode 100644 index c2853a2ac7..0000000000 --- a/components/carousel/style/rtl.less +++ /dev/null @@ -1,54 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@carousel-prefix-cls: ~'@{ant-prefix}-carousel'; - -.@{carousel-prefix-cls} { - &-rtl { - direction: rtl; - } - - .slick-track { - .@{carousel-prefix-cls}-rtl & { - right: 0; - left: auto; - } - } - - .slick-prev { - .@{carousel-prefix-cls}-rtl & { - right: -25px; - left: auto; - - &::before { - content: '→'; - } - } - } - - .slick-next { - .@{carousel-prefix-cls}-rtl & { - right: auto; - left: -25px; - - &::before { - content: '←'; - } - } - } - - // Dots - .slick-dots { - .@{carousel-prefix-cls}-rtl& { - flex-direction: row-reverse; - } - } -} - -.@{ant-prefix}-carousel-vertical { - .slick-dots { - .@{carousel-prefix-cls}-rtl& { - flex-direction: column; - } - } -} diff --git a/components/cascader/__tests__/__snapshots__/demo.test.js.snap b/components/cascader/__tests__/__snapshots__/demo.test.js.snap index 9cee6bb7b8..c3718f2a76 100644 --- a/components/cascader/__tests__/__snapshots__/demo.test.js.snap +++ b/components/cascader/__tests__/__snapshots__/demo.test.js.snap @@ -6,7 +6,8 @@ exports[`renders ./components/cascader/demo/basic.vue correctly 1`] = `
    Please select -
    +
    `; @@ -17,7 +18,8 @@ exports[`renders ./components/cascader/demo/change-on-select.vue correctly 1`] =
    Please select -
    +
    `; @@ -28,7 +30,8 @@ exports[`renders ./components/cascader/demo/custom-render.vue correctly 1`] = `
    Zhejiang /Hangzhou /West Lake ( 752100 ) -
    +
    `; @@ -40,7 +43,8 @@ exports[`renders ./components/cascader/demo/disabled-option.vue correctly 1`] =
    Please select -
    +
    `; @@ -51,7 +55,8 @@ exports[`renders ./components/cascader/demo/fields-name.vue correctly 1`] = `
    Please select -
    +
    `; @@ -62,7 +67,8 @@ exports[`renders ./components/cascader/demo/hover.vue correctly 1`] = `
    Please select -
    +
    `; @@ -73,25 +79,57 @@ exports[`renders ./components/cascader/demo/lazy.vue correctly 1`] = `
    Please select -
    +
    `; exports[`renders ./components/cascader/demo/multiple.vue correctly 1`] = ` -
    - +
    +
    +

    Cascader.SHOW_PARENT

    +
    -
    -
    +
    +
    -
    - + +
    +
    + +
    + +
    + +
    Please select
    -
    Please select + +
    +
    +

    Cascader.SHOW_CHILD

    +
    + +
    +
    + + +
    +
    + +
    + +
    + +
    Please select +
    + + +
    +
    `; @@ -102,7 +140,8 @@ exports[`renders ./components/cascader/demo/search.vue correctly 1`] = `
    Please select -
    +
    `; @@ -113,7 +152,8 @@ exports[`renders ./components/cascader/demo/size.vue correctly 1`] = `
    Please select -
    +

    @@ -123,7 +163,8 @@ exports[`renders ./components/cascader/demo/size.vue correctly 1`] = `
    Please select -
    +

    @@ -133,7 +174,8 @@ exports[`renders ./components/cascader/demo/size.vue correctly 1`] = `
    Please select -
    +

    @@ -148,7 +190,8 @@ exports[`renders ./components/cascader/demo/suffix.vue correctly 1`] = `
    Please select -
    +
    @@ -159,7 +202,7 @@ exports[`renders ./components/cascader/demo/suffix.vue correctly 1`] = `
    Please select -
    +
    diff --git a/components/cascader/demo/basic.vue b/components/cascader/demo/basic.vue index 1f1f77ff5a..8ff8a45892 100644 --- a/components/cascader/demo/basic.vue +++ b/components/cascader/demo/basic.vue @@ -18,8 +18,8 @@ Cascade selection box for selecting province/city/district. - diff --git a/components/cascader/demo/change-on-select.vue b/components/cascader/demo/change-on-select.vue index 2cfd5ad572..8d250951e0 100644 --- a/components/cascader/demo/change-on-select.vue +++ b/components/cascader/demo/change-on-select.vue @@ -23,8 +23,9 @@ Allow only select parent options. change-on-select /> - diff --git a/components/cascader/demo/custom-render.vue b/components/cascader/demo/custom-render.vue index 8a52a0d00d..b19fd7be1d 100644 --- a/components/cascader/demo/custom-render.vue +++ b/components/cascader/demo/custom-render.vue @@ -36,8 +36,9 @@ For instance, add an external link after the selected value. - diff --git a/components/cascader/demo/custom-trigger.vue b/components/cascader/demo/custom-trigger.vue index 06220cc43f..13aafa44e5 100644 --- a/components/cascader/demo/custom-trigger.vue +++ b/components/cascader/demo/custom-trigger.vue @@ -28,8 +28,8 @@ Separate trigger button and result. - diff --git a/components/cascader/demo/disabled-option.vue b/components/cascader/demo/disabled-option.vue index cfe603238e..7107600014 100644 --- a/components/cascader/demo/disabled-option.vue +++ b/components/cascader/demo/disabled-option.vue @@ -18,8 +18,8 @@ Disable option by specifying the `disabled` property in `options`. - diff --git a/components/cascader/demo/fields-name.vue b/components/cascader/demo/fields-name.vue index 7b3ffaa670..63c4d1f534 100644 --- a/components/cascader/demo/fields-name.vue +++ b/components/cascader/demo/fields-name.vue @@ -23,8 +23,8 @@ Custom Field Names placeholder="Please select" /> - diff --git a/components/cascader/demo/hover.vue b/components/cascader/demo/hover.vue index 2e59ed0b6f..452e312e7c 100644 --- a/components/cascader/demo/hover.vue +++ b/components/cascader/demo/hover.vue @@ -23,8 +23,8 @@ Hover to expand sub menu, click to select option. placeholder="Please select" /> - diff --git a/components/cascader/demo/lazy.vue b/components/cascader/demo/lazy.vue index daa36e205d..c2dd7d4a5e 100644 --- a/components/cascader/demo/lazy.vue +++ b/components/cascader/demo/lazy.vue @@ -26,51 +26,43 @@ Load options lazily with `loadData`. change-on-select /> - diff --git a/components/cascader/demo/multiple.vue b/components/cascader/demo/multiple.vue index eb1f82c542..8164155680 100644 --- a/components/cascader/demo/multiple.vue +++ b/components/cascader/demo/multiple.vue @@ -16,18 +16,32 @@ title: Select multiple options - diff --git a/components/cascader/demo/search.vue b/components/cascader/demo/search.vue index 778a3c426f..4832ef4095 100644 --- a/components/cascader/demo/search.vue +++ b/components/cascader/demo/search.vue @@ -25,8 +25,8 @@ Search and select options directly. placeholder="Please select" /> - diff --git a/components/cascader/demo/size.vue b/components/cascader/demo/size.vue index e72b69a748..ca4db878a9 100644 --- a/components/cascader/demo/size.vue +++ b/components/cascader/demo/size.vue @@ -26,8 +26,8 @@ Cascade selection box of different sizes.

    - diff --git a/components/cascader/demo/suffix.vue b/components/cascader/demo/suffix.vue index e486dd870e..f166ab2cdf 100644 --- a/components/cascader/demo/suffix.vue +++ b/components/cascader/demo/suffix.vue @@ -34,9 +34,9 @@ Custom suffix icon /> - diff --git a/components/cascader/demo/tagRender.vue b/components/cascader/demo/tagRender.vue index 3052c34459..e2c32f8768 100644 --- a/components/cascader/demo/tagRender.vue +++ b/components/cascader/demo/tagRender.vue @@ -21,8 +21,8 @@ title: - diff --git a/components/cascader/index.en-US.md b/components/cascader/index.en-US.md index 25a3415ccd..9ac50cd4a6 100644 --- a/components/cascader/index.en-US.md +++ b/components/cascader/index.en-US.md @@ -2,7 +2,8 @@ category: Components type: Data Entry title: Cascader -cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*tokLTp73TsQAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*5-ArSLl5UBsAAAAAAAAAAAAADrJ8AQ/original --- Cascade selection box. @@ -28,7 +29,7 @@ Cascade selection box. | changeOnSelect | (Work on single select) change value on each selection if set to true, see above demo for details | boolean | false | | | disabled | whether disabled select | boolean | false | | | displayRender | render function of displaying selected options, you can use #displayRender="{labels, selectedOptions}". | `({labels, selectedOptions}) => VNode` | `labels => labels.join(' / ')` | | -| dropdownClassName | additional className of popup overlay | string | - | 3.0 | +| popupClassName | additional className of popup overlay | string | - | 4.0 | | dropdownStyle | additional style of popup overlay | CSSProperties | {} | 3.0 | | expandIcon | Customize the current item expand icon | slot | - | 3.0 | | expandTrigger | expand current item when click or hover | `click` \| `hover` | 'click' | | @@ -47,7 +48,9 @@ Cascade selection box. | searchValue | Set search value,Need work with `showSearch` | string | - | 3.0 | | showSearch | Whether show search input in single mode. | boolean \| [object](#showsearch) | false | | | size | input size | `large` \| `default` \| `small` | `default` | | +| status | Set validation status | 'error' \| 'warning' | - | 3.3.0 | | suffixIcon | The custom suffix icon | string \| VNode \| slot | - | | +| showCheckedStrategy | The way show selected item in box. ** `SHOW_CHILD`: ** just show child treeNode. **`Cascader.SHOW_PARENT`:** just show parent treeNode (when all child treeNode under the parent treeNode are checked) | `Cascader.SHOW_PARENT` \| `Cascader.SHOW_CHILD` | `Cascader.SHOW_PARENT` | 3.3.0 | | tagRender | Customize tag render when `multiple` | slot | - | 3.0 | | value(v-model) | selected value | string\[] \| number\[] | - | | diff --git a/components/cascader/index.tsx b/components/cascader/index.tsx index 9328736c8b..e79a675cf5 100644 --- a/components/cascader/index.tsx +++ b/components/cascader/index.tsx @@ -1,5 +1,9 @@ import type { ShowSearchType, FieldNames, BaseOptionType, DefaultOptionType } from '../vc-cascader'; -import VcCascader, { cascaderProps as vcCascaderProps } from '../vc-cascader'; +import VcCascader, { + cascaderProps as vcCascaderProps, + SHOW_CHILD, + SHOW_PARENT, +} from '../vc-cascader'; import RightOutlined from '@ant-design/icons-vue/RightOutlined'; import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; import LeftOutlined from '@ant-design/icons-vue/LeftOutlined'; @@ -11,7 +15,7 @@ import { computed, defineComponent, ref, watchEffect } from 'vue'; import type { ExtractPropTypes, PropType } from 'vue'; import PropTypes from '../_util/vue-types'; import { initDefaultProps } from '../_util/props-util'; -import useConfigInject from '../_util/hooks/useConfigInject'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; import classNames from '../_util/classNames'; import type { SizeType } from '../config-provider'; import devWarning from '../vc-util/devWarning'; @@ -19,7 +23,14 @@ import type { SelectCommonPlacement } from '../_util/transition'; import { getTransitionDirection, getTransitionName } from '../_util/transition'; import { useInjectFormItemContext } from '../form'; import type { ValueType } from '../vc-cascader/Cascader'; +import type { InputStatus } from '../_util/statusUtils'; +import { getStatusClassNames, getMergedStatus } from '../_util/statusUtils'; +import { FormItemInputContext } from '../form/FormItemContext'; +import { useCompactItemContext } from '../space/Compact'; +import useSelectStyle from '../select/style'; +import useStyle from './style'; +import { useInjectDisabled } from '../config-provider/DisabledContext'; // Align the design since we use `rc-select` in root. This help: // - List search content will show all content // - Hover opacity style @@ -31,7 +42,7 @@ export type FieldNamesType = FieldNames; export type FilledFieldNamesType = Required; -function highlightKeyword(str: string, lowerKeyword: string, prefixCls: string | undefined) { +function highlightKeyword(str: string, lowerKeyword: string, prefixCls?: string) { const cells = str .toLowerCase() .split(lowerKeyword) @@ -99,7 +110,11 @@ export function cascaderProps }, suffixIcon: PropTypes.any, + status: String as PropType, options: Array as PropType, + popupClassName: String, + /** @deprecated Please use `popupClassName` instead */ + dropdownClassName: String, 'onUpdate:value': Function as PropType<(value: ValueType) => void>, }; } @@ -121,7 +136,17 @@ const Cascader = defineComponent({ allowClear: true, }), setup(props, { attrs, expose, slots, emit }) { + // ====================== Warning ====================== + if (process.env.NODE_ENV !== 'production') { + devWarning( + !props.dropdownClassName, + 'Cascader', + '`dropdownClassName` is deprecated. Please use `popupClassName` instead.', + ); + } const formItemContext = useInjectFormItemContext(); + const formItemInputContext = FormItemInputContext.useInject(); + const mergedStatus = computed(() => getMergedStatus(formItemInputContext.status, props.status)); const { prefixCls: cascaderPrefixCls, rootPrefixCls, @@ -129,9 +154,18 @@ const Cascader = defineComponent({ direction, getPopupContainer, renderEmpty, - size, + size: contextSize, + disabled, } = useConfigInject('cascader', props); const prefixCls = computed(() => getPrefixCls('select', props.prefixCls)); + const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction); + const mergedSize = computed(() => compactSize.value || contextSize.value); + const contextDisabled = useInjectDisabled(); + const mergedDisabled = computed(() => disabled.value ?? contextDisabled.value); + + const [wrapSelectSSR, hashId] = useSelectStyle(prefixCls); + const [wrapCascaderSSR] = useStyle(cascaderPrefixCls); + const isRtl = computed(() => direction.value === 'rtl'); // =================== Warning ===================== if (process.env.NODE_ENV !== 'production') { @@ -166,11 +200,12 @@ const Cascader = defineComponent({ // =================== Dropdown ==================== const mergedDropdownClassName = computed(() => classNames( - props.dropdownClassName || props.popupClassName, + props.popupClassName || props.dropdownClassName, `${cascaderPrefixCls.value}-dropdown`, { [`${cascaderPrefixCls.value}-dropdown-rtl`]: isRtl.value, }, + hashId.value, ), ); @@ -217,7 +252,7 @@ const Cascader = defineComponent({ ...restProps } = props; // =================== No Found ==================== - const mergedNotFoundContent = notFoundContent || renderEmpty.value('Cascader'); + const mergedNotFoundContent = notFoundContent || renderEmpty('Cascader'); // ===================== Icon ====================== let mergedExpandIcon = expandIcon; @@ -235,64 +270,86 @@ const Cascader = defineComponent({ const { suffixIcon, removeIcon, clearIcon } = getIcons( { ...props, + hasFeedback: formItemInputContext.hasFeedback, + feedbackIcon: formItemInputContext.feedbackIcon, multiple, prefixCls: prefixCls.value, showArrow: mergedShowArrow.value, }, slots, ); - return ( - , - }} - tagRender={props.tagRender || slots.tagRender} - displayRender={props.displayRender || slots.displayRender} - maxTagPlaceholder={props.maxTagPlaceholder || slots.maxTagPlaceholder} - showArrow={props.showArrow} - onChange={handleChange} - onBlur={handleBlur} - v-slots={slots} - ref={selectRef} - /> + return wrapCascaderSSR( + wrapSelectSSR( + , + }} + tagRender={props.tagRender || slots.tagRender} + displayRender={props.displayRender || slots.displayRender} + maxTagPlaceholder={props.maxTagPlaceholder || slots.maxTagPlaceholder} + showArrow={formItemInputContext.hasFeedback || props.showArrow} + onChange={handleChange} + onBlur={handleBlur} + v-slots={slots} + ref={selectRef} + />, + ), ); }; }, }); - -export default withInstall(Cascader); +export default withInstall< + typeof Cascader & { + SHOW_PARENT: typeof SHOW_PARENT; + SHOW_CHILD: typeof SHOW_CHILD; + } +>( + Object.assign(Cascader, { + SHOW_CHILD, + SHOW_PARENT, + } as any), +); diff --git a/components/cascader/index.zh-CN.md b/components/cascader/index.zh-CN.md index d8363fd237..731a51ac60 100644 --- a/components/cascader/index.zh-CN.md +++ b/components/cascader/index.zh-CN.md @@ -3,7 +3,8 @@ category: Components type: 数据录入 title: Cascader subtitle: 级联选择 -cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*tokLTp73TsQAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*5-ArSLl5UBsAAAAAAAAAAAAADrJ8AQ/original --- 级联选择框。 @@ -30,7 +31,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg | defaultValue | 默认的选中项 | string\[] \| number\[] | \[] | | | disabled | 禁用 | boolean | false | | | displayRender | 选择后展示的渲染函数,可使用 #displayRender="{labels, selectedOptions}" | `({labels, selectedOptions}) => VNode` | `labels => labels.join(' / ')` | | -| dropdownClassName | 自定义浮层类名 | string | - | 3.0 | +| popupClassName | 自定义浮层类名 | string | - | 4.0 | | dropdownStyle | 自定义浮层样式 | CSSProperties | {} | 3.0 | | expandIcon | 自定义次级菜单展开图标 | slot | - | 3.0 | | expandTrigger | 次级菜单的展开方式 | `click` \| `hover` | 'click' | | @@ -45,9 +46,11 @@ cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg | options | 可选项数据源 | [Option](#option)\[] | - | | | placeholder | 输入框占位文本 | string | '请选择' | | | placement | 浮层预设位置 | `bottomLeft` \| `bottomRight` \| `topLeft` \| `topRight` | `bottomLeft` | 3.0 | +| showCheckedStrategy | 定义选中项回填的方式。`Cascader.SHOW_CHILD`: 只显示选中的子节点。`Cascader.SHOW_PARENT`: 只显示父节点(当父节点下所有子节点都选中时)。 | `Cascader.SHOW_PARENT` \| `Cascader.SHOW_CHILD` | `Cascader.SHOW_PARENT` | 3.3.0 | | removeIcon | 自定义的多选框清除图标 | slot | - | 3.2 | | searchValue | 设置搜索的值,需要与 `showSearch` 配合使用 | string | - | 3.0 | | showSearch | 在选择框中显示搜索框 | boolean \| [object](#showsearch) | false | | +| status | 设置校验状态 | 'error' \| 'warning' | - | 3.3.0 | | size | 输入框大小 | `large` \| `default` \| `small` | `default` | | | suffixIcon | 自定义的选择框后缀图标 | string \| VNode \| slot | - | | | tagRender | 自定义 tag 内容,多选时生效 | slot | - | 3.0 | diff --git a/components/cascader/style/index.less b/components/cascader/style/index.less deleted file mode 100644 index 8df1d85fc7..0000000000 --- a/components/cascader/style/index.less +++ /dev/null @@ -1,104 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; -@import '../../input/style/mixin'; -@import '../../checkbox/style/mixin'; - -@cascader-prefix-cls: ~'@{ant-prefix}-cascader'; - -.antCheckboxFn(@checkbox-prefix-cls: ~'@{cascader-prefix-cls}-checkbox'); - -.@{cascader-prefix-cls} { - width: 184px; - - &-checkbox { - top: 0; - margin-right: @padding-xs; - } - - &-menus { - display: flex; - flex-wrap: nowrap; - align-items: flex-start; - - &.@{cascader-prefix-cls}-menu-empty { - .@{cascader-prefix-cls}-menu { - width: 100%; - height: auto; - } - } - } - - &-menu { - min-width: 111px; - height: 180px; - margin: 0; - margin: -@dropdown-edge-child-vertical-padding 0; - padding: @cascader-dropdown-edge-child-vertical-padding 0; - overflow: auto; - vertical-align: top; - list-style: none; - border-right: @border-width-base @border-style-base @cascader-menu-border-color-split; - -ms-overflow-style: -ms-autohiding-scrollbar; // https://github.com/ant-design/ant-design/issues/11857 - - &-item { - display: flex; - flex-wrap: nowrap; - align-items: center; - padding: @cascader-dropdown-vertical-padding @control-padding-horizontal; - overflow: hidden; - line-height: @cascader-dropdown-line-height; - white-space: nowrap; - text-overflow: ellipsis; - cursor: pointer; - transition: all 0.3s; - - &:hover { - background: @item-hover-bg; - } - - &-disabled { - color: @disabled-color; - cursor: not-allowed; - - &:hover { - background: transparent; - } - } - - .@{cascader-prefix-cls}-menu-empty & { - color: @disabled-color; - cursor: default; - pointer-events: none; - } - - &-active:not(&-disabled) { - &, - &:hover { - font-weight: @select-item-selected-font-weight; - background-color: @cascader-item-selected-bg; - } - } - - &-content { - flex: auto; - } - - &-expand &-expand-icon, - &-loading-icon { - margin-left: @padding-xss; - color: @text-color-secondary; - font-size: 10px; - - .@{cascader-prefix-cls}-menu-item-disabled& { - color: @disabled-color; - } - } - - &-keyword { - color: @highlight-color; - } - } - } -} - -@import './rtl'; diff --git a/components/cascader/style/index.ts b/components/cascader/style/index.ts new file mode 100644 index 0000000000..07e6417ae2 --- /dev/null +++ b/components/cascader/style/index.ts @@ -0,0 +1,165 @@ +import { getStyle as getCheckboxStyle } from '../../checkbox/style'; +import type { FullToken, GenerateStyle } from '../../theme/internal'; +import { genComponentStyleHook } from '../../theme/internal'; +import { textEllipsis } from '../../style'; +import { genCompactItemStyle } from '../../style/compact-item'; + +export interface ComponentToken { + controlWidth: number; + controlItemWidth: number; + dropdownHeight: number; +} + +type CascaderToken = FullToken<'Cascader'>; + +// =============================== Base =============================== +const genBaseStyle: GenerateStyle = token => { + const { prefixCls, componentCls, antCls } = token; + const cascaderMenuItemCls = `${componentCls}-menu-item`; + const iconCls = ` + &${cascaderMenuItemCls}-expand ${cascaderMenuItemCls}-expand-icon, + ${cascaderMenuItemCls}-loading-icon + `; + + const itemPaddingVertical = Math.round( + (token.controlHeight - token.fontSize * token.lineHeight) / 2, + ); + + return [ + // ===================================================== + // == Control == + // ===================================================== + { + [componentCls]: { + width: token.controlWidth, + }, + }, + // ===================================================== + // == Popup == + // ===================================================== + { + [`${componentCls}-dropdown`]: [ + // ==================== Checkbox ==================== + getCheckboxStyle(`${prefixCls}-checkbox`, token), + { + [`&${antCls}-select-dropdown`]: { + padding: 0, + }, + }, + { + [componentCls]: { + // ================== Checkbox ================== + '&-checkbox': { + top: 0, + marginInlineEnd: token.paddingXS, + }, + + // ==================== Menu ==================== + // >>> Menus + '&-menus': { + display: 'flex', + flexWrap: 'nowrap', + alignItems: 'flex-start', + + [`&${componentCls}-menu-empty`]: { + [`${componentCls}-menu`]: { + width: '100%', + height: 'auto', + + [cascaderMenuItemCls]: { + color: token.colorTextDisabled, + }, + }, + }, + }, + + // >>> Menu + '&-menu': { + flexGrow: 1, + minWidth: token.controlItemWidth, + height: token.dropdownHeight, + margin: 0, + padding: token.paddingXXS, + overflow: 'auto', + verticalAlign: 'top', + listStyle: 'none', + '-ms-overflow-style': '-ms-autohiding-scrollbar', // https://github.com/ant-design/ant-design/issues/11857 + + '&:not(:last-child)': { + borderInlineEnd: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`, + }, + + '&-item': { + ...textEllipsis, + display: 'flex', + flexWrap: 'nowrap', + alignItems: 'center', + padding: `${itemPaddingVertical}px ${token.paddingSM}px`, + lineHeight: token.lineHeight, + cursor: 'pointer', + transition: `all ${token.motionDurationMid}`, + borderRadius: token.borderRadiusSM, + + '&:hover': { + background: token.controlItemBgHover, + }, + '&-disabled': { + color: token.colorTextDisabled, + cursor: 'not-allowed', + + '&:hover': { + background: 'transparent', + }, + + [iconCls]: { + color: token.colorTextDisabled, + }, + }, + + [`&-active:not(${cascaderMenuItemCls}-disabled)`]: { + [`&, &:hover`]: { + fontWeight: token.fontWeightStrong, + backgroundColor: token.controlItemBgActive, + }, + }, + + '&-content': { + flex: 'auto', + }, + + [iconCls]: { + marginInlineStart: token.paddingXXS, + color: token.colorTextDescription, + fontSize: token.fontSizeIcon, + }, + + '&-keyword': { + color: token.colorHighlight, + }, + }, + }, + }, + }, + ], + }, + // ===================================================== + // == RTL == + // ===================================================== + { + [`${componentCls}-dropdown-rtl`]: { + direction: 'rtl', + }, + }, + // ===================================================== + // == Space Compact == + // ===================================================== + genCompactItemStyle(token), + ]; +}; + +// ============================== Export ============================== +export default genComponentStyleHook('Cascader', token => [genBaseStyle(token)], { + controlWidth: 184, + controlItemWidth: 111, + dropdownHeight: 180, +}); diff --git a/components/cascader/style/index.tsx b/components/cascader/style/index.tsx deleted file mode 100644 index e07deeea0a..0000000000 --- a/components/cascader/style/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import '../../style/index.less'; -import './index.less'; - -// style dependencies -import '../../empty/style'; -import '../../select/style'; diff --git a/components/cascader/style/rtl.less b/components/cascader/style/rtl.less deleted file mode 100644 index c70bf1dd3a..0000000000 --- a/components/cascader/style/rtl.less +++ /dev/null @@ -1,19 +0,0 @@ -// We can not import reference of `./index` directly since it will make dead loop in less -@import (reference) '../../style/themes/index'; -@cascader-prefix-cls: ~'@{ant-prefix}-cascader'; - -.@{cascader-prefix-cls}-rtl { - .@{cascader-prefix-cls}-menu-item { - &-expand-icon, - &-loading-icon { - margin-right: @padding-xss; - margin-left: 0; - } - } - - .@{cascader-prefix-cls}-checkbox { - top: 0; - margin-right: 0; - margin-left: @padding-xs; - } -} diff --git a/components/checkbox/Checkbox.tsx b/components/checkbox/Checkbox.tsx index 3c6f62f181..a52109a5c2 100644 --- a/components/checkbox/Checkbox.tsx +++ b/components/checkbox/Checkbox.tsx @@ -1,16 +1,27 @@ import type { CSSProperties } from 'vue'; -import { watchEffect, onMounted, defineComponent, inject, onBeforeUnmount, ref } from 'vue'; +import { + computed, + watchEffect, + onMounted, + defineComponent, + inject, + onBeforeUnmount, + ref, +} from 'vue'; import classNames from '../_util/classNames'; import VcCheckbox from '../vc-checkbox/Checkbox'; import { flattenChildren } from '../_util/props-util'; import warning from '../_util/warning'; import type { EventHandler } from '../_util/EventInterface'; -import { useInjectFormItemContext } from '../form/FormItemContext'; -import useConfigInject from '../_util/hooks/useConfigInject'; +import { FormItemInputContext, useInjectFormItemContext } from '../form/FormItemContext'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; import type { CheckboxChangeEvent, CheckboxProps } from './interface'; import { CheckboxGroupContextKey, checkboxProps } from './interface'; +// CSSINJS +import useStyle from './style'; + export default defineComponent({ compatConfig: { MODE: 3 }, name: 'ACheckbox', @@ -20,10 +31,16 @@ export default defineComponent({ // emits: ['change', 'update:checked'], setup(props, { emit, attrs, slots, expose }) { const formItemContext = useInjectFormItemContext(); - const { prefixCls, direction } = useConfigInject('checkbox', props); + const formItemInputContext = FormItemInputContext.useInject(); + const { prefixCls, direction, disabled } = useConfigInject('checkbox', props); + // style + const [wrapSSR, hashId] = useStyle(prefixCls); + const checkboxGroup = inject(CheckboxGroupContextKey, undefined); const uniId = Symbol('checkboxUniId'); - + const mergedDisabled = computed(() => { + return checkboxGroup?.disabled.value || disabled.value; + }); watchEffect(() => { if (!props.skipGroup && checkboxGroup) { checkboxGroup.registerValue(uniId, props.value); @@ -36,7 +53,7 @@ export default defineComponent({ }); onMounted(() => { warning( - props.checked !== undefined || checkboxGroup || props.value === undefined, + !!(props.checked !== undefined || checkboxGroup || props.value === undefined), 'Checkbox', '`value` is not validate prop, do you mean `checked`?', ); @@ -67,6 +84,7 @@ export default defineComponent({ id, prefixCls: prefixCls.value, ...restAttrs, + disabled: mergedDisabled.value, }; if (checkboxGroup && !skipGroup) { checkboxProps.onChange = (...args) => { @@ -74,8 +92,8 @@ export default defineComponent({ checkboxGroup.toggleOption({ label: children, value: props.value }); }; checkboxProps.name = checkboxGroup.name.value; - checkboxProps.checked = checkboxGroup.mergedValue.value.indexOf(props.value) !== -1; - checkboxProps.disabled = props.disabled || checkboxGroup.disabled.value; + checkboxProps.checked = checkboxGroup.mergedValue.value.includes(props.value); + checkboxProps.disabled = mergedDisabled.value || checkboxGroup.disabled.value; checkboxProps.indeterminate = indeterminate; } else { checkboxProps.onChange = handleChange; @@ -86,22 +104,34 @@ export default defineComponent({ [`${prefixCls.value}-rtl`]: direction.value === 'rtl', [`${prefixCls.value}-wrapper-checked`]: checkboxProps.checked, [`${prefixCls.value}-wrapper-disabled`]: checkboxProps.disabled, + [`${prefixCls.value}-wrapper-in-form-item`]: formItemInputContext.isFormItemInput, }, className, + hashId.value, + ); + const checkboxClass = classNames( + { + [`${prefixCls.value}-indeterminate`]: indeterminate, + }, + hashId.value, ); - const checkboxClass = classNames({ - [`${prefixCls.value}-indeterminate`]: indeterminate, - }); - return ( + const ariaChecked = indeterminate ? 'mixed' : undefined; + return wrapSSR( + , ); }; }, diff --git a/components/checkbox/Group.tsx b/components/checkbox/Group.tsx index d096fd8ce5..57fb871ba2 100644 --- a/components/checkbox/Group.tsx +++ b/components/checkbox/Group.tsx @@ -1,18 +1,27 @@ import { computed, ref, watch, defineComponent, provide } from 'vue'; import Checkbox from './Checkbox'; import { useInjectFormItemContext } from '../form/FormItemContext'; -import useConfigInject from '../_util/hooks/useConfigInject'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; import type { CheckboxOptionType } from './interface'; import { CheckboxGroupContextKey, checkboxGroupProps } from './interface'; +// CSSINJS +import useStyle from './style'; + export default defineComponent({ compatConfig: { MODE: 3 }, name: 'ACheckboxGroup', + inheritAttrs: false, props: checkboxGroupProps(), // emits: ['change', 'update:value'], - setup(props, { slots, emit, expose }) { + setup(props, { slots, attrs, emit, expose }) { const formItemContext = useInjectFormItemContext(); const { prefixCls, direction } = useConfigInject('checkbox', props); + const groupPrefixCls = computed(() => `${prefixCls.value}-group`); + + // style + const [wrapSSR, hashId] = useStyle(groupPrefixCls); + const mergedValue = ref((props.value === undefined ? props.defaultValue : props.value) || []); watch( () => props.value, @@ -87,7 +96,6 @@ export default defineComponent({ return () => { const { id = formItemContext.id.value } = props; let children = null; - const groupPrefixCls = `${prefixCls.value}-group`; if (options.value && options.value.length > 0) { children = options.value.map(option => ( - {option.label === undefined ? slots.label?.(option) : option.label} + {slots.label !== undefined ? slots.label?.(option) : option.label} )); } - return ( + return wrapSSR(
    {children || slots.default?.()} -
    +
    , ); }; }, diff --git a/components/checkbox/__tests__/__snapshots__/demo.test.js.snap b/components/checkbox/__tests__/__snapshots__/demo.test.js.snap index 01ce052f82..55fb8740d8 100644 --- a/components/checkbox/__tests__/__snapshots__/demo.test.js.snap +++ b/components/checkbox/__tests__/__snapshots__/demo.test.js.snap @@ -3,7 +3,7 @@ exports[`renders ./components/checkbox/demo/basic.vue correctly 1`] = ``; exports[`renders ./components/checkbox/demo/check-all.vue correctly 1`] = ` -
    +
    @@ -39,11 +39,11 @@ exports[`renders ./components/checkbox/demo/group.vue correctly 1`] = `


    -
    +
    `; exports[`renders ./components/checkbox/demo/layout.vue correctly 1`] = ` -
    +
    diff --git a/components/checkbox/__tests__/checkbox.test.js b/components/checkbox/__tests__/checkbox.test.js index 0dc53d1655..904bc0478f 100644 --- a/components/checkbox/__tests__/checkbox.test.js +++ b/components/checkbox/__tests__/checkbox.test.js @@ -34,7 +34,7 @@ describe('Checkbox', () => { }, }); expect(errorSpy).toHaveBeenCalledWith( - 'Warning: [antdv: Checkbox] `value` is not validate prop, do you mean `checked`?', + 'Warning: [ant-design-vue: Checkbox] `value` is not validate prop, do you mean `checked`?', ); errorSpy.mockRestore(); }); diff --git a/components/checkbox/demo/basic.vue b/components/checkbox/demo/basic.vue index d1057f6f67..4840d5ec61 100644 --- a/components/checkbox/demo/basic.vue +++ b/components/checkbox/demo/basic.vue @@ -19,13 +19,7 @@ Basic usage of checkbox - diff --git a/components/checkbox/demo/check-all.vue b/components/checkbox/demo/check-all.vue index 93e3fe9897..a0be3c8b29 100644 --- a/components/checkbox/demo/check-all.vue +++ b/components/checkbox/demo/check-all.vue @@ -19,46 +19,36 @@ The `indeterminate` property can help you to achieve a 'check all' effect. - diff --git a/components/checkbox/demo/controller.vue b/components/checkbox/demo/controller.vue index e2e6833b11..5ebf14d80b 100644 --- a/components/checkbox/demo/controller.vue +++ b/components/checkbox/demo/controller.vue @@ -31,34 +31,21 @@ Communicated with other components

    - diff --git a/components/checkbox/demo/disabled.vue b/components/checkbox/demo/disabled.vue index 6586d9f36f..025f337480 100644 --- a/components/checkbox/demo/disabled.vue +++ b/components/checkbox/demo/disabled.vue @@ -17,18 +17,14 @@ Disabled checkbox - diff --git a/components/checkbox/demo/group.vue b/components/checkbox/demo/group.vue index e2169312aa..fe737b83b0 100644 --- a/components/checkbox/demo/group.vue +++ b/components/checkbox/demo/group.vue @@ -17,23 +17,23 @@ Generate a group of checkboxes from an array - diff --git a/components/collapse/demo/ghost.vue b/components/collapse/demo/ghost.vue index 5390d237af..087813bb10 100644 --- a/components/collapse/demo/ghost.vue +++ b/components/collapse/demo/ghost.vue @@ -29,22 +29,12 @@ Making collapse's background to transparent. - diff --git a/components/collapse/demo/index.vue b/components/collapse/demo/index.vue index 6b4afef1c5..bf6158e16d 100644 --- a/components/collapse/demo/index.vue +++ b/components/collapse/demo/index.vue @@ -8,6 +8,7 @@ + @@ -20,6 +21,7 @@ import Mix from './mix.vue'; import Noarrow from './noarrow.vue'; import Extra from './extra.vue'; import Ghost from './ghost.vue'; +import Collapsible from './collapsible.vue'; import CN from '../index.zh-CN.md'; import US from '../index.en-US.md'; @@ -35,6 +37,7 @@ export default { Noarrow, Extra, Ghost, + Collapsible, }, }; diff --git a/components/collapse/demo/mix.vue b/components/collapse/demo/mix.vue index afba125dff..5286d4ca44 100644 --- a/components/collapse/demo/mix.vue +++ b/components/collapse/demo/mix.vue @@ -33,23 +33,13 @@ title: - diff --git a/components/collapse/demo/noarrow.vue b/components/collapse/demo/noarrow.vue index 1d0ad74b4e..85c287925e 100644 --- a/components/collapse/demo/noarrow.vue +++ b/components/collapse/demo/noarrow.vue @@ -25,22 +25,12 @@ You can hide the arrow icon by passing `showArrow={false}` to `CollapsePanel` co - diff --git a/components/collapse/index.en-US.md b/components/collapse/index.en-US.md index 5c4c1f1137..4a80753b00 100644 --- a/components/collapse/index.en-US.md +++ b/components/collapse/index.en-US.md @@ -2,7 +2,8 @@ category: Components type: Data Display title: Collapse -cover: https://gw.alipayobjects.com/zos/alicdn/IxH16B9RD/Collapse.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*B7HKR5OBe8gAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*sir-TK0HkWcAAAAAAAAAAAAADrJ8AQ/original --- A content area which can be collapsed and expanded. @@ -19,12 +20,12 @@ A content area which can be collapsed and expanded. | Property | Description | Type | Default | Version | | --- | --- | --- | --- | --- | | accordion | If `true`, `Collapse` renders as `Accordion` | boolean | `false` | | -| activeKey(v-model) | Key of the active panel | string\[]\|string | No default value. In `accordion` mode, it's the key of the first panel. | | +| activeKey(v-model) | Key of the active panel | string\[] \| string
    number\[] \| number | No default value. In `accordion` mode, it's the key of the first panel. | | | bordered | Toggles rendering of the border around the collapse block | boolean | `true` | | -| collapsible | Specify whether the panels of children be collapsible or the trigger area of collapsible | `header` \| `disabled` | - | 3.0 | +| collapsible | Specify whether the panels of children be collapsible or the trigger area of collapsible | `header` \| `icon` \| `disabled` | - | 4.0 | | destroyInactivePanel | Destroy Inactive Panel | boolean | `false` | | | expandIcon | allow to customize collapse icon | Function(props):VNode \| v-slot:expandIcon="props" | | | -| expandIconPosition | Set expand icon position: `left`, `right` | `left` | - | 1.5.0 | +| expandIconPosition | Set expand icon position | `start` \| `end` | - | 4.0 | | ghost | Make the collapse borderless and its background transparent | boolean | false | 3.0 | ### events diff --git a/components/collapse/index.zh-CN.md b/components/collapse/index.zh-CN.md index 9216f00189..1c750c85f5 100644 --- a/components/collapse/index.zh-CN.md +++ b/components/collapse/index.zh-CN.md @@ -3,7 +3,8 @@ category: Components type: 数据展示 title: Collapse subtitle: 折叠面板 -cover: https://gw.alipayobjects.com/zos/alicdn/IxH16B9RD/Collapse.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*B7HKR5OBe8gAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*sir-TK0HkWcAAAAAAAAAAAAADrJ8AQ/original --- 可以折叠/展开的内容区域。 @@ -20,12 +21,12 @@ cover: https://gw.alipayobjects.com/zos/alicdn/IxH16B9RD/Collapse.svg | 参数 | 说明 | 类型 | 默认值 | 版本 | | --- | --- | --- | --- | --- | | accordion | 手风琴模式 | boolean | `false` | | -| activeKey(v-model) | 当前激活 tab 面板的 key | string\[]\|string | 默认无,accordion 模式下默认第一个元素 | | +| activeKey(v-model) | 当前激活 tab 面板的 key | string\[] \| string
    number\[] \| number | 默认无,accordion 模式下默认第一个元素 | | | bordered | 带边框风格的折叠面板 | boolean | `true` | | -| collapsible | 所有子面板是否可折叠或指定可折叠触发区域 | `header` \| `disabled` | - | 3.0 | +| collapsible | 所有子面板是否可折叠或指定可折叠触发区域 | `header` \| `icon` \| `disabled` | - | 4.0 | | destroyInactivePanel | 销毁折叠隐藏的面板 | boolean | `false` | | | expandIcon | 自定义切换图标 | Function(props):VNode \| slot="expandIcon" slot-scope="props"\|#expandIcon="props" | | | -| expandIconPosition | 设置图标位置: `left`, `right` | `left` | - | 1.5.0 | +| expandIconPosition | 设置图标位置 | `start` \| `end` | - | 4.0 | | ghost | 使折叠面板透明且无边框 | boolean | false | 3.0 | ### 事件 diff --git a/components/collapse/style/index.less b/components/collapse/style/index.less deleted file mode 100644 index 6b10c990b8..0000000000 --- a/components/collapse/style/index.less +++ /dev/null @@ -1,157 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@collapse-prefix-cls: ~'@{ant-prefix}-collapse'; - -.@{collapse-prefix-cls} { - .reset-component(); - - background-color: @collapse-header-bg; - border: @border-width-base @border-style-base @border-color-base; - border-bottom: 0; - border-radius: @collapse-panel-border-radius; - - & > &-item { - border-bottom: @border-width-base @border-style-base @border-color-base; - - &:last-child { - &, - & > .@{collapse-prefix-cls}-header { - border-radius: 0 0 @collapse-panel-border-radius @collapse-panel-border-radius; - } - } - - > .@{collapse-prefix-cls}-header { - position: relative; // Compatible with old version of antd, should remove in next version - display: flex; - flex-wrap: nowrap; - align-items: flex-start; - padding: @collapse-header-padding; - color: @heading-color; - line-height: @line-height-base; - cursor: pointer; - transition: all 0.3s, visibility 0s; - - .@{collapse-prefix-cls}-arrow { - display: inline-block; - margin-right: @margin-sm; - font-size: @font-size-sm; - vertical-align: -1px; - - & svg { - transition: transform 0.24s; - } - } - - .@{collapse-prefix-cls}-extra { - margin-left: auto; - } - - &:focus { - outline: none; - } - } - - .@{collapse-prefix-cls}-header-collapsible-only { - cursor: default; - .@{collapse-prefix-cls}-header-text { - cursor: pointer; - } - } - - &.@{collapse-prefix-cls}-no-arrow { - > .@{collapse-prefix-cls}-header { - padding-left: @padding-sm; - } - } - } - - // Expand Icon right - &-icon-position-right { - & > .@{collapse-prefix-cls}-item { - > .@{collapse-prefix-cls}-header { - position: relative; - padding: @collapse-header-padding; - padding-right: @collapse-header-padding-extra; - - .@{collapse-prefix-cls}-arrow { - position: absolute; - top: 50%; - right: @padding-md; - left: auto; - margin: 0; - transform: translateY(-50%); - } - } - } - } - - &-content { - color: @text-color; - background-color: @collapse-content-bg; - border-top: @border-width-base @border-style-base @border-color-base; - - & > &-box { - padding: @collapse-content-padding; - } - - &-hidden { - display: none; - } - } - - &-item:last-child { - > .@{collapse-prefix-cls}-content { - border-radius: 0 0 @collapse-panel-border-radius @collapse-panel-border-radius; - } - } - - &-borderless { - background-color: @collapse-header-bg; - border: 0; - } - - &-borderless > &-item { - border-bottom: 1px solid @border-color-base; - } - - &-borderless > &-item:last-child, - &-borderless > &-item:last-child &-header { - border-radius: 0; - } - - &-borderless > &-item > &-content { - background-color: transparent; - border-top: 0; - } - - &-borderless > &-item > &-content > &-content-box { - padding-top: 4px; - } - - &-ghost { - background-color: transparent; - border: 0; - > .@{collapse-prefix-cls}-item { - border-bottom: 0; - > .@{collapse-prefix-cls}-content { - background-color: transparent; - border-top: 0; - > .@{collapse-prefix-cls}-content-box { - padding-top: 12px; - padding-bottom: 12px; - } - } - } - } - - & &-item-disabled > &-header { - &, - & > .arrow { - color: @disabled-color; - cursor: not-allowed; - } - } -} - -@import './rtl'; diff --git a/components/collapse/style/index.tsx b/components/collapse/style/index.tsx index 3a3ab0de59..3d9b433026 100644 --- a/components/collapse/style/index.tsx +++ b/components/collapse/style/index.tsx @@ -1,2 +1,271 @@ -import '../../style/index.less'; -import './index.less'; +import { genCollapseMotion } from '../../style/motion'; +import type { FullToken, GenerateStyle } from '../../theme/internal'; +import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import { resetComponent, resetIcon } from '../../style'; + +export interface ComponentToken {} + +type CollapseToken = FullToken<'Collapse'> & { + collapseContentBg: string; + collapseHeaderBg: string; + collapseHeaderPadding: string; + collapsePanelBorderRadius: number; + collapseContentPaddingHorizontal: number; +}; + +export const genBaseStyle: GenerateStyle = token => { + const { + componentCls, + collapseContentBg, + padding, + collapseContentPaddingHorizontal, + collapseHeaderBg, + collapseHeaderPadding, + collapsePanelBorderRadius, + + lineWidth, + lineType, + colorBorder, + colorText, + colorTextHeading, + colorTextDisabled, + fontSize, + lineHeight, + marginSM, + paddingSM, + motionDurationSlow, + fontSizeIcon, + } = token; + + const borderBase = `${lineWidth}px ${lineType} ${colorBorder}`; + + return { + [componentCls]: { + ...resetComponent(token), + backgroundColor: collapseHeaderBg, + border: borderBase, + borderBottom: 0, + borderRadius: `${collapsePanelBorderRadius}px`, + + [`&-rtl`]: { + direction: 'rtl', + }, + + [`& > ${componentCls}-item`]: { + borderBottom: borderBase, + [`&:last-child`]: { + [` + &, + & > ${componentCls}-header`]: { + borderRadius: `0 0 ${collapsePanelBorderRadius}px ${collapsePanelBorderRadius}px`, + }, + }, + + [`> ${componentCls}-header`]: { + position: 'relative', // Compatible with old version of antd, should remove in next version + display: 'flex', + flexWrap: 'nowrap', + alignItems: 'flex-start', + padding: collapseHeaderPadding, + color: colorTextHeading, + lineHeight, + cursor: 'pointer', + transition: `all ${motionDurationSlow}, visibility 0s`, + + [`> ${componentCls}-header-text`]: { + flex: 'auto', + }, + + '&:focus': { + outline: 'none', + }, + + // >>>>> Arrow + [`${componentCls}-expand-icon`]: { + height: fontSize * lineHeight, + display: 'flex', + alignItems: 'center', + paddingInlineEnd: marginSM, + }, + + [`${componentCls}-arrow`]: { + ...resetIcon(), + fontSize: fontSizeIcon, + + svg: { + transition: `transform ${motionDurationSlow}`, + }, + }, + + // >>>>> Text + [`${componentCls}-header-text`]: { + marginInlineEnd: 'auto', + }, + }, + + [`${componentCls}-header-collapsible-only`]: { + cursor: 'default', + + [`${componentCls}-header-text`]: { + flex: 'none', + cursor: 'pointer', + }, + [`${componentCls}-expand-icon`]: { + cursor: 'pointer', + }, + }, + + [`${componentCls}-icon-collapsible-only`]: { + cursor: 'default', + + [`${componentCls}-expand-icon`]: { + cursor: 'pointer', + }, + }, + + [`&${componentCls}-no-arrow`]: { + [`> ${componentCls}-header`]: { + paddingInlineStart: paddingSM, + }, + }, + }, + + [`${componentCls}-content`]: { + color: colorText, + backgroundColor: collapseContentBg, + borderTop: borderBase, + + [`& > ${componentCls}-content-box`]: { + padding: `${padding}px ${collapseContentPaddingHorizontal}px`, + }, + + [`&-hidden`]: { + display: 'none', + }, + }, + + [`${componentCls}-item:last-child`]: { + [`> ${componentCls}-content`]: { + borderRadius: `0 0 ${collapsePanelBorderRadius}px ${collapsePanelBorderRadius}px`, + }, + }, + + [`& ${componentCls}-item-disabled > ${componentCls}-header`]: { + [` + &, + & > .arrow + `]: { + color: colorTextDisabled, + cursor: 'not-allowed', + }, + }, + + // ========================== Icon Position ========================== + [`&${componentCls}-icon-position-end`]: { + [`& > ${componentCls}-item`]: { + [`> ${componentCls}-header`]: { + [`${componentCls}-expand-icon`]: { + order: 1, + paddingInlineEnd: 0, + paddingInlineStart: marginSM, + }, + }, + }, + }, + }, + }; +}; + +const genArrowStyle: GenerateStyle = token => { + const { componentCls } = token; + + const fixedSelector = `> ${componentCls}-item > ${componentCls}-header ${componentCls}-arrow svg`; + + return { + [`${componentCls}-rtl`]: { + [fixedSelector]: { + transform: `rotate(180deg)`, + }, + }, + }; +}; + +const genBorderlessStyle: GenerateStyle = token => { + const { + componentCls, + collapseHeaderBg, + paddingXXS, + + colorBorder, + } = token; + + return { + [`${componentCls}-borderless`]: { + backgroundColor: collapseHeaderBg, + border: 0, + + [`> ${componentCls}-item`]: { + borderBottom: `1px solid ${colorBorder}`, + }, + + [` + > ${componentCls}-item:last-child, + > ${componentCls}-item:last-child ${componentCls}-header + `]: { + borderRadius: 0, + }, + + [`> ${componentCls}-item:last-child`]: { + borderBottom: 0, + }, + + [`> ${componentCls}-item > ${componentCls}-content`]: { + backgroundColor: 'transparent', + borderTop: 0, + }, + + [`> ${componentCls}-item > ${componentCls}-content > ${componentCls}-content-box`]: { + paddingTop: paddingXXS, + }, + }, + }; +}; + +const genGhostStyle: GenerateStyle = token => { + const { componentCls, paddingSM } = token; + + return { + [`${componentCls}-ghost`]: { + backgroundColor: 'transparent', + border: 0, + [`> ${componentCls}-item`]: { + borderBottom: 0, + [`> ${componentCls}-content`]: { + backgroundColor: 'transparent', + border: 0, + [`> ${componentCls}-content-box`]: { + paddingBlock: paddingSM, + }, + }, + }, + }, + }; +}; + +export default genComponentStyleHook('Collapse', token => { + const collapseToken = mergeToken(token, { + collapseContentBg: token.colorBgContainer, + collapseHeaderBg: token.colorFillAlter, + collapseHeaderPadding: `${token.paddingSM}px ${token.padding}px`, + collapsePanelBorderRadius: token.borderRadiusLG, + collapseContentPaddingHorizontal: 16, // Fixed value + }); + + return [ + genBaseStyle(collapseToken), + genBorderlessStyle(collapseToken), + genGhostStyle(collapseToken), + genArrowStyle(collapseToken), + genCollapseMotion(collapseToken), + ]; +}); diff --git a/components/collapse/style/rtl.less b/components/collapse/style/rtl.less deleted file mode 100644 index 559a922de2..0000000000 --- a/components/collapse/style/rtl.less +++ /dev/null @@ -1,48 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@collapse-prefix-cls: ~'@{ant-prefix}-collapse'; - -.@{collapse-prefix-cls} { - &-rtl { - direction: rtl; - } - - & > &-item { - > .@{collapse-prefix-cls}-header { - .@{collapse-prefix-cls}-rtl & { - padding: @collapse-header-padding; - padding-right: @collapse-header-padding-extra; - } - - .@{collapse-prefix-cls}-arrow { - .@{collapse-prefix-cls}-rtl& { - margin-right: 0; - margin-left: @margin-sm; - } - - & svg { - .@{collapse-prefix-cls}-rtl& { - transform: rotate(180deg); - } - } - } - - .@{collapse-prefix-cls}-extra { - .@{collapse-prefix-cls}-rtl& { - margin-right: auto; - margin-left: 0; - } - } - } - - &.@{collapse-prefix-cls}-no-arrow { - > .@{collapse-prefix-cls}-header { - .@{collapse-prefix-cls}-rtl& { - padding-right: @padding-sm; - padding-left: 0; - } - } - } - } -} diff --git a/components/color-picker/ColorPicker.jsx b/components/color-picker/ColorPicker.jsx deleted file mode 100644 index 66b6c36739..0000000000 --- a/components/color-picker/ColorPicker.jsx +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable */ -import PropTypes from '../_util/vue-types'; -import { defaultConfigProvider } from '../config-provider'; -import BaseMixin from '../_util/BaseMixin'; -import Pickr from '@simonwep/pickr/dist/pickr.es5.min'; -import Icon from '../icon'; -import LocaleReceiver from '../locale-provider/LocaleReceiver'; -import enUS from './locale/en_US'; -import debounce from 'lodash-es/debounce'; - -import { getOptionProps, findDOMNode } from '../_util/props-util'; -let colors = '#194d33'; -export default { - name: 'AColorPicker', - mixins: [BaseMixin], - inject: { - configProvider: { default: () => defaultConfigProvider }, - }, - model: { - prop: 'value', - event: 'change.value', //为了支持v-model直接返回颜色字符串 所以用了自定义的事件,与pickr自带change事件进行区分 - }, - props: { - prefixCls: String, - defaultValue: String, //默认值 - config: PropTypes.object, //pickr配置 - value: String, //颜色值 - locale: PropTypes.object, //双语包 - colorRounded: Number, //颜色数值保留几位小数 - size: PropTypes.oneOf(['default', 'small', 'large']).def('default'), //尺寸 - getPopupContainer: Function, //指定渲染容器 - disabled: { type: Boolean, default: false }, //是否禁用 - format: String, //颜色格式设置 - alpha: { type: Boolean, default: false }, //是否开启透明通道 - hue: { type: Boolean, default: true }, //是否开启色彩预选 - }, - - data() { - return { - colors, - myOpen: false, - pickr: null, - i18n: enUS, - }; - }, - watch: { - 'configProvider.locale.ColorPicker': { - handler(val) { - if (this.locale) return; - this.i18n = val; - this.reInitialize(); - }, - }, - locale(val) { - this.i18n = val.ColorPicker || val.lang; - this.reInitialize(); - }, - value(val) { - this.setColor(val); - }, - disabled(val) { - this.pickr[val ? 'disable' : 'enable'](); - }, - config: { - handler() { - this.reInitialize(); - }, - deep: true, - }, - format(val) { - const type = val.toLocaleUpperCase(); - let res = this.pickr.setColorRepresentation(type); - if (res) { - this.pickr.applyColor(); - } else { - throw new TypeError('format was invalid'); - } - }, - }, - mounted() { - if (this.locale) { - this.i18n = this.locale.ColorPicker || this.locale.lang; - } - this.createPickr(); - this.eventsBinding(); - }, - unmounted() { - this.pickr.destroyAndRemove(); - }, - methods: { - reInitialize() { - this.pickr.destroyAndRemove(); - const dom = document.createElement('div'); - dom.id = 'color-picker' + this._uid; - const box = findDOMNode(this).querySelector('#color-picker-box' + this._uid); - box.appendChild(dom); - this.createPickr(); - this.eventsBinding(); - }, - setColor: debounce(function (val) { - this.pickr.setColor(val); - }, 1000), - eventsBinding() { - const pickrEvents = [ - 'init', - 'hide', - 'show', - 'save', - 'clear', - 'change', - 'changestop', - 'cancel', - 'swatchselect', - ]; - Object.keys(this.$listeners).forEach(event => { - pickrEvents.includes(event) && this.pickr.on(event, this.$listeners[event]); - }); - }, - createPickr() { - const { getPopupContainer } = getOptionProps(this); - const { getPopupContainer: getContextPopupContainer } = this.configProvider; - const container = getPopupContainer || getContextPopupContainer; - this.pickr = Pickr.create( - Object.assign( - { - el: '#color-picker' + this._uid, - container: (container && container(findDOMNode(this))) || document.body, - theme: 'monolith', // or 'monolith', or 'nano' - default: this.value || this.defaultValue || null, // 有默认颜色pickr才可以获取到_representation - components: { - // Main components - preview: true, - opacity: this.alpha, - hue: this.hue, - // Input / output Options - interaction: { - hex: true, - rgba: true, - input: true, - clear: true, - save: true, - }, - }, - }, - this.config, - { i18n: this.i18n }, - ), - ) - .on('save', (color, instance) => { - if (color) { - let _representation = instance._representation || 'HEXA'; - color = color['to' + _representation]().toString(this.colorRounded || 0); - } - this.$emit('change.value', color || ''); - }) - .on('hide', () => { - this.setState({ myOpen: false }); - }); - }, - handleOpenChange() { - const open = !this.myOpen; - this.setState({ myOpen: open }); - this.pickr[open ? 'show' : 'hide'](); - this.$emit('openChange', open); - }, - getDefaultLocale() { - const result = { - ...enUS, - ...this.$props.locale, - }; - result.lang = { - ...result.lang, - ...(this.$props.locale || {}).lang, - }; - return result; - }, - renderColorPicker() { - const { prefixCls: customizePrefixCls } = this.$props; - const { getPrefixCls } = this.configProvider; - const prefixCls = getPrefixCls('color-picker', customizePrefixCls); - const { disabled } = getOptionProps(this); - const classString = { - [prefixCls]: true, - [`${prefixCls}-open`]: this.myOpen, - [`${prefixCls}-lg`]: this.size === 'large', - [`${prefixCls}-sm`]: this.size === 'small', - [`${prefixCls}-disabled`]: this.disabled, - }; - return ( -
    -
    -
    -
    -
    - -
    -
    - ); - }, - }, - render() { - return ( - - ); - }, -}; diff --git a/components/color-picker/__tests__/__snapshots__/index.test.js.snap b/components/color-picker/__tests__/__snapshots__/index.test.js.snap deleted file mode 100644 index 488cb5ecfa..0000000000 --- a/components/color-picker/__tests__/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,337 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ColorPicker prop locale should works 1`] = ` -
    -
    -
    -
    - - - - -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    - - -
    - -
    - -
    - - - - - - - - - - - -
    -
    -
    -`; - -exports[`ColorPicker save event should works 1`] = ` -
    -
    -
    -
    - - - - -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    - - -
    - -
    - -
    - - - - - - - - - - - -
    -
    -
    -`; - -exports[`ColorPicker should support default value 1`] = ` -
    -
    -
    -
    - - - - -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    - - -
    - -
    - -
    - - - - - - - - - - - -
    -
    -
    -`; - -exports[`ColorPicker should support disabled 1`] = ` -
    -
    -
    -
    - - - - -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    - - -
    - -
    - -
    - - - - - - - - - - - -
    -
    -
    -`; - -exports[`ColorPicker should support format 1`] = ` -
    -
    -
    -
    - - - - -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    - - -
    - -
    - -
    - - - - - - - - - - - -
    -
    -
    -`; - -exports[`ColorPicker should support v-model 1`] = ` -
    -
    -
    -
    - - - - -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    - - -
    - -
    - -
    - - - - - - - - - - - -
    -
    -
    -`; diff --git a/components/color-picker/__tests__/index.test.js b/components/color-picker/__tests__/index.test.js deleted file mode 100644 index 1ad6a1d035..0000000000 --- a/components/color-picker/__tests__/index.test.js +++ /dev/null @@ -1,155 +0,0 @@ -import { mount } from '@vue/test-utils'; -import ColorPicker from '..'; -import { asyncExpect } from '../../../tests/utils'; -describe('ColorPicker', () => { - xit('should support default value', async () => { - const wrapper = mount( - { - render() { - return p}>; - }, - }, - { sync: false, attachTo: 'body' }, - ); - await asyncExpect(() => { - expect(wrapper.html()).toMatchSnapshot(); - wrapper.unmount(); - }, 1000); - }); - xit('should support v-model', async () => { - let color = 'rgba(10, 10, 10, 1)'; - const wrapper = mount( - { - data() { - return { - color, - }; - }, - render() { - return p}>; - }, - mounted() { - this.color = 'rgba(110, 120, 130, 1)'; - }, - }, - { sync: false, attachTo: 'body' }, - ); - - await asyncExpect(() => { - expect(wrapper.html()).toMatchSnapshot(); - wrapper.unmount(); - }, 1000); - }); - xit('should support disabled', async () => { - const wrapper = mount( - { - data() { - return { - disabled: false, - }; - }, - render() { - return p}>; - }, - mounted() { - this.disabled = true; - }, - }, - { sync: false, attachTo: 'body' }, - ); - - await asyncExpect(async () => { - expect(wrapper.html()).toMatchSnapshot(); - await asyncExpect(() => { - wrapper.unmount(); - }); - }, 1000); - }); - xit('should support format', async () => { - const wrapper = mount( - { - data() { - return { - format: 'RGBA', - }; - }, - render() { - return p}>; - }, - mounted() { - this.format = 'HEX'; - }, - }, - { sync: false, attachTo: 'body' }, - ); - - await asyncExpect(async () => { - expect(wrapper.html()).toMatchSnapshot(); - await asyncExpect(() => { - wrapper.unmount(); - }); - }, 1000); - }); - xit('prop locale should works', async () => { - const wrapper = mount( - { - data() { - return { - locale: { - lang: { - 'btn:save': 'セーブ', - 'btn:cancel': 'キャンセル', - 'btn:clear': '晴れ', - }, - }, - }; - }, - render() { - return ( - p} /> - ); - }, - mounted() { - this.locale = { - lang: { - 'btn:save': '1セーブ', - 'btn:cancel': '1キャンセル', - 'btn:clear': '1晴れ', - }, - }; - }, - }, - { sync: false, attachTo: 'body' }, - ); - await asyncExpect(async () => { - expect(wrapper.html()).toMatchSnapshot(); - await asyncExpect(() => { - wrapper.unmount(); - }); - }, 1000); - }); - xit('save event should works', async () => { - const wrapper = mount( - { - render() { - return ( - p} onSave={this.save} /> - ); - }, - methods: { - save(val) { - return val; - }, - }, - }, - { sync: false, attachTo: 'body' }, - ); - await asyncExpect(async () => { - wrapper.find('.pcr-save').trigger('click'); - expect(wrapper.html()).toMatchSnapshot(); - await asyncExpect(() => { - wrapper.unmount(); - }); - }, 1000); - }); -}); diff --git a/components/color-picker/demo/alpha.md b/components/color-picker/demo/alpha.md deleted file mode 100644 index 181412876e..0000000000 --- a/components/color-picker/demo/alpha.md +++ /dev/null @@ -1,24 +0,0 @@ - -#### 透明度 -开启属性 `alpha`,可以选择带 Alpha 通道的颜色。 - - - -#### Alpha -Set the property `alpha` true, to select a color with alpha channel. - - -```vue - - -``` diff --git a/components/color-picker/demo/basic.md b/components/color-picker/demo/basic.md deleted file mode 100644 index 1f25f71369..0000000000 --- a/components/color-picker/demo/basic.md +++ /dev/null @@ -1,34 +0,0 @@ - -#### 基础用法 -基本用法,可以使用 v-model 实现数据的双向绑定。 - - - -#### Basic -Basic usage. You can use v-model to enable a two-way bingding on data. - - -```vue - - -``` diff --git a/components/color-picker/demo/colors.md b/components/color-picker/demo/colors.md deleted file mode 100644 index 5609088c34..0000000000 --- a/components/color-picker/demo/colors.md +++ /dev/null @@ -1,44 +0,0 @@ - -#### 颜色预设 -可以通过`config.swatches`设置,来自定义预设颜色 -更多配置 - - - -#### Color Presets -Set `config.swatches ` to customize the default color presets. -More config settings - - -```vue - - -``` diff --git a/components/color-picker/demo/hue.md b/components/color-picker/demo/hue.md deleted file mode 100644 index 4d4bd2c1b4..0000000000 --- a/components/color-picker/demo/hue.md +++ /dev/null @@ -1,24 +0,0 @@ - -#### 色彩 -设置属性 `hue` 为 false,可以禁用色彩选项。 - - - -#### Hue -Set property `hue` to false, can hide hue slider. - - -```vue - - -``` diff --git a/components/color-picker/demo/size.md b/components/color-picker/demo/size.md deleted file mode 100644 index 469d428c0b..0000000000 --- a/components/color-picker/demo/size.md +++ /dev/null @@ -1,36 +0,0 @@ - -#### 尺寸 -选择器有三种尺寸:大、默认(中)、小。 - - - -#### Size -There are three size of ColorPicker: large, medium(default), small. - - -```vue - - -``` diff --git a/components/color-picker/index.en-US.md b/components/color-picker/index.en-US.md deleted file mode 100644 index 99e345b599..0000000000 --- a/components/color-picker/index.en-US.md +++ /dev/null @@ -1,27 +0,0 @@ -## API - -| Property | Description | Type | Default | -| --- | --- | --- | --- | -| colorRounded | precision of color | number | 0 | -| config | pickr config | [pickr options](https://github.com/Simonwep/pickr) | - | -| defaultValue | default color | string | - | -| disabled | whether disabled picker | boolean | false | -| format | Color format | 'HEXA' \|'RGBA' \|'HSVA' \|'HSLA' \|'CMYK' | 'HEXA' | -| getPopupContainer | to set the container of the floating layer, while the default is to create a div element in body | Function(triggerNode) | () => document.body | -| locale | locale package | [default setting](https://github.com/vueComponent/ant-design-vue/blob/main/components/color-picker/locale) | - | -| size | size of pickr | 'large'\|'small'\|'default' | 'default' | -| value | color value | string | - | - -### Event - -| Event | Description | Arguments | -| --- | --- | --- | -| `cancel` | User clicked the cancel button (return to previous color). | `PickrInstance` | -| `change` | Color has changed (but not saved). Also fired on `swatchselect` | `HSVaColorObject, PickrInstance` | -| `changestop` | User stopped to change the color | `PickrInstance` | -| `clear` | User cleared the color. | `PickrInstance` | -| `hide` | Pickr got closed | `PickrInstance` | -| `init` | Initialization done - pickr can be used | `PickrInstance` | -| `save` | User clicked the save / clear button. Also fired on clear with `null` as color. | `HSVaColorObject or null, PickrInstance` | -| `show` | Pickr got opened | `PickrInstance` | -| `swatchselect` | User clicked one of the swatches | `HSVaColorObject, PickrInstance` | diff --git a/components/color-picker/index.js b/components/color-picker/index.js deleted file mode 100644 index 3bf52f6071..0000000000 --- a/components/color-picker/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import ColorPicker from './ColorPicker'; -/* istanbul ignore next */ -ColorPicker.install = function (app) { - app.component(ColorPicker.name, ColorPicker); - return app; -}; - -export default ColorPicker; diff --git a/components/color-picker/index.zh-CN.md b/components/color-picker/index.zh-CN.md deleted file mode 100644 index 2bee7391e2..0000000000 --- a/components/color-picker/index.zh-CN.md +++ /dev/null @@ -1,27 +0,0 @@ -## API - -| 参数 | 说明 | 类型 | 默认值 | -| --- | --- | --- | --- | -| colorRounded | 颜色数值精度 | number | 0 | -| config | pickr 配置项 | [pickr options](https://github.com/Simonwep/pickr) | - | -| defaultValue | 默认颜色 | string | - | -| disabled | 是否禁用 | boolean | false | -| format | 定义返回的颜色格式 | 'HEXA' \|'RGBA' \|'HSVA' \|'HSLA' \|'CMYK' | 'HEXA' | -| getPopupContainer | 浮层渲染父节点,默认渲染到 body 上 | Function(triggerNode) | () => document.body | -| locale | 语言包 | [默认配置](https://github.com/vueComponent/ant-design-vue/blob/main/components/color-picker/locale) | - | -| size | 取色器尺寸 | 'large'\|'small'\|'default' | 'default' | -| value | 颜色值 | string | - | - -### 事件 - -| 事件名称 | 说明 | 回调参数 | -| --- | --- | --- | -| `cancel` | 用户点击取消时(颜色返回至上个颜色) | `PickrInstance` | -| `change` | 颜色值发生变更时(非保存).`swatchselect`也会触发 | `HSVaColorObject, PickrInstance` | -| `changestop` | 用户不再改变颜色时 | `PickrInstance` | -| `clear` | 清空颜色时 | `PickrInstance` | -| `hide` | Pickr 关闭时 | `PickrInstance` | -| `init` | 初始化完成,可以使用 pickr | `PickrInstance` | -| `save` | 用户点击保存/清空按钮时 | `HSVaColorObject or null, PickrInstance` | -| `show` | Pickr 开启时 | `PickrInstance` | -| `swatchselect` | 用户切换了色板 | `HSVaColorObject, PickrInstance` | diff --git a/components/color-picker/locale/en_US.js b/components/color-picker/locale/en_US.js deleted file mode 100644 index d8f2854de6..0000000000 --- a/components/color-picker/locale/en_US.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - 'btn:save': 'Save', - 'btn:cancel': 'Cancel', - 'btn:clear': 'Clear', -}; diff --git a/components/color-picker/locale/ku_KU.js b/components/color-picker/locale/ku_KU.js deleted file mode 100644 index 6da0dccd1f..0000000000 --- a/components/color-picker/locale/ku_KU.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - 'btn:save': 'پاشکەوت کردن', - 'btn:cancel': 'هەڵوەشاندنەوە', - 'btn:clear': 'پاککردنەوە', -}; diff --git a/components/color-picker/locale/zh_CN.js b/components/color-picker/locale/zh_CN.js deleted file mode 100644 index 74117e2c79..0000000000 --- a/components/color-picker/locale/zh_CN.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - 'btn:save': '保存', - 'btn:cancel': '取消', - 'btn:clear': '清除', -}; diff --git a/components/color-picker/locale/zh_TW.js b/components/color-picker/locale/zh_TW.js deleted file mode 100644 index 74117e2c79..0000000000 --- a/components/color-picker/locale/zh_TW.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - 'btn:save': '保存', - 'btn:cancel': '取消', - 'btn:clear': '清除', -}; diff --git a/components/color-picker/style/index.tsx b/components/color-picker/style/index.tsx deleted file mode 100644 index 934b4e1442..0000000000 --- a/components/color-picker/style/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// TODO -import '../../style/index.less'; diff --git a/components/comment/__tests__/__snapshots__/demo.test.js.snap b/components/comment/__tests__/__snapshots__/demo.test.js.snap index 002c755860..3236016900 100644 --- a/components/comment/__tests__/__snapshots__/demo.test.js.snap +++ b/components/comment/__tests__/__snapshots__/demo.test.js.snap @@ -31,29 +31,33 @@ exports[`renders ./components/comment/demo/editor.vue correctly 1`] = `
    -
    - -
    -
    -
    +
    +
    + +
    +
    +
    +
    +
    - -
    -
    -
    -
    -
    -
    +
    +
    +
    + +
    +
    +
    +
    +
    - -
    +
    diff --git a/components/comment/__tests__/__snapshots__/index.test.js.snap b/components/comment/__tests__/__snapshots__/index.test.js.snap index 99acaabacf..4b5ccb05dd 100644 --- a/components/comment/__tests__/__snapshots__/index.test.js.snap +++ b/components/comment/__tests__/__snapshots__/index.test.js.snap @@ -42,29 +42,33 @@ exports[`Comment Comment can be used as editor, user can customize the editor co
    -
    - -
    -
    -
    +
    +
    + +
    +
    +
    +
    +
    - -
    -
    -
    -
    -
    -
    +
    +
    +
    + +
    +
    +
    +
    +
    - -
    +
    diff --git a/components/comment/demo/basic.vue b/components/comment/demo/basic.vue index 23cff53211..540fc8ecab 100644 --- a/components/comment/demo/basic.vue +++ b/components/comment/demo/basic.vue @@ -65,45 +65,26 @@ A basic comment with author, avatar, time and actions. - diff --git a/components/comment/demo/editor.vue b/components/comment/demo/editor.vue index 32f5326e6a..7f9abd7623 100644 --- a/components/comment/demo/editor.vue +++ b/components/comment/demo/editor.vue @@ -50,47 +50,36 @@ Comment can be used as editor, user can customize the editor component. - diff --git a/components/comment/demo/list.vue b/components/comment/demo/list.vue index 81db3af106..b9efef0122 100644 --- a/components/comment/demo/list.vue +++ b/components/comment/demo/list.vue @@ -44,35 +44,27 @@ Displaying a series of comments using the `antd` List Component. - diff --git a/components/comment/index.tsx b/components/comment/index.tsx index 8502f1a564..eda4e8ac50 100644 --- a/components/comment/index.tsx +++ b/components/comment/index.tsx @@ -4,7 +4,11 @@ import PropTypes from '../_util/vue-types'; import { flattenChildren } from '../_util/props-util'; import type { CustomSlotsType, VueNode } from '../_util/type'; import { withInstall } from '../_util/type'; -import useConfigInject from '../_util/hooks/useConfigInject'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; + +// CSSINJS +import useStyle from './style'; + export const commentProps = () => ({ actions: Array, /** The element to display as the comment author. */ @@ -24,8 +28,8 @@ export type CommentProps = Partial, - setup(props, { slots }) { + setup(props, { slots, attrs }) { const { prefixCls, direction } = useConfigInject('comment', props); + + // style + const [wrapSSR, hashId] = useStyle(prefixCls); + const renderNested = (prefixCls: string, children: VueNode) => { return
    {children}
    ; }; @@ -87,18 +95,21 @@ const Comment = defineComponent({
    ); const children = flattenChildren(slots.default?.()); - return ( + return wrapSSR(
    {comment} {children && children.length ? renderNested(pre, children) : null} -
    +
    , ); }; }, diff --git a/components/comment/style/index.less b/components/comment/style/index.less deleted file mode 100644 index 84da3a3a2a..0000000000 --- a/components/comment/style/index.less +++ /dev/null @@ -1,105 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@comment-prefix-cls: ~'@{ant-prefix}-comment'; - -.@{comment-prefix-cls} { - position: relative; - background-color: @comment-bg; - - &-inner { - display: flex; - padding: @comment-padding-base; - } - - &-avatar { - position: relative; - flex-shrink: 0; - margin-right: @margin-sm; - cursor: pointer; - - img { - width: 32px; - height: 32px; - border-radius: 50%; - } - } - - &-content { - position: relative; - flex: 1 1 auto; - min-width: 1px; - font-size: @comment-font-size-base; - word-wrap: break-word; - - &-author { - display: flex; - flex-wrap: wrap; - justify-content: flex-start; - margin-bottom: @margin-xss; - font-size: @comment-font-size-base; - - & > a, - & > span { - padding-right: @padding-xs; - font-size: @comment-font-size-sm; - line-height: 18px; - } - - &-name { - color: @comment-author-name-color; - font-size: @comment-font-size-base; - transition: color 0.3s; - - > * { - color: @comment-author-name-color; - - &:hover { - color: @comment-author-name-color; - } - } - } - - &-time { - color: @comment-author-time-color; - white-space: nowrap; - cursor: auto; - } - } - - &-detail p { - margin-bottom: @comment-content-detail-p-margin-bottom; - white-space: pre-wrap; - } - } - - &-actions { - margin-top: @comment-actions-margin-top; - margin-bottom: @comment-actions-margin-bottom; - padding-left: 0; - - > li { - display: inline-block; - color: @comment-action-color; - - > span { - margin-right: 10px; - color: @comment-action-color; - font-size: @comment-font-size-sm; - cursor: pointer; - transition: color 0.3s; - user-select: none; - - &:hover { - color: @comment-action-hover-color; - } - } - } - } - - &-nested { - margin-left: @comment-nest-indent; - } -} - -@import './rtl'; diff --git a/components/comment/style/index.ts b/components/comment/style/index.ts new file mode 100644 index 0000000000..c1ac4592a4 --- /dev/null +++ b/components/comment/style/index.ts @@ -0,0 +1,160 @@ +import type { FullToken, GenerateStyle } from '../../theme/internal'; +import { genComponentStyleHook, mergeToken } from '../../theme/internal'; + +export interface ComponentToken {} + +type CommentToken = FullToken<'Comment'> & { + commentBg: string; + commentPaddingBase: string; + commentNestIndent: string; + commentFontSizeBase: number; + commentFontSizeSm: number; + commentAuthorNameColor: string; + commentAuthorTimeColor: string; + commentActionColor: string; + commentActionHoverColor: string; + commentActionsMarginBottom: string; + commentActionsMarginTop: number; + commentContentDetailPMarginBottom: string; +}; + +const genBaseStyle: GenerateStyle = token => { + const { + componentCls, + commentBg, + commentPaddingBase, + commentNestIndent, + commentFontSizeBase, + commentFontSizeSm, + commentAuthorNameColor, + commentAuthorTimeColor, + commentActionColor, + commentActionHoverColor, + commentActionsMarginBottom, + commentActionsMarginTop, + commentContentDetailPMarginBottom, + } = token; + + return { + [componentCls]: { + position: 'relative', + backgroundColor: commentBg, + + [`${componentCls}-inner`]: { + display: 'flex', + padding: commentPaddingBase, + }, + + [`${componentCls}-avatar`]: { + position: 'relative', + flexShrink: 0, + marginRight: token.marginSM, + cursor: 'pointer', + + [`img`]: { + width: '32px', + height: '32px', + borderRadius: '50%', + }, + }, + + [`${componentCls}-content`]: { + position: 'relative', + flex: `1 1 auto`, + minWidth: `1px`, + fontSize: commentFontSizeBase, + wordWrap: 'break-word', + + [`&-author`]: { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'flex-start', + marginBottom: token.marginXXS, + fontSize: commentFontSizeBase, + + [`& > a,& > span`]: { + paddingRight: token.paddingXS, + fontSize: commentFontSizeSm, + lineHeight: `18px`, + }, + + [`&-name`]: { + color: commentAuthorNameColor, + fontSize: commentFontSizeBase, + transition: `color ${token.motionDurationSlow}`, + + [`> *`]: { + color: commentAuthorNameColor, + + [`&:hover`]: { + color: commentAuthorNameColor, + }, + }, + }, + + [`&-time`]: { + color: commentAuthorTimeColor, + whiteSpace: 'nowrap', + cursor: 'auto', + }, + }, + + [`&-detail p`]: { + marginBottom: commentContentDetailPMarginBottom, + whiteSpace: 'pre-wrap', + }, + }, + + [`${componentCls}-actions`]: { + marginTop: commentActionsMarginTop, + marginBottom: commentActionsMarginBottom, + paddingLeft: 0, + + [`> li`]: { + display: 'inline-block', + color: commentActionColor, + + [`> span`]: { + marginRight: '10px', + color: commentActionColor, + fontSize: commentFontSizeSm, + cursor: 'pointer', + transition: `color ${token.motionDurationSlow}`, + userSelect: 'none', + + [`&:hover`]: { + color: commentActionHoverColor, + }, + }, + }, + }, + + [`${componentCls}-nested`]: { + marginLeft: commentNestIndent, + }, + + '&-rtl': { + direction: 'rtl', + }, + }, + }; +}; + +export default genComponentStyleHook('Comment', token => { + const commentToken = mergeToken(token, { + commentBg: 'inherit', + commentPaddingBase: `${token.paddingMD}px 0`, + commentNestIndent: `44px`, + commentFontSizeBase: token.fontSize, + commentFontSizeSm: token.fontSizeSM, + commentAuthorNameColor: token.colorTextTertiary, + commentAuthorTimeColor: token.colorTextPlaceholder, + commentActionColor: token.colorTextTertiary, + commentActionHoverColor: token.colorTextSecondary, + commentActionsMarginBottom: 'inherit', + commentActionsMarginTop: token.marginSM, + commentContentDetailPMarginBottom: 'inherit', + }); + + return [genBaseStyle(commentToken)]; +}); diff --git a/components/comment/style/index.tsx b/components/comment/style/index.tsx deleted file mode 100644 index 3a3ab0de59..0000000000 --- a/components/comment/style/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import '../../style/index.less'; -import './index.less'; diff --git a/components/comment/style/rtl.less b/components/comment/style/rtl.less deleted file mode 100644 index a930d83846..0000000000 --- a/components/comment/style/rtl.less +++ /dev/null @@ -1,51 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@comment-prefix-cls: ~'@{ant-prefix}-comment'; - -.@{comment-prefix-cls} { - &-rtl { - direction: rtl; - } - - &-avatar { - .@{comment-prefix-cls}-rtl & { - margin-right: 0; - margin-left: 12px; - } - } - - &-content { - &-author { - & > a, - & > span { - .@{comment-prefix-cls}-rtl & { - padding-right: 0; - padding-left: 8px; - } - } - } - } - - &-actions { - .@{comment-prefix-cls}-rtl & { - padding-right: 0; - } - - > li { - > span { - .@{comment-prefix-cls}-rtl & { - margin-right: 0; - margin-left: 10px; - } - } - } - } - - &-nested { - .@{comment-prefix-cls}-rtl & { - margin-right: @comment-nest-indent; - margin-left: 0; - } - } -} diff --git a/components/components.ts b/components/components.ts index 020c5add2a..f9db584a68 100644 --- a/components/components.ts +++ b/components/components.ts @@ -13,9 +13,6 @@ export { default as Alert } from './alert'; export type { AvatarProps } from './avatar'; export { default as Avatar, AvatarGroup } from './avatar'; -export type { BackTopProps } from './back-top'; -export { default as BackTop } from './back-top'; - export type { BadgeProps } from './badge'; export { default as Badge, BadgeRibbon } from './badge'; @@ -76,6 +73,13 @@ export { default as Drawer } from './drawer'; export type { EmptyProps } from './empty'; export { default as Empty } from './empty'; +export type { + FloatButtonProps, + FloatButtonGroupProps, + BackTopProps, +} from './float-button/interface'; +export { default as FloatButton, FloatButtonGroup, BackTop } from './float-button'; + export type { FormProps, FormItemProps, FormInstance, FormItemInstance } from './form'; export { default as Form, FormItem, FormItemRest } from './form'; @@ -112,6 +116,7 @@ export type { MenuItemProps, MenuMode, MenuDividerProps, + ItemType, } from './menu'; export { default as Menu, MenuDivider, MenuItem, MenuItemGroup, SubMenu } from './menu'; @@ -244,3 +249,15 @@ export type { UploadProps, UploadListProps, UploadChangeParam, UploadFile } from export { default as Upload, UploadDragger } from './upload'; export { default as LocaleProvider } from './locale-provider'; + +export { default as Watermark } from './watermark'; +export type { WatermarkProps } from './watermark'; + +export type { SegmentedProps } from './segmented'; +export { default as Segmented } from './segmented'; + +export type { QRCodeProps } from './qrcode'; +export { default as QRCode } from './qrcode'; + +export type { TourProps, TourStepProps } from './tour'; +export { default as Tour } from './tour'; diff --git a/components/config-provider/DisabledContext.ts b/components/config-provider/DisabledContext.ts new file mode 100644 index 0000000000..7f53886614 --- /dev/null +++ b/components/config-provider/DisabledContext.ts @@ -0,0 +1,17 @@ +import type { InjectionKey, Ref } from 'vue'; +import { computed, inject, ref, provide } from 'vue'; + +export type DisabledType = boolean | undefined; +const DisabledContextKey: InjectionKey> = Symbol('DisabledContextKey'); + +export const useInjectDisabled = () => { + return inject(DisabledContextKey, ref(undefined)); +}; +export const useProviderDisabled = (disabled: Ref) => { + const parentDisabled = useInjectDisabled(); + provide( + DisabledContextKey, + computed(() => disabled.value ?? parentDisabled.value), + ); + return disabled; +}; diff --git a/components/config-provider/SizeContext.ts b/components/config-provider/SizeContext.ts new file mode 100644 index 0000000000..9fe6c9c65c --- /dev/null +++ b/components/config-provider/SizeContext.ts @@ -0,0 +1,16 @@ +import type { InjectionKey, Ref } from 'vue'; +import { computed, inject, ref, provide } from 'vue'; +export type SizeType = 'small' | 'middle' | 'large' | undefined; +const SizeContextKey: InjectionKey> = Symbol('SizeContextKey'); + +export const useInjectSize = () => { + return inject(SizeContextKey, ref(undefined as SizeType)); +}; +export const useProviderSize = (size: Ref) => { + const parentSize = useInjectSize(); + provide( + SizeContextKey, + computed(() => size.value || parentSize.value), + ); + return size; +}; diff --git a/components/config-provider/context.ts b/components/config-provider/context.ts index f1661a2152..aa26266730 100644 --- a/components/config-provider/context.ts +++ b/components/config-provider/context.ts @@ -1,14 +1,24 @@ -import type { ExtractPropTypes, InjectionKey, PropType, Ref } from 'vue'; +import type { ComputedRef, ExtractPropTypes, InjectionKey, PropType, Ref } from 'vue'; import { computed, inject, provide } from 'vue'; import type { ValidateMessages } from '../form/interface'; import type { RequiredMark } from '../form/Form'; import type { RenderEmptyHandler } from './renderEmpty'; import type { TransformCellTextProps } from '../table/interface'; import type { Locale } from '../locale-provider'; +import type { DerivativeFunc } from '../_util/cssinjs'; +import type { AliasToken, SeedToken } from '../theme/internal'; +import type { MapToken, OverrideToken } from '../theme/interface'; +import type { VueNode } from '../_util/type'; +import { objectType } from '../_util/type'; + +export const defaultIconPrefixCls = 'anticon'; type GlobalFormCOntextProps = { validateMessages?: Ref; }; + +export type DirectionType = 'ltr' | 'rtl' | undefined; + export const GlobalFormContextKey: InjectionKey = Symbol('GlobalFormContextKey'); @@ -39,38 +49,20 @@ export type SizeType = 'small' | 'middle' | 'large' | undefined; export type Direction = 'ltr' | 'rtl'; -export interface ConfigConsumerProps { - getTargetContainer?: () => HTMLElement; - getPopupContainer?: (triggerNode?: HTMLElement) => HTMLElement; - rootPrefixCls?: string; - getPrefixCls: (suffixCls?: string, customizePrefixCls?: string) => string; - renderEmpty: RenderEmptyHandler; - transformCellText?: (tableProps: TransformCellTextProps) => any; - csp?: CSPConfig; - autoInsertSpaceInButton?: boolean; - input?: { - autocomplete?: string; - }; - locale?: Locale; - pageHeader?: { - ghost: boolean; - }; - componentSize?: SizeType; - direction?: 'ltr' | 'rtl'; - space?: { - size?: SizeType | number; - }; - virtual?: boolean; - dropdownMatchSelectWidth?: boolean | number; - form?: { - requiredMark?: RequiredMark; - colon?: boolean; - }; +export type MappingAlgorithm = DerivativeFunc; + +export interface ThemeConfig { + token?: Partial; + components?: OverrideToken; + algorithm?: MappingAlgorithm | MappingAlgorithm[]; + hashed?: boolean; + inherit?: boolean; } export const configProviderProps = () => ({ + iconPrefixCls: String, getTargetContainer: { - type: Function as PropType<() => HTMLElement>, + type: Function as PropType<() => HTMLElement | Window>, }, getPopupContainer: { type: Function as PropType<(triggerNode?: HTMLElement) => HTMLElement>, @@ -85,46 +77,90 @@ export const configProviderProps = () => ({ transformCellText: { type: Function as PropType<(tableProps: TransformCellTextProps) => any>, }, - csp: { - type: Object as PropType, - default: undefined as CSPConfig, - }, - input: { - type: Object as PropType<{ autocomplete: string }>, - }, + csp: objectType(), + input: objectType<{ autocomplete?: string }>(), autoInsertSpaceInButton: { type: Boolean, default: undefined }, - locale: { - type: Object as PropType, - default: undefined as Locale, - }, - pageHeader: { - type: Object as PropType<{ ghost: boolean }>, - }, + locale: objectType(), + pageHeader: objectType<{ ghost?: boolean }>(), componentSize: { type: String as PropType, }, + componentDisabled: { type: Boolean, default: undefined }, direction: { type: String as PropType<'ltr' | 'rtl'>, }, - space: { - type: Object as PropType<{ size: SizeType | number }>, - }, + space: objectType<{ size?: SizeType | number }>(), virtual: { type: Boolean, default: undefined }, dropdownMatchSelectWidth: { type: [Number, Boolean], default: true }, - form: { - type: Object as PropType<{ - validateMessages?: ValidateMessages; - requiredMark?: RequiredMark; - colon?: boolean; - }>, - default: undefined as { - validateMessages?: ValidateMessages; - requiredMark?: RequiredMark; - colon?: boolean; - }, - }, - // internal use - notUpdateGlobalConfig: Boolean, + form: objectType<{ + validateMessages?: ValidateMessages; + requiredMark?: RequiredMark; + colon?: boolean; + }>(), + pagination: objectType<{ + showSizeChanger?: boolean; + }>(), + theme: objectType(), + select: objectType<{ + showSearch?: boolean; + }>(), }); export type ConfigProviderProps = Partial>>; + +export interface ConfigProviderInnerProps { + csp?: ComputedRef; + autoInsertSpaceInButton?: ComputedRef; + locale?: ComputedRef; + direction?: ComputedRef<'ltr' | 'rtl'>; + space?: ComputedRef<{ + size?: number | SizeType; + }>; + virtual?: ComputedRef; + dropdownMatchSelectWidth?: ComputedRef; + getPrefixCls: (suffixCls?: string, customizePrefixCls?: string) => string; + iconPrefixCls: ComputedRef; + theme?: ComputedRef; + renderEmpty?: (name?: string) => VueNode; + getTargetContainer?: ComputedRef<() => HTMLElement | Window>; + getPopupContainer?: ComputedRef<(triggerNode?: HTMLElement) => HTMLElement>; + pageHeader?: ComputedRef<{ + ghost?: boolean; + }>; + input?: ComputedRef<{ + autocomplete?: string; + }>; + pagination?: ComputedRef<{ + showSizeChanger?: boolean; + }>; + form?: ComputedRef<{ + validateMessages?: ValidateMessages; + requiredMark?: RequiredMark; + colon?: boolean; + }>; + select?: ComputedRef<{ + showSearch?: boolean; + }>; + componentSize?: ComputedRef; + componentDisabled?: ComputedRef; + transformCellText?: ComputedRef<(tableProps: TransformCellTextProps) => any>; +} + +export const configProviderKey: InjectionKey = Symbol('configProvider'); + +export const defaultConfigProvider: ConfigProviderInnerProps = { + getPrefixCls: (suffixCls?: string, customizePrefixCls?: string) => { + if (customizePrefixCls) return customizePrefixCls; + return suffixCls ? `ant-${suffixCls}` : 'ant'; + }, + iconPrefixCls: computed(() => defaultIconPrefixCls), + getPopupContainer: computed(() => () => document.body), +}; + +export const useConfigContextInject = () => { + return inject(configProviderKey, defaultConfigProvider); +}; + +export const useConfigContextProvider = (props: ConfigProviderInnerProps) => { + return provide(configProviderKey, props); +}; diff --git a/components/config-provider/cssVariables.ts b/components/config-provider/cssVariables.ts new file mode 100644 index 0000000000..505380ae37 --- /dev/null +++ b/components/config-provider/cssVariables.ts @@ -0,0 +1,103 @@ +/* eslint-disable import/prefer-default-export, prefer-destructuring */ + +import { TinyColor } from '@ctrl/tinycolor'; +import { generate } from '@ant-design/colors'; +import type { Theme } from './context'; +import { updateCSS } from '../vc-util/Dom/dynamicCSS'; +import canUseDom from '../_util/canUseDom'; +import warning from '../_util/warning'; + +const dynamicStyleMark = `-ant-${Date.now()}-${Math.random()}`; + +export function getStyle(globalPrefixCls: string, theme: Theme) { + const variables: Record = {}; + + const formatColor = (color: TinyColor, updater?: (cloneColor: TinyColor) => TinyColor) => { + let clone = color.clone(); + clone = updater?.(clone) || clone; + return clone.toRgbString(); + }; + + const fillColor = (colorVal: string, type: string) => { + const baseColor = new TinyColor(colorVal); + const colorPalettes = generate(baseColor.toRgbString()); + + variables[`${type}-color`] = formatColor(baseColor); + variables[`${type}-color-disabled`] = colorPalettes[1]; + variables[`${type}-color-hover`] = colorPalettes[4]; + variables[`${type}-color-active`] = colorPalettes[6]; + variables[`${type}-color-outline`] = baseColor.clone().setAlpha(0.2).toRgbString(); + variables[`${type}-color-deprecated-bg`] = colorPalettes[0]; + variables[`${type}-color-deprecated-border`] = colorPalettes[2]; + }; + + // ================ Primary Color ================ + if (theme.primaryColor) { + fillColor(theme.primaryColor, 'primary'); + + const primaryColor = new TinyColor(theme.primaryColor); + const primaryColors = generate(primaryColor.toRgbString()); + + // Legacy - We should use semantic naming standard + primaryColors.forEach((color, index) => { + variables[`primary-${index + 1}`] = color; + }); + // Deprecated + variables['primary-color-deprecated-l-35'] = formatColor(primaryColor, c => c.lighten(35)); + variables['primary-color-deprecated-l-20'] = formatColor(primaryColor, c => c.lighten(20)); + variables['primary-color-deprecated-t-20'] = formatColor(primaryColor, c => c.tint(20)); + variables['primary-color-deprecated-t-50'] = formatColor(primaryColor, c => c.tint(50)); + variables['primary-color-deprecated-f-12'] = formatColor(primaryColor, c => + c.setAlpha(c.getAlpha() * 0.12), + ); + + const primaryActiveColor = new TinyColor(primaryColors[0]); + variables['primary-color-active-deprecated-f-30'] = formatColor(primaryActiveColor, c => + c.setAlpha(c.getAlpha() * 0.3), + ); + variables['primary-color-active-deprecated-d-02'] = formatColor(primaryActiveColor, c => + c.darken(2), + ); + } + + // ================ Success Color ================ + if (theme.successColor) { + fillColor(theme.successColor, 'success'); + } + + // ================ Warning Color ================ + if (theme.warningColor) { + fillColor(theme.warningColor, 'warning'); + } + + // ================= Error Color ================= + if (theme.errorColor) { + fillColor(theme.errorColor, 'error'); + } + + // ================= Info Color ================== + if (theme.infoColor) { + fillColor(theme.infoColor, 'info'); + } + + // Convert to css variables + const cssList = Object.keys(variables).map( + key => `--${globalPrefixCls}-${key}: ${variables[key]};`, + ); + + return ` + :root { + ${cssList.join('\n')} + } + `.trim(); +} + +export function registerTheme(globalPrefixCls: string, theme: Theme) { + const style = getStyle(globalPrefixCls, theme); + + if (canUseDom()) { + updateCSS(style, `${dynamicStyleMark}-dynamic-theme`); + } else { + warning(false, 'ConfigProvider', 'SSR do not support dynamic theme with css variables.'); + } +} diff --git a/components/config-provider/cssVariables.tsx b/components/config-provider/cssVariables.tsx deleted file mode 100644 index 4d1140e1f3..0000000000 --- a/components/config-provider/cssVariables.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* eslint-disable import/prefer-default-export, prefer-destructuring */ - -import { TinyColor } from '@ctrl/tinycolor'; -import { generate } from '@ant-design/colors'; -import type { Theme } from './context'; -import { updateCSS } from '../vc-util/Dom/dynamicCSS'; -import canUseDom from '../_util/canUseDom'; -import devWarning from '../vc-util/devWarning'; - -const dynamicStyleMark = `-ant-${Date.now()}-${Math.random()}`; - -export function registerTheme(globalPrefixCls: string, theme: Theme) { - const variables: Record = {}; - - const formatColor = ( - color: TinyColor, - updater?: (cloneColor: TinyColor) => TinyColor | undefined, - ) => { - let clone = color.clone(); - clone = updater?.(clone) || clone; - return clone.toRgbString(); - }; - - const fillColor = (colorVal: string, type: string) => { - const baseColor = new TinyColor(colorVal); - const colorPalettes = generate(baseColor.toRgbString()); - - variables[`${type}-color`] = formatColor(baseColor); - variables[`${type}-color-disabled`] = colorPalettes[1]; - variables[`${type}-color-hover`] = colorPalettes[4]; - variables[`${type}-color-active`] = colorPalettes[6]; - variables[`${type}-color-outline`] = baseColor.clone().setAlpha(0.2).toRgbString(); - variables[`${type}-color-deprecated-bg`] = colorPalettes[1]; - variables[`${type}-color-deprecated-border`] = colorPalettes[3]; - }; - - // ================ Primary Color ================ - if (theme.primaryColor) { - fillColor(theme.primaryColor, 'primary'); - - const primaryColor = new TinyColor(theme.primaryColor); - const primaryColors = generate(primaryColor.toRgbString()); - - // Legacy - We should use semantic naming standard - primaryColors.forEach((color, index) => { - variables[`primary-${index + 1}`] = color; - }); - // Deprecated - variables['primary-color-deprecated-l-35'] = formatColor(primaryColor, c => c.lighten(35)); - variables['primary-color-deprecated-l-20'] = formatColor(primaryColor, c => c.lighten(20)); - variables['primary-color-deprecated-t-20'] = formatColor(primaryColor, c => c.tint(20)); - variables['primary-color-deprecated-t-50'] = formatColor(primaryColor, c => c.tint(50)); - variables['primary-color-deprecated-f-12'] = formatColor(primaryColor, c => - c.setAlpha(c.getAlpha() * 0.12), - ); - - const primaryActiveColor = new TinyColor(primaryColors[0]); - variables['primary-color-active-deprecated-f-30'] = formatColor(primaryActiveColor, c => - c.setAlpha(c.getAlpha() * 0.3), - ); - variables['primary-color-active-deprecated-d-02'] = formatColor(primaryActiveColor, c => - c.darken(2), - ); - } - - // ================ Success Color ================ - if (theme.successColor) { - fillColor(theme.successColor, 'success'); - } - - // ================ Warning Color ================ - if (theme.warningColor) { - fillColor(theme.warningColor, 'warning'); - } - - // ================= Error Color ================= - if (theme.errorColor) { - fillColor(theme.errorColor, 'error'); - } - - // ================= Info Color ================== - if (theme.infoColor) { - fillColor(theme.infoColor, 'info'); - } - - // Convert to css variables - const cssList = Object.keys(variables).map( - key => `--${globalPrefixCls}-${key}: ${variables[key]};`, - ); - - if (canUseDom()) { - updateCSS( - ` - :root { - ${cssList.join('\n')} - } - `, - `${dynamicStyleMark}-dynamic-theme`, - ); - } else { - devWarning(false, 'ConfigProvider', 'SSR do not support dynamic theme with css variables.'); - } -} diff --git a/components/config-provider/demo/direction.vue b/components/config-provider/demo/direction.vue index f8564144b6..3d5280930e 100644 --- a/components/config-provider/demo/direction.vue +++ b/components/config-provider/demo/direction.vue @@ -226,7 +226,7 @@ Components which support rtl direction are listed here, you can toggle the direc Modal example
    Open Modal - +

    نگاشته‌های خود را اینجا قراردهید

    نگاشته‌های خود را اینجا قراردهید

    نگاشته‌های خود را اینجا قراردهید

    @@ -239,17 +239,43 @@ Components which support rtl direction are listed here, you can toggle the direc Steps example
    - - - - - +
    - - - - - +
    @@ -332,8 +358,8 @@ Components which support rtl direction are listed here, you can toggle the direc
    - -
    + +
    +
    +
    +
    +
    + +
    +`; + +exports[`renders ./components/float-button/demo/shape.vue correctly 1`] = ` + + + +`; + +exports[`renders ./components/float-button/demo/tooltip.vue correctly 1`] = ` + + +`; + +exports[`renders ./components/float-button/demo/type.vue correctly 1`] = ` + + +`; diff --git a/components/float-button/__tests__/__snapshots__/index.test.js.snap b/components/float-button/__tests__/__snapshots__/index.test.js.snap new file mode 100644 index 0000000000..c0cf145cb2 --- /dev/null +++ b/components/float-button/__tests__/__snapshots__/index.test.js.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FloatButton fixbug renders {0} , 0 and {false} 1`] = ` + +`; + +exports[`FloatButton fixbug renders {0} , 0 and {false} 2`] = ` + +`; + +exports[`FloatButton fixbug renders {0} , 0 and {false} 3`] = ` + +`; + +exports[`FloatButton renders correctly 1`] = ` + +`; diff --git a/components/float-button/__tests__/demo.test.js b/components/float-button/__tests__/demo.test.js new file mode 100644 index 0000000000..fdcb0c780c --- /dev/null +++ b/components/float-button/__tests__/demo.test.js @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest'; + +demoTest('float-button'); diff --git a/components/float-button/__tests__/index.test.js b/components/float-button/__tests__/index.test.js new file mode 100644 index 0000000000..d5bc3c4dc1 --- /dev/null +++ b/components/float-button/__tests__/index.test.js @@ -0,0 +1,48 @@ +import FloatButton from '../index'; +import { mount } from '@vue/test-utils'; +import mountTest from '../../../tests/shared/mountTest'; + +describe('FloatButton', () => { + mountTest(FloatButton); + mountTest(FloatButton.Group); + it('renders correctly', () => { + const wrapper = mount({ + render() { + return ; + }, + }); + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('create primary button', () => { + const wrapper = mount({ + render() { + return 按钮; + }, + }); + expect(wrapper.find('.ant-float-btn-primary').exists()).toBe(true); + }); + + it('fixbug renders {0} , 0 and {false}', () => { + const wrapper = mount({ + render() { + return {0}; + }, + }); + expect(wrapper.html()).toMatchSnapshot(); + + const wrapper1 = mount({ + render() { + return 0; + }, + }); + expect(wrapper1.html()).toMatchSnapshot(); + + const wrapper2 = mount({ + render() { + return {false}; + }, + }); + expect(wrapper2.html()).toMatchSnapshot(); + }); +}); diff --git a/components/float-button/__tests__/wave.test.js b/components/float-button/__tests__/wave.test.js new file mode 100644 index 0000000000..3102ccf3df --- /dev/null +++ b/components/float-button/__tests__/wave.test.js @@ -0,0 +1,79 @@ +import FloatButton from '../index'; +import { mount } from '@vue/test-utils'; +import { asyncExpect, sleep } from '../../../tests/utils'; + +describe('click wave effect', () => { + async function clickFloatButton(wrapper) { + await asyncExpect(() => { + wrapper.find('.ant-float-btn').trigger('click'); + }); + wrapper.find('.ant-float-btn').element.dispatchEvent(new Event('transitionstart')); + await sleep(20); + wrapper.find('.ant-float-btn').element.dispatchEvent(new Event('animationend')); + await sleep(20); + } + + it('should have click wave effect for primary button', async () => { + const wrapper = mount({ + render() { + return ; + }, + }); + await clickFloatButton(wrapper); + expect( + wrapper.find('.ant-float-btn').attributes('ant-click-animating-without-extra-node'), + ).toBe('true'); + }); + + it('should have click wave effect for default button', async () => { + const wrapper = mount({ + render() { + return button; + }, + }); + await clickFloatButton(wrapper); + expect( + wrapper.find('.ant-float-btn').attributes('ant-click-animating-without-extra-node'), + ).toBe('true'); + }); + + it('should not have click wave effect for link type button', async () => { + const wrapper = mount({ + render() { + return button; + }, + }); + await clickFloatButton(wrapper); + expect( + wrapper.find('.ant-float-btn').attributes('ant-click-animating-without-extra-node'), + ).toBe(undefined); + }); + + it('should not have click wave effect for text type button', async () => { + const wrapper = mount({ + render() { + return button; + }, + }); + await clickFloatButton(wrapper); + expect( + wrapper.find('.ant-float-btn').attributes('ant-click-animating-without-extra-node'), + ).toBe(undefined); + }); + + it('should handle transitionstart', async () => { + const wrapper = mount({ + render() { + return button; + }, + }); + await clickFloatButton(wrapper); + const buttonNode = wrapper.find('.ant-float-btn').element; + buttonNode.dispatchEvent(new Event('transitionstart')); + expect( + wrapper.find('.ant-float-btn').attributes('ant-click-animating-without-extra-node'), + ).toBe('true'); + wrapper.unmount(); + buttonNode.dispatchEvent(new Event('transitionstart')); + }); +}); diff --git a/components/float-button/context.ts b/components/float-button/context.ts new file mode 100644 index 0000000000..3882a9d65c --- /dev/null +++ b/components/float-button/context.ts @@ -0,0 +1,19 @@ +import type { Ref, InjectionKey } from 'vue'; +import { inject, provide, ref } from 'vue'; + +import type { FloatButtonShape } from './interface'; + +interface FloatButtonGroupContext { + shape: Ref; +} +const contextKey: InjectionKey = Symbol('floatButtonGroupContext'); + +export const useProvideFloatButtonGroupContext = (props: FloatButtonGroupContext) => { + provide(contextKey, props); + + return props; +}; + +export const useInjectFloatButtonGroupContext = () => { + return inject(contextKey, { shape: ref() } as FloatButtonGroupContext); +}; diff --git a/components/float-button/demo/back-top.vue b/components/float-button/demo/back-top.vue new file mode 100644 index 0000000000..a75bb2f7e5 --- /dev/null +++ b/components/float-button/demo/back-top.vue @@ -0,0 +1,31 @@ + +--- +order: 7 +iframe: 360 +title: + zh-CN: 回到顶部 + en-US: BackTop +--- + +## zh-CN + +返回页面顶部的操作按钮。 + +## en-US + +`BackTop` makes it easy to go back to the top of the page. + + + + diff --git a/components/float-button/demo/basic.vue b/components/float-button/demo/basic.vue new file mode 100644 index 0000000000..eaf39b7988 --- /dev/null +++ b/components/float-button/demo/basic.vue @@ -0,0 +1,26 @@ + +--- +order: 0 +iframe: 360 +title: + zh-CN: 基本 + en-US: Basic Usage +--- + +## zh-CN + +最简单的用法。 + +## en-US + +The most basic usage. + + + + + + diff --git a/components/float-button/demo/description.vue b/components/float-button/demo/description.vue new file mode 100644 index 0000000000..befa27c1f3 --- /dev/null +++ b/components/float-button/demo/description.vue @@ -0,0 +1,61 @@ + +--- +order: 3 +iframe: 360 +title: + zh-CN: 描述 + en-US: Description +--- + +## zh-CN + +可以通过 `description` 设置文字内容。 + +> 仅当 `shape` 属性为 `square` 时支持。由于空间较小,推荐使用比较精简的双数文字。 + +## en-US + +Setting `description` prop to show FloatButton with description. + +> supported only when `shape` is `square`. Due to narrow space for text, short sentence is recommended. + + + + + + + diff --git a/components/float-button/demo/group-menu.vue b/components/float-button/demo/group-menu.vue new file mode 100644 index 0000000000..2daa16f1b8 --- /dev/null +++ b/components/float-button/demo/group-menu.vue @@ -0,0 +1,48 @@ + +--- +order: 6 +iframe: 360 +title: + zh-CN: 菜单模式 + en-US: Menu mode +--- + +## zh-CN + +设置 `trigger` 属性即可开启菜单模式。提供 `hover` 和 `click` 两种触发方式。 + +## en-US + +Open menu mode with `trigger`, which could be `hover` or `click`. + + + + + + + diff --git a/components/float-button/demo/group.vue b/components/float-button/demo/group.vue new file mode 100644 index 0000000000..14d0f77311 --- /dev/null +++ b/components/float-button/demo/group.vue @@ -0,0 +1,49 @@ + +--- +order: 5 +iframe: 360 +title: + zh-CN: 浮动按钮组 + en-US: FloatButton Group +--- + +## zh-CN + +按钮组合使用时,推荐使用 ``,并通过设置 `shape` 属性改变悬浮按钮组的形状。悬浮按钮组的 `shape` 会覆盖内部 FloatButton 的 `shape` 属性。 + +## en-US + +When multiple buttons are used together, `` is recommended. By setting `shape` of FloatButton.Group, you can change the shape of group. `shape` of FloatButton.Group will override `shape` of FloatButton inside. + + + + + + diff --git a/components/float-button/demo/index.vue b/components/float-button/demo/index.vue new file mode 100644 index 0000000000..caa7ba7822 --- /dev/null +++ b/components/float-button/demo/index.vue @@ -0,0 +1,86 @@ + + + diff --git a/components/float-button/demo/shape.vue b/components/float-button/demo/shape.vue new file mode 100644 index 0000000000..f71b1fd57a --- /dev/null +++ b/components/float-button/demo/shape.vue @@ -0,0 +1,51 @@ + +--- +order: 2 +iframe: 360 +title: + zh-CN: 形状 + en-US: Shape +--- + +## zh-CN + +最简单的用法。 + +## en-US + +The most basic usage. + + + + + + diff --git a/components/float-button/demo/tooltip.vue b/components/float-button/demo/tooltip.vue new file mode 100644 index 0000000000..d1bfa11522 --- /dev/null +++ b/components/float-button/demo/tooltip.vue @@ -0,0 +1,37 @@ + +--- +order: 4 +iframe: 360 +title: + zh-CN: 含有气泡卡片的悬浮按钮 + en-US: FloatButton with tooltip +--- + +## zh-CN + +设置 tooltip 属性,即可开启气泡卡片。 + +## en-US + +Setting `tooltip` prop to show FloatButton with tooltip. + + + + diff --git a/components/float-button/demo/type.vue b/components/float-button/demo/type.vue new file mode 100644 index 0000000000..007923763a --- /dev/null +++ b/components/float-button/demo/type.vue @@ -0,0 +1,46 @@ + +--- +order: 1 +iframe: 360 +title: + zh-CN: 类型 + en-US: Type +--- + +## zh-CN + +通过 `type` 改变悬浮按钮的类型。 + +## en-US + +Change the type of the FloatButton with `type`. + + + + + + diff --git a/components/float-button/index.en-US.md b/components/float-button/index.en-US.md new file mode 100644 index 0000000000..0ad7887452 --- /dev/null +++ b/components/float-button/index.en-US.md @@ -0,0 +1,58 @@ +--- +category: Components +type: Other +title: FloatButton +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*HS-wTIIwu0kAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a0hwTY_rOSUAAAAAAAAAAAAADrJ8AQ/original +--- + +FloatButton. Available since `4.0.0`. + +## When To Use + +- For global functionality on the site. +- Buttons that can be seen wherever you browse. + +## API + +> This component is available since `ant-design-vue@4.0.0`. + +### common API + +| Property | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | +| icon | Set the icon component of button | slot | - | | +| description | Text and other | string \| slot | - | | +| tooltip | The text shown in the tooltip | string \| slot | | | +| type | Setting button type | `default` \| `primary` | `default` | | +| shape | Setting button shape | `circle` \| `square` | `circle` | | +| href | The target of hyperlink | string | - | | +| target | Specifies where to display the linked URL | string | - | | + +### common events + +| Events Name | Description | Arguments | Version | +| ----------- | --------------------------------------- | ----------------- | ------- | +| click | Set the handler to handle `click` event | `(event) => void` | - | + +### FloatButton.Group + +| Property | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | +| shape | Setting button shape of children | `circle` \| `square` | `circle` | | +| trigger | Which action can trigger menu open/close | `click` \| `hover` | - | | +| open(v-model) | Whether the menu is visible or not | boolean | - | | + +### FloatButton.Group Events + +| Events Name | Description | Arguments | Version | +| ----------- | --------------------------------------------- | ----------------------- | ------- | +| openChange | Callback executed when active menu is changed | (open: boolean) => void | - | + +### FloatButton.BackTop + +| Property | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | +| duration | Time to return to top(ms) | number | 450 | | +| target | Specifies the scrollable area dom node | () => HTMLElement | () => window | | +| visibilityHeight | The BackTop button will not show until the scroll height reaches this value | number | 400 | | diff --git a/components/float-button/index.ts b/components/float-button/index.ts new file mode 100644 index 0000000000..7f7ccb9f6c --- /dev/null +++ b/components/float-button/index.ts @@ -0,0 +1,42 @@ +import type { App, Plugin } from 'vue'; +import FloatButton from './FloatButton'; +import FloatButtonGroup from './FloatButtonGroup'; +import BackTop from './BackTop'; + +import type { + FloatButtonProps, + FloatButtonShape, + FloatButtonType, + FloatButtonGroupProps, + BackTopProps, +} from './interface'; + +import type { SizeType as FloatButtonSize } from '../config-provider'; + +export type { + FloatButtonProps, + FloatButtonShape, + FloatButtonType, + FloatButtonGroupProps, + BackTopProps, + FloatButtonSize, +}; + +FloatButton.Group = FloatButtonGroup; +FloatButton.BackTop = BackTop; + +/* istanbul ignore next */ +FloatButton.install = function (app: App) { + app.component(FloatButton.name, FloatButton); + app.component(FloatButtonGroup.name, FloatButtonGroup); + app.component(BackTop.name, BackTop); + return app; +}; + +export { FloatButtonGroup, BackTop }; + +export default FloatButton as typeof FloatButton & + Plugin & { + readonly Group: typeof FloatButtonGroup; + readonly BackTop: typeof BackTop; + }; diff --git a/components/float-button/index.zh-CN.md b/components/float-button/index.zh-CN.md new file mode 100644 index 0000000000..83fb6b3456 --- /dev/null +++ b/components/float-button/index.zh-CN.md @@ -0,0 +1,61 @@ +--- +category: Components +subtitle: 悬浮按钮 +type: 其他 +title: FloatButton +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*HS-wTIIwu0kAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a0hwTY_rOSUAAAAAAAAAAAAADrJ8AQ/original +--- + +悬浮按钮。自 `4.0.0` 版本开始提供该组件。 + +## 何时使用 + +- 用于网站上的全局功能; +- 无论浏览到何处都可以看见的按钮。 + +## API + +> 自 `ant-design-vue@4.0.0` 版本开始提供该组件。 + +### 共同的 API + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| --- | --- | --- | --- | --- | +| icon | 自定义图标 | slot | - | | +| description | 文字及其它内容 | string \| slot | - | | +| tooltip | 气泡卡片的内容 | string \| slot | - | | +| type | 设置按钮类型 | `default` \| `primary` | `default` | | +| shape | 设置按钮形状 | `circle` \| `square` | `circle` | | +| onClick | 点击按钮时的回调 | (event) => void | - | | +| href | 点击跳转的地址,指定此属性 button 的行为和 a 链接一致 | string | - | | +| target | 相当于 a 标签的 target 属性,href 存在时生效 | string | - | | + +### common events + +| 事件名称 | 说明 | 回调参数 | 版本 | +| -------- | --------------------------------------- | ----------------- | ---- | +| click | Set the handler to handle `click` event | `(event) => void` | - | + +### FloatButton.Group + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| ------------- | -------------------------------- | -------------------- | -------- | ---- | +| shape | 设置包含的 FloatButton 按钮形状 | `circle` \| `square` | `circle` | | +| trigger | 触发方式(有触发方式为菜单模式) | `click` \| `hover` | - | | +| open(v-model) | 受控展开 | boolean | - | | + +### FloatButton.Group Events + +| 事件名称 | 说明 | 回调参数 | 版本 | +| ---------- | ---------------- | ----------------------- | ---- | +| openChange | 展开收起时的回调 | (open: boolean) => void | - | + +### FloatButton.BackTop + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| ---------------- | ---------------------------------- | ----------------- | ------------ | ---- | +| duration | 回到顶部所需时间(ms) | number | 450 | | +| target | 设置需要监听其滚动事件的元素 | () => HTMLElement | () => window | | +| visibilityHeight | 滚动高度达到此参数值才出现 BackTop | number | 400 | | +| onClick | 点击按钮的回调函数 | () => void | - | | diff --git a/components/float-button/interface.ts b/components/float-button/interface.ts new file mode 100644 index 0000000000..a60e1afce4 --- /dev/null +++ b/components/float-button/interface.ts @@ -0,0 +1,66 @@ +import type { ExtractPropTypes } from 'vue'; +import PropTypes from '../_util/vue-types'; +import type { MouseEventHandler } from '../_util/EventInterface'; +import { stringType, booleanType, functionType } from '../_util/type'; + +export type FloatButtonType = 'default' | 'primary'; + +export type FloatButtonShape = 'circle' | 'square'; + +export type FloatButtonGroupTrigger = 'click' | 'hover'; + +export const floatButtonProps = () => { + return { + prefixCls: String, + description: PropTypes.any, + type: stringType('default'), + shape: stringType('circle'), + tooltip: PropTypes.any, + href: String, + target: functionType<() => Window | HTMLElement | null>(), + onClick: functionType(), + }; +}; + +export type FloatButtonProps = Partial>>; + +export const floatButtonContentProps = () => { + return { + prefixCls: stringType(), + }; +}; + +export type FloatButtonContentProps = Partial< + ExtractPropTypes> +>; + +export const floatButtonGroupProps = () => { + return { + ...floatButtonProps(), + // 包含的 Float Button + // 触发方式 (有触发方式为菜单模式) + trigger: stringType(), + // 受控展开 + open: booleanType(), + // 展开收起的回调 + onOpenChange: functionType<(open: boolean) => void>(), + 'onUpdate:open': functionType<(open: boolean) => void>(), + }; +}; + +export type FloatButtonGroupProps = Partial< + ExtractPropTypes> +>; + +export const backTopProps = () => { + return { + ...floatButtonProps(), + prefixCls: String, + duration: Number, + target: functionType<() => HTMLElement | Window | Document>(), + visibilityHeight: Number, + onClick: functionType(), + }; +}; + +export type BackTopProps = Partial>>; diff --git a/components/float-button/style/index.ts b/components/float-button/style/index.ts new file mode 100644 index 0000000000..86d786ef35 --- /dev/null +++ b/components/float-button/style/index.ts @@ -0,0 +1,333 @@ +import type { CSSObject } from '../../_util/cssinjs'; +import { Keyframes } from '../../_util/cssinjs'; +import type { FullToken, GenerateStyle } from '../../theme/internal'; +import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import { initFadeMotion } from '../../style/motion/fade'; +import { resetComponent } from '../../style'; +import { initMotion } from '../../style/motion/motion'; + +/** Component only token. Which will handle additional calculation of alias token */ +export interface ComponentToken { + zIndexPopup: number; +} + +type FloatButtonToken = FullToken<'FloatButton'> & { + floatButtonColor: string; + floatButtonBackgroundColor: string; + floatButtonHoverBackgroundColor: string; + floatButtonFontSize: number; + floatButtonSize: number; + floatButtonIconSize: number; + + // Position + floatButtonInsetBlockEnd: number; + floatButtonInsetInlineEnd: number; +}; + +const initFloatButtonGroupMotion = (token: FloatButtonToken) => { + const { componentCls, floatButtonSize, motionDurationSlow, motionEaseInOutCirc } = token; + const groupPrefixCls = `${componentCls}-group`; + const moveDownIn = new Keyframes('antFloatButtonMoveDownIn', { + '0%': { + transform: `translate3d(0, ${floatButtonSize}px, 0)`, + transformOrigin: '0 0', + opacity: 0, + }, + + '100%': { + transform: 'translate3d(0, 0, 0)', + transformOrigin: '0 0', + opacity: 1, + }, + }); + const moveDownOut = new Keyframes('antFloatButtonMoveDownOut', { + '0%': { + transform: 'translate3d(0, 0, 0)', + transformOrigin: '0 0', + opacity: 1, + }, + + '100%': { + transform: `translate3d(0, ${floatButtonSize}px, 0)`, + transformOrigin: '0 0', + opacity: 0, + }, + }); + + return [ + { + [`${groupPrefixCls}-wrap`]: { + ...initMotion(`${groupPrefixCls}-wrap`, moveDownIn, moveDownOut, motionDurationSlow, true), + }, + }, + { + [`${groupPrefixCls}-wrap`]: { + [` + &${groupPrefixCls}-wrap-enter, + &${groupPrefixCls}-wrap-appear + `]: { + opacity: 0, + animationTimingFunction: motionEaseInOutCirc, + }, + + [`&${groupPrefixCls}-wrap-leave`]: { + animationTimingFunction: motionEaseInOutCirc, + }, + }, + }, + ]; +}; + +// ============================== Group ============================== +const floatButtonGroupStyle: GenerateStyle = token => { + const { componentCls, floatButtonSize, margin, borderRadiusLG } = token; + const groupPrefixCls = `${componentCls}-group`; + return { + [groupPrefixCls]: { + ...resetComponent(token), + zIndex: 99, + display: 'block', + border: 'none', + position: 'fixed', + width: floatButtonSize, + height: 'auto', + boxShadow: 'none', + minHeight: floatButtonSize, + insetInlineEnd: token.floatButtonInsetInlineEnd, + insetBlockEnd: token.floatButtonInsetBlockEnd, + borderRadius: borderRadiusLG, + + [`${groupPrefixCls}-wrap`]: { + zIndex: -1, + display: 'block', + position: 'relative', + marginBottom: margin, + }, + [`&${groupPrefixCls}-rtl`]: { + direction: 'rtl', + }, + [componentCls]: { + position: 'static', + }, + }, + [`${groupPrefixCls}-circle`]: { + [`${componentCls}-circle:not(:last-child)`]: { + marginBottom: token.margin, + [`${componentCls}-body`]: { + width: floatButtonSize, + height: floatButtonSize, + }, + }, + }, + [`${groupPrefixCls}-square`]: { + [`${componentCls}-square`]: { + borderRadius: 0, + padding: 0, + '&:first-child': { + borderStartStartRadius: borderRadiusLG, + borderStartEndRadius: borderRadiusLG, + }, + '&:last-child': { + borderEndStartRadius: borderRadiusLG, + borderEndEndRadius: borderRadiusLG, + }, + '&:not(:last-child)': { + borderBottom: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`, + }, + }, + [`${groupPrefixCls}-wrap`]: { + display: 'block', + borderRadius: borderRadiusLG, + boxShadow: token.boxShadowSecondary, + overflow: 'hidden', + [`${componentCls}-square`]: { + boxShadow: 'none', + marginTop: 0, + borderRadius: 0, + padding: token.paddingXXS, + '&:first-child': { + borderStartStartRadius: borderRadiusLG, + borderStartEndRadius: borderRadiusLG, + }, + '&:last-child': { + borderEndStartRadius: borderRadiusLG, + borderEndEndRadius: borderRadiusLG, + }, + '&:not(:last-child)': { + borderBottom: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`, + }, + [`${componentCls}-body`]: { + width: floatButtonSize - token.paddingXXS * 2, + height: floatButtonSize - token.paddingXXS * 2, + }, + }, + }, + }, + + [`${groupPrefixCls}-circle-shadow`]: { + boxShadow: 'none', + }, + [`${groupPrefixCls}-square-shadow`]: { + boxShadow: token.boxShadowSecondary, + [`${componentCls}-square`]: { + boxShadow: 'none', + padding: token.paddingXXS, + [`${componentCls}-body`]: { + width: floatButtonSize - token.paddingXXS * 2, + height: floatButtonSize - token.paddingXXS * 2, + }, + }, + }, + }; +}; + +// ============================== Shared ============================== +const sharedFloatButtonStyle: GenerateStyle = token => { + const { componentCls, floatButtonIconSize, floatButtonSize, borderRadiusLG } = token; + return { + [componentCls]: { + ...resetComponent(token), + border: 'none', + position: 'fixed', + cursor: 'pointer', + overflow: 'hidden', + zIndex: 99, + display: 'block', + justifyContent: 'center', + alignItems: 'center', + width: floatButtonSize, + height: floatButtonSize, + insetInlineEnd: token.floatButtonInsetInlineEnd, + insetBlockEnd: token.floatButtonInsetBlockEnd, + boxShadow: token.boxShadowSecondary, + + // Pure Panel + '&-pure': { + position: 'relative', + inset: 'auto', + }, + + '&:empty': { + display: 'none', + }, + + [`${componentCls}-body`]: { + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + transition: `all ${token.motionDurationMid}`, + [`${componentCls}-content`]: { + overflow: 'hidden', + textAlign: 'center', + minHeight: floatButtonSize, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + padding: `2px 4px`, + [`${componentCls}-icon`]: { + textAlign: 'center', + margin: 'auto', + width: floatButtonIconSize, + fontSize: floatButtonIconSize, + lineHeight: 1, + }, + }, + }, + }, + [`${componentCls}-circle`]: { + height: floatButtonSize, + borderRadius: '50%', + [`${componentCls}-body`]: { + borderRadius: '50%', + }, + }, + [`${componentCls}-square`]: { + height: 'auto', + minHeight: floatButtonSize, + borderRadius: borderRadiusLG, + [`${componentCls}-body`]: { + height: 'auto', + borderRadius: token.borderRadiusSM, + }, + }, + [`${componentCls}-default`]: { + backgroundColor: token.floatButtonBackgroundColor, + transition: `background-color ${token.motionDurationMid}`, + [`${componentCls}-body`]: { + backgroundColor: token.floatButtonBackgroundColor, + transition: `background-color ${token.motionDurationMid}`, + '&:hover': { + backgroundColor: token.colorFillContent, + }, + [`${componentCls}-content`]: { + [`${componentCls}-icon`]: { + color: token.colorText, + }, + [`${componentCls}-description`]: { + display: 'flex', + alignItems: 'center', + lineHeight: `${token.fontSizeLG}px`, + color: token.colorText, + fontSize: token.fontSizeSM, + }, + }, + }, + }, + [`${componentCls}-primary`]: { + backgroundColor: token.colorPrimary, + [`${componentCls}-body`]: { + backgroundColor: token.colorPrimary, + transition: `background-color ${token.motionDurationMid}`, + '&:hover': { + backgroundColor: token.colorPrimaryHover, + }, + [`${componentCls}-content`]: { + [`${componentCls}-icon`]: { + color: token.colorTextLightSolid, + }, + [`${componentCls}-description`]: { + display: 'flex', + alignItems: 'center', + lineHeight: `${token.fontSizeLG}px`, + color: token.colorTextLightSolid, + fontSize: token.fontSizeSM, + }, + }, + }, + }, + }; +}; + +// ============================== Export ============================== +export default genComponentStyleHook<'FloatButton'>('FloatButton', token => { + const { + colorTextLightSolid, + colorBgElevated, + controlHeightLG, + marginXXL, + marginLG, + fontSize, + fontSizeIcon, + controlItemBgHover, + } = token; + const floatButtonToken = mergeToken(token, { + floatButtonBackgroundColor: colorBgElevated, + floatButtonColor: colorTextLightSolid, + floatButtonHoverBackgroundColor: controlItemBgHover, + floatButtonFontSize: fontSize, + floatButtonIconSize: fontSizeIcon * 1.5, + floatButtonSize: controlHeightLG, + + floatButtonInsetBlockEnd: marginXXL, + floatButtonInsetInlineEnd: marginLG, + }); + return [ + floatButtonGroupStyle(floatButtonToken), + sharedFloatButtonStyle(floatButtonToken), + initFadeMotion(token), + initFloatButtonGroupMotion(floatButtonToken), + ]; +}); diff --git a/components/form/ErrorList.tsx b/components/form/ErrorList.tsx index 8863e7d68e..43eaf26646 100644 --- a/components/form/ErrorList.tsx +++ b/components/form/ErrorList.tsx @@ -1,29 +1,29 @@ import { useInjectFormItemPrefix } from './context'; import type { VueNode } from '../_util/type'; -import { computed, defineComponent, ref, watch } from 'vue'; -import { getTransitionGroupProps, TransitionGroup } from '../_util/transition'; -import useConfigInject from '../_util/hooks/useConfigInject'; +import { computed, defineComponent, ref, Transition, watch, TransitionGroup } from 'vue'; +import { getTransitionGroupProps, getTransitionProps } from '../_util/transition'; + import collapseMotion from '../_util/collapseMotion'; +import useStyle from './style'; export interface ErrorListProps { errors?: VueNode[]; /** @private Internal Usage. Do not use in your production */ help?: VueNode; - /** @private Internal Usage. Do not use in your production */ - onDomErrorVisibleChange?: (visible: boolean) => void; + onErrorVisibleChanged?: (visible: boolean) => void; } export default defineComponent({ compatConfig: { MODE: 3 }, name: 'ErrorList', - props: ['errors', 'help', 'onDomErrorVisibleChange', 'helpStatus', 'warnings'], - setup(props) { - const { prefixCls: rootPrefixCls } = useConfigInject('', props); + inheritAttrs: false, + props: ['errors', 'help', 'onErrorVisibleChanged', 'helpStatus', 'warnings'], + setup(props, { attrs }) { const { prefixCls, status } = useInjectFormItemPrefix(); const baseClassName = computed(() => `${prefixCls.value}-item-explain`); const visible = computed(() => !!(props.errors && props.errors.length)); const innerStatus = ref(status.value); - + const [, hashId] = useStyle(prefixCls); // Memo status in same visible watch([visible, status], () => { if (visible.value) { @@ -32,25 +32,36 @@ export default defineComponent({ }); return () => { - const colMItem = collapseMotion(`${rootPrefixCls.value}-show-help-item`); + const colMItem = collapseMotion(`${prefixCls.value}-show-help-item`); const transitionGroupProps = getTransitionGroupProps( - `${rootPrefixCls.value}-show-help-item`, + `${prefixCls.value}-show-help-item`, colMItem, ); - (transitionGroupProps as any).class = baseClassName.value; - return props.errors?.length ? ( - - {props.errors?.map((error: any, index: number) => ( - - ))} - - ) : null; + (transitionGroupProps as any).role = 'alert'; + (transitionGroupProps as any).class = [ + hashId.value, + baseClassName.value, + attrs.class, + `${prefixCls.value}-show-help`, + ]; + return ( + props.onErrorVisibleChanged(true)} + onAfterLeave={() => props.onErrorVisibleChanged(false)} + > + + {props.errors?.map((error: any, index: number) => ( +
    + {error} +
    + ))} +
    +
    + ); }; }, }); diff --git a/components/form/Form.tsx b/components/form/Form.tsx index 73f5e85f2d..d630fa047a 100755 --- a/components/form/Form.tsx +++ b/components/form/Form.tsx @@ -1,4 +1,4 @@ -import type { PropType, ExtractPropTypes, HTMLAttributes, ComponentPublicInstance } from 'vue'; +import type { ExtractPropTypes, HTMLAttributes, ComponentPublicInstance } from 'vue'; import { defineComponent, computed, watch, ref } from 'vue'; import PropTypes from '../_util/vue-types'; import classNames from '../_util/classNames'; @@ -13,7 +13,15 @@ import isEqual from 'lodash-es/isEqual'; import type { Options } from 'scroll-into-view-if-needed'; import scrollIntoView from 'scroll-into-view-if-needed'; import initDefaultProps from '../_util/props-util/initDefaultProps'; -import { tuple } from '../_util/type'; +import { + anyType, + booleanType, + functionType, + objectType, + someType, + stringType, + tuple, +} from '../_util/type'; import type { ColProps } from '../grid/Col'; import type { InternalNamePath, @@ -26,45 +34,44 @@ import type { Rule, FormLabelAlign, } from './interface'; -import { useInjectSize } from '../_util/hooks/useSize'; -import useConfigInject from '../_util/hooks/useConfigInject'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; import { useProvideForm } from './context'; import type { SizeType } from '../config-provider'; import useForm from './useForm'; import { useInjectGlobalForm } from '../config-provider/context'; - +import useStyle from './style'; +import { useProviderSize } from '../config-provider/SizeContext'; +import { useProviderDisabled } from '../config-provider/DisabledContext'; export type RequiredMark = boolean | 'optional'; export type FormLayout = 'horizontal' | 'inline' | 'vertical'; export const formProps = () => ({ - layout: PropTypes.oneOf(tuple('horizontal', 'inline', 'vertical') as FormLayout[]), - labelCol: { type: Object as PropType }, - wrapperCol: { type: Object as PropType }, - colon: { type: Boolean, default: undefined }, - labelAlign: PropTypes.oneOf(tuple('left', 'right') as FormLabelAlign[]), - labelWrap: { type: Boolean, default: undefined }, + layout: PropTypes.oneOf(tuple('horizontal', 'inline', 'vertical')), + labelCol: objectType(), + wrapperCol: objectType(), + colon: booleanType(), + labelAlign: stringType(), + labelWrap: booleanType(), prefixCls: String, - requiredMark: { type: [String, Boolean] as PropType, default: undefined }, + requiredMark: someType([String, Boolean]), /** @deprecated Will warning in future branch. Pls use `requiredMark` instead. */ - hideRequiredMark: { type: Boolean, default: undefined }, + hideRequiredMark: booleanType(), model: PropTypes.object, - rules: { type: Object as PropType<{ [k: string]: Rule[] | Rule }> }, - validateMessages: { - type: Object as PropType, - default: undefined as ValidateMessages, - }, - validateOnRuleChange: { type: Boolean, default: undefined }, + rules: objectType<{ [k: string]: Rule[] | Rule }>(), + validateMessages: objectType(), + validateOnRuleChange: booleanType(), // 提交失败自动滚动到第一个错误字段 - scrollToFirstError: { type: [Boolean, Object] as PropType }, - onSubmit: Function as PropType<(e: Event) => void>, + scrollToFirstError: anyType(), + onSubmit: functionType<(e: Event) => void>(), name: String, - validateTrigger: { type: [String, Array] as PropType }, - size: { type: String as PropType }, - onValuesChange: { type: Function as PropType }, - onFieldsChange: { type: Function as PropType }, - onFinish: { type: Function as PropType }, - onFinishFailed: { type: Function as PropType }, - onValidate: { type: Function as PropType }, + validateTrigger: someType([String, Array]), + size: stringType(), + disabled: booleanType(), + onValuesChange: functionType(), + onFieldsChange: functionType(), + onFinish: functionType(), + onFinishFailed: functionType(), + onValidate: functionType(), }); export type FormProps = Partial>>; @@ -109,8 +116,13 @@ const Form = defineComponent({ useForm, // emits: ['finishFailed', 'submit', 'finish', 'validate'], setup(props, { emit, slots, expose, attrs }) { - const size = useInjectSize(props); - const { prefixCls, direction, form: contextForm } = useConfigInject('form', props); + const { + prefixCls, + direction, + form: contextForm, + size, + disabled, + } = useConfigInject('form', props); const requiredMark = computed(() => props.requiredMark === '' || props.requiredMark); const mergedRequiredMark = computed(() => { if (requiredMark.value !== undefined) { @@ -126,6 +138,8 @@ const Form = defineComponent({ } return true; }); + useProviderSize(size); + useProviderDisabled(disabled); const mergedColon = computed(() => props.colon ?? contextForm.value?.colon); const { validateMessages: globalValidateMessages } = useInjectGlobalForm(); const validateMessages = computed(() => { @@ -135,13 +149,21 @@ const Form = defineComponent({ ...props.validateMessages, }; }); + + // Style + const [wrapSSR, hashId] = useStyle(prefixCls); + const formClassName = computed(() => - classNames(prefixCls.value, { - [`${prefixCls.value}-${props.layout}`]: true, - [`${prefixCls.value}-hide-required-mark`]: mergedRequiredMark.value === false, - [`${prefixCls.value}-rtl`]: direction.value === 'rtl', - [`${prefixCls.value}-${size.value}`]: size.value, - }), + classNames( + prefixCls.value, + { + [`${prefixCls.value}-${props.layout}`]: true, + [`${prefixCls.value}-hide-required-mark`]: mergedRequiredMark.value === false, + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + [`${prefixCls.value}-${size.value}`]: size.value, + }, + hashId.value, + ), ); const lastValidatePromise = ref(); const fields: Record = {}; @@ -376,10 +398,10 @@ const Form = defineComponent({ ); return () => { - return ( + return wrapSSR(
    {slots.default?.()} -
    + , ); }; }, diff --git a/components/form/FormItem.tsx b/components/form/FormItem.tsx index 28d983454f..1f8a1eb2ef 100644 --- a/components/form/FormItem.tsx +++ b/components/form/FormItem.tsx @@ -7,15 +7,21 @@ import type { HTMLAttributes, } from 'vue'; import { + onMounted, + reactive, watch, defineComponent, computed, nextTick, - ref, + shallowRef, watchEffect, onBeforeUnmount, toRaw, } from 'vue'; +import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; +import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled'; +import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled'; +import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFilled'; import cloneDeep from 'lodash-es/cloneDeep'; import PropTypes from '../_util/vue-types'; import Row from '../grid/Row'; @@ -36,12 +42,15 @@ import type { RuleObject, ValidateOptions, } from './interface'; -import useConfigInject from '../_util/hooks/useConfigInject'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; import { useInjectForm } from './context'; import FormItemLabel from './FormItemLabel'; import FormItemInput from './FormItemInput'; -import { useProvideFormItemContext } from './FormItemContext'; +import type { FormItemStatusContextProps } from './FormItemContext'; +import { FormItemInputContext, useProvideFormItemContext } from './FormItemContext'; import useDebounce from './utils/useDebounce'; +import classNames from '../_util/classNames'; +import useStyle from './style'; const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', ''); export type ValidateStatus = (typeof ValidateStatuses)[number]; @@ -57,6 +66,13 @@ export interface FieldExpose { validateRules: (options: ValidateOptions) => Promise | Promise; } +const iconMap: { [key: string]: any } = { + success: CheckCircleFilled, + warning: ExclamationCircleFilled, + error: CloseCircleFilled, + validating: LoadingOutlined, +}; + function getPropByPath(obj: any, namePathList: any, strict?: boolean) { let tempObj = obj; @@ -144,15 +160,18 @@ export default defineComponent({ warning(props.prop === undefined, `\`prop\` is deprecated. Please use \`name\` instead.`); const eventKey = `form-item-${++indexGuid}`; const { prefixCls } = useConfigInject('form', props); + const [wrapSSR, hashId] = useStyle(prefixCls); + const itemRef = shallowRef(); const formContext = useInjectForm(); const fieldName = computed(() => props.name || props.prop); - const errors = ref([]); - const validateDisabled = ref(false); - const inputRef = ref(); + const errors = shallowRef([]); + const validateDisabled = shallowRef(false); + const inputRef = shallowRef(); const namePath = computed(() => { const val = fieldName.value; return getNamePath(val); }); + const fieldId = computed(() => { if (!namePath.value.length) { return undefined; @@ -172,7 +191,7 @@ export default defineComponent({ }; const fieldValue = computed(() => getNewFieldValue()); - const initialValue = ref(cloneDeep(fieldValue.value)); + const initialValue = shallowRef(cloneDeep(fieldValue.value)); const mergedValidateTrigger = computed(() => { let validateTrigger = props.validateTrigger !== undefined @@ -212,7 +231,7 @@ export default defineComponent({ return isRequired || props.required; }); - const validateState = ref(); + const validateState = shallowRef(); watchEffect(() => { validateState.value = props.validateStatus; }); @@ -395,7 +414,7 @@ export default defineComponent({ }); const itemClassName = computed(() => ({ [`${prefixCls.value}-item`]: true, - + [hashId.value]: true, // Status [`${prefixCls.value}-item-has-feedback`]: mergedValidateStatus.value && props.hasFeedback, [`${prefixCls.value}-item-has-success`]: mergedValidateStatus.value === 'success', @@ -404,51 +423,118 @@ export default defineComponent({ [`${prefixCls.value}-item-is-validating`]: mergedValidateStatus.value === 'validating', [`${prefixCls.value}-item-hidden`]: props.hidden, })); + const formItemInputContext = reactive({}); + FormItemInputContext.useProvide(formItemInputContext); + watchEffect(() => { + let feedbackIcon: any; + if (props.hasFeedback) { + const IconNode = mergedValidateStatus.value && iconMap[mergedValidateStatus.value]; + feedbackIcon = IconNode ? ( + + + + ) : null; + } + Object.assign(formItemInputContext, { + status: mergedValidateStatus.value, + hasFeedback: props.hasFeedback, + feedbackIcon, + isFormItemInput: true, + }); + }); + + const marginBottom = shallowRef(null); + const showMarginOffset = shallowRef(false); + const updateMarginBottom = () => { + if (itemRef.value) { + const itemStyle = getComputedStyle(itemRef.value); + marginBottom.value = parseInt(itemStyle.marginBottom, 10); + } + }; + onMounted(() => { + watch( + showMarginOffset, + () => { + if (showMarginOffset.value) { + updateMarginBottom(); + } + }, + { flush: 'post', immediate: true }, + ); + }); + + const onErrorVisibleChanged = (nextVisible: boolean) => { + if (!nextVisible) { + marginBottom.value = null; + } + }; return () => { if (props.noStyle) return slots.default?.(); const help = props.help ?? (slots.help ? filterEmpty(slots.help()) : null); - return ( - ( - <> - {/* Label */} - - {/* Input Group */} - [firstChildren, children.slice(1)] }} - > - - ), - }} - > + ref={itemRef} + > + ( + <> + {/* Label */} + + {/* Input Group */} + + + ), + }} + > + {!!marginBottom.value && ( +
    + )} +
    , ); }; }, diff --git a/components/form/FormItemContext.ts b/components/form/FormItemContext.ts index 860a9f86e8..590f173c29 100644 --- a/components/form/FormItemContext.ts +++ b/components/form/FormItemContext.ts @@ -10,6 +10,8 @@ import { defineComponent, } from 'vue'; import devWarning from '../vc-util/devWarning'; +import createContext from '../_util/createContext'; +import type { ValidateStatus } from './FormItem'; export type FormItemContext = { id: ComputedRef; @@ -104,3 +106,22 @@ export default defineComponent({ }; }, }); + +export interface FormItemStatusContextProps { + isFormItemInput?: boolean; + status?: ValidateStatus; + hasFeedback?: boolean; + feedbackIcon?: any; +} + +export const FormItemInputContext = createContext({}); + +export const NoFormStatus = defineComponent({ + name: 'NoFormStatus', + setup(_, { slots }) { + FormItemInputContext.useProvide({}); + return () => { + return slots.default?.(); + }; + }, +}); diff --git a/components/form/FormItemInput.tsx b/components/form/FormItemInput.tsx index 1cc56f6f23..656116c149 100644 --- a/components/form/FormItemInput.tsx +++ b/components/form/FormItemInput.tsx @@ -1,8 +1,3 @@ -import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; -import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled'; -import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled'; -import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFilled'; - import type { ColProps } from '../grid/Col'; import Col from '../grid/Col'; import { useProvideForm, useInjectForm, useProvideFormItemPrefix } from './context'; @@ -12,6 +7,7 @@ import type { ValidateStatus } from './FormItem'; import type { CustomSlotsType, VueNode } from '../_util/type'; import type { HTMLAttributes } from 'vue'; import { computed, defineComponent } from 'vue'; +import { filterEmpty } from '../_util/props-util'; export interface FormItemInputMiscProps { prefixCls: string; @@ -27,12 +23,6 @@ export interface FormItemInputProps { status?: ValidateStatus; } -const iconMap: { [key: string]: any } = { - success: CheckCircleFilled, - warning: ExclamationCircleFilled, - error: CloseCircleFilled, - validating: LoadingOutlined, -}; const FormItemInput = defineComponent({ compatConfig: { MODE: 3 }, slots: Object as CustomSlotsType<{ @@ -51,6 +41,8 @@ const FormItemInput = defineComponent({ 'help', 'extra', 'status', + 'marginBottom', + 'onErrorVisibleChanged', ], setup(props, { slots }) { const formContext = useInjectForm(); @@ -70,10 +62,12 @@ const FormItemInput = defineComponent({ const { prefixCls, wrapperCol, + marginBottom, + onErrorVisibleChanged, help = slots.help?.(), - errors = slots.errors?.(), - hasFeedback, - status, + errors = filterEmpty(slots.errors?.()), + // hasFeedback, + // status, extra = slots.extra?.(), } = props; const baseClassName = `${prefixCls}-item`; @@ -84,8 +78,7 @@ const FormItemInput = defineComponent({ const className = classNames(`${baseClassName}-control`, mergedWrapperCol.class); // Should provides additional icon if `hasFeedback` - const IconNode = status && iconMap[status]; - + // const IconNode = status && iconMap[status]; return (
    {slots.default?.()}
    - {hasFeedback && IconNode ? ( - - - - ) : null}
    - + {marginBottom !== null || errors.length ? ( +
    + + {!!marginBottom &&
    } +
    + ) : null} {extra ?
    {extra}
    : null} ), diff --git a/components/form/FormItemLabel.tsx b/components/form/FormItemLabel.tsx index 2b0dd28a94..a9d3eee44e 100644 --- a/components/form/FormItemLabel.tsx +++ b/components/form/FormItemLabel.tsx @@ -4,7 +4,7 @@ import type { FormLabelAlign } from './interface'; import { useInjectForm } from './context'; import type { RequiredMark } from './Form'; import { useLocaleReceiver } from '../locale-provider/LocaleReceiver'; -import defaultLocale from '../locale/default'; +import defaultLocale from '../locale/en_US'; import classNames from '../_util/classNames'; import type { VueNode } from '../_util/type'; import type { FunctionalComponent, HTMLAttributes } from 'vue'; diff --git a/components/form/__tests__/__snapshots__/demo.test.js.snap b/components/form/__tests__/__snapshots__/demo.test.js.snap index 2927c6c8b0..ce9d389508 100644 --- a/components/form/__tests__/__snapshots__/demo.test.js.snap +++ b/components/form/__tests__/__snapshots__/demo.test.js.snap @@ -5,160 +5,180 @@ exports[`renders ./components/form/demo/advanced-search.vue correctly 1`] = `
    -
    -