diff --git a/components/_util/__tests__/capitalize.test.ts b/components/_util/__tests__/capitalize.test.ts new file mode 100644 index 0000000000..6369d5470f --- /dev/null +++ b/components/_util/__tests__/capitalize.test.ts @@ -0,0 +1,17 @@ +import capitalize from '../capitalize'; + +describe('capitalize', () => { + it('should capitalize the first character of a string', () => { + expect(capitalize('antd')).toBe('Antd'); + expect(capitalize('Antd')).toBe('Antd'); + expect(capitalize(' antd')).toBe(' antd'); + expect(capitalize('')).toBe(''); + }); + + it('should return the original value when is not a string', () => { + expect(capitalize(1 as any)).toBe(1); + expect(capitalize(true as any)).toBe(true); + expect(capitalize(undefined as any)).toBe(undefined); + expect(capitalize(null as any)).toBe(null); + }); +}); diff --git a/components/_util/capitalize.ts b/components/_util/capitalize.ts new file mode 100644 index 0000000000..b00c691b74 --- /dev/null +++ b/components/_util/capitalize.ts @@ -0,0 +1,8 @@ +export default function capitalize(str: T): Capitalize { + if (typeof str !== 'string') { + return str; + } + + const ret = str.charAt(0).toUpperCase() + str.slice(1); + return ret as Capitalize; +} diff --git a/components/_util/theme/default.tsx b/components/_util/theme/default.tsx index 12258f3ee7..5c8d027dcd 100644 --- a/components/_util/theme/default.tsx +++ b/components/_util/theme/default.tsx @@ -1,5 +1,22 @@ import { TinyColor } from '@ctrl/tinycolor'; -import type { DesignToken } from '.'; + +import type { DesignToken, PresetColorType } from '.'; + +const presetColors: PresetColorType = { + blue: '#1890FF', + purple: '#722ED1', + cyan: '#13C2C2', + green: '#52C41A', + magenta: '#EB2F96', + pink: '#eb2f96', + red: '#F5222D', + orange: '#FA8C16', + yellow: '#FADB14', + volcano: '#FA541C', + geekblue: '#2F54EB', + gold: '#FAAD14', + lime: '#A0D911', +}; const defaultDesignToken: DesignToken = { primaryColor: '#1890ff', @@ -59,6 +76,9 @@ const defaultDesignToken: DesignToken = { duration: 0.3, zIndexDropdown: 1050, + + // preset color palettes + ...presetColors, }; export default defaultDesignToken; diff --git a/components/_util/theme/index.tsx b/components/_util/theme/index.tsx index d263d0154d..51182b9b49 100644 --- a/components/_util/theme/index.tsx +++ b/components/_util/theme/index.tsx @@ -38,7 +38,39 @@ export { slideRightOut, }; -export interface DesignToken { +export interface PresetColorType { + blue: string; + purple: string; + cyan: string; + green: string; + magenta: string; + pink: string; + red: string; + orange: string; + yellow: string; + volcano: string; + geekblue: string; + lime: string; + gold: string; +} + +export const PresetColorKeys: ReadonlyArray = [ + 'blue', + 'purple', + 'cyan', + 'green', + 'magenta', + 'pink', + 'red', + 'orange', + 'yellow', + 'volcano', + 'geekblue', + 'lime', + 'gold', +]; + +export interface DesignToken extends PresetColorType { primaryColor: string; successColor: string; warningColor: string; @@ -93,8 +125,14 @@ export interface DesignToken { boxShadow?: string; } +type ColorPaletteKeyIndexes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + +type ColorPalettes = { + [key in `${keyof PresetColorType}-${ColorPaletteKeyIndexes[number]}`]: string; +}; + /** This is temporary token definition since final token definition is not ready yet. */ -export interface DerivativeToken extends Omit { +export interface DerivativeToken extends ColorPalettes, Omit { primaryHoverColor: string; primaryActiveColor: string; primaryOutlineColor: string; @@ -131,21 +169,53 @@ export interface DerivativeToken extends Omit { // TMP tmpPrimaryColorWeak: string; tmpPrimaryHoverColorWeak: string; + // Checked background for Checkable Tag + tmpPrimaryColor6: string; + // Active background for Checkable Tag + tmpPrimaryColor7: string; + + tmpSuccessColorDeprecatedBg: string; + tmpWarningColorDeprecatedBg: string; + tmpErrorColorDeprecatedBg: string; + tmpInfoColorDeprecatedBg: string; + + tmpSuccessColorDeprecatedBorder: string; + tmpWarningColorDeprecatedBorder: string; + tmpErrorColorDeprecatedBorder: string; + tmpInfoColorDeprecatedBorder: string; } export { useStyleRegister }; // =============================== Derivative =============================== function derivative(designToken: DesignToken): DerivativeToken { - const { primaryColor, errorColor, warningColor } = designToken; + const { primaryColor, errorColor, warningColor, infoColor, successColor } = designToken; const primaryColors = generate(primaryColor); const errorColors = generate(errorColor); const warningColors = generate(warningColor); + const infoColors = generate(infoColor); + const successColors = generate(successColor); const paddingSM = (designToken.padding / 4) * 3; const paddingXS = designToken.padding * 0.5; + const colorPalettes = PresetColorKeys.map((colorKey: keyof PresetColorType) => { + const colors = generate(designToken[colorKey]); + + const ret = new Array(10).fill(1).reduce((prev, _, i) => { + prev[`${colorKey}-${i + 1}`] = colors[i]; + return prev; + }, {}) as ColorPalettes; + return ret; + }).reduce((prev, cur) => { + prev = { + ...prev, + ...cur, + }; + return prev; + }, {} as ColorPalettes); + return { // FIXME: Need design token boxShadow: ` @@ -155,8 +225,6 @@ function derivative(designToken: DesignToken): DerivativeToken { ...designToken, - tmpPrimaryColorWeak: primaryColors[2], - tmpPrimaryHoverColorWeak: primaryColors[0], primaryHoverColor: primaryColors[4], primaryActiveColor: primaryColors[6], primaryOutlineColor: new TinyColor(primaryColor).setAlpha(0.2).toRgbString(), @@ -191,6 +259,24 @@ function derivative(designToken: DesignToken): DerivativeToken { duration: `${designToken.duration}s`, durationMid: `${(designToken.duration / 3) * 2}s`, durationFast: `${designToken.duration / 3}s`, + + ...colorPalettes, + + // TMP + tmpPrimaryColorWeak: primaryColors[2], + tmpPrimaryHoverColorWeak: primaryColors[0], + tmpPrimaryColor6: primaryColors[5], + tmpPrimaryColor7: primaryColors[6], + + tmpSuccessColorDeprecatedBg: successColors[0], + tmpWarningColorDeprecatedBg: warningColors[0], + tmpErrorColorDeprecatedBg: errorColors[0], + tmpInfoColorDeprecatedBg: infoColors[0], + + tmpSuccessColorDeprecatedBorder: successColors[2], + tmpWarningColorDeprecatedBorder: warningColors[2], + tmpErrorColorDeprecatedBorder: errorColors[2], + tmpInfoColorDeprecatedBorder: infoColors[2], }; } diff --git a/components/tag/CheckableTag.tsx b/components/tag/CheckableTag.tsx index d68c98d260..5a9fd8fba9 100644 --- a/components/tag/CheckableTag.tsx +++ b/components/tag/CheckableTag.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import classNames from 'classnames'; import { ConfigContext } from '../config-provider'; +import useStyle from './style'; export interface CheckableTagProps { prefixCls?: string; @@ -24,7 +25,7 @@ const CheckableTag: React.FC = ({ onClick, ...restProps }) => { - const { getPrefixCls } = React.useContext(ConfigContext); + const { getPrefixCls, iconPrefixCls } = React.useContext(ConfigContext); const handleClick = (e: React.MouseEvent) => { onChange?.(!checked); @@ -32,6 +33,9 @@ const CheckableTag: React.FC = ({ }; const prefixCls = getPrefixCls('tag', customizePrefixCls); + // Style + const [wrapSSR, hashId] = useStyle(prefixCls, iconPrefixCls); + const cls = classNames( prefixCls, { @@ -39,9 +43,10 @@ const CheckableTag: React.FC = ({ [`${prefixCls}-checkable-checked`]: checked, }, className, + hashId, ); - return ; + return wrapSSR(); }; export default CheckableTag; diff --git a/components/tag/index.tsx b/components/tag/index.tsx index bc8db06d38..d6a80c6e59 100644 --- a/components/tag/index.tsx +++ b/components/tag/index.tsx @@ -14,6 +14,8 @@ import { import Wave from '../_util/wave'; import { LiteralUnion } from '../_util/type'; +import useStyle from './style'; + export { CheckableTagProps } from './CheckableTag'; export interface TagProps extends React.HTMLAttributes { @@ -51,7 +53,7 @@ const InternalTag: React.ForwardRefRenderFunction = ( }, ref, ) => { - const { getPrefixCls, direction } = React.useContext(ConfigContext); + const { getPrefixCls, direction, iconPrefixCls } = React.useContext(ConfigContext); const [visible, setVisible] = React.useState(true); React.useEffect(() => { @@ -74,6 +76,9 @@ const InternalTag: React.ForwardRefRenderFunction = ( const presetColor = isPresetColor(); const prefixCls = getPrefixCls('tag', customizePrefixCls); + // Style + const [wrapSSR, hashId] = useStyle(prefixCls, iconPrefixCls); + const tagClassName = classNames( prefixCls, { @@ -83,6 +88,7 @@ const InternalTag: React.ForwardRefRenderFunction = ( [`${prefixCls}-rtl`]: direction === 'rtl', }, className, + hashId, ); const handleCloseClick = (e: React.MouseEvent) => { @@ -130,7 +136,7 @@ const InternalTag: React.ForwardRefRenderFunction = ( ); - return isNeedWave ? {tagNode} : tagNode; + return wrapSSR(isNeedWave ? {tagNode} : tagNode); }; const Tag = React.forwardRef(InternalTag) as TagType; diff --git a/components/tag/style/index.less b/components/tag/style/index.less index ec220bbd27..7965580834 100644 --- a/components/tag/style/index.less +++ b/components/tag/style/index.less @@ -1,129 +1,129 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@tag-prefix-cls: ~'@{ant-prefix}-tag'; - -.@{tag-prefix-cls} { - .reset-component(); - - display: inline-block; - height: auto; - margin-right: 8px; - padding: 0 7px; - font-size: @tag-font-size; - line-height: @tag-line-height; - white-space: nowrap; - background: @tag-default-bg; - border: @border-width-base @border-style-base @border-color-base; - border-radius: @border-radius-base; - opacity: 1; - transition: all 0.3s; - - &, - a, - a:hover { - color: @tag-default-color; - } - - > a:first-child:last-child { - display: inline-block; - margin: 0 -8px; - padding: 0 8px; - } - - &-close-icon { - margin-left: 3px; - color: @text-color-secondary; - font-size: 10px; - cursor: pointer; - transition: all 0.3s; - - &:hover { - color: @heading-color; - } - } - - &-has-color { - border-color: transparent; - &, - a, - a:hover, - .@{iconfont-css-prefix}-close, - .@{iconfont-css-prefix}-close:hover { - color: @text-color-inverse; - } - } - - &-checkable { - background-color: transparent; - border-color: transparent; - cursor: pointer; - - &:not(&-checked):hover { - color: @primary-color; - } - - &:active, - &-checked { - color: @text-color-inverse; - } - - &-checked { - background-color: @primary-6; - } - - &:active { - background-color: @primary-7; - } - } - - &-hidden { - display: none; - } - - // 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); - @lightColor: '@{color}-1'; - @lightBorderColor: '@{color}-3'; - @darkColor: '@{color}-6'; - @textColor: '@{color}-7'; - &-@{color} { - color: @@textColor; - background: @@lightColor; - border-color: @@lightBorderColor; - } - &-@{color}-inverse { - color: @text-color-inverse; - background: @@darkColor; - border-color: @@darkColor; - } - } - - .make-status-color-classes(@status, @cssVariableType) { - @bgColor: '@{cssVariableType}-color-deprecated-bg'; - @borderColor: '@{cssVariableType}-color-deprecated-border'; - @textColor: '@{cssVariableType}-color'; - &-@{status} { - color: @@textColor; - background: @@bgColor; - border-color: @@borderColor; - } - } - - .make-color-classes(); - - .make-status-color-classes(success, success); - .make-status-color-classes(processing, info); - .make-status-color-classes(error, error); - .make-status-color-classes(warning, warning); - - // To ensure that a space will be placed between character and `Icon`. - > .@{iconfont-css-prefix} + span, - > span + .@{iconfont-css-prefix} { - margin-left: 7px; - } -} - -@import './rtl'; +// @import '../../style/themes/index'; +// @import '../../style/mixins/index'; + +// @tag-prefix-cls: ~'@{ant-prefix}-tag'; + +// .@{tag-prefix-cls} { +// .reset-component(); + +// display: inline-block; +// height: auto; +// margin-right: 8px; +// padding: 0 7px; +// font-size: @tag-font-size; +// line-height: @tag-line-height; +// white-space: nowrap; +// background: @tag-default-bg; +// border: @border-width-base @border-style-base @border-color-base; +// border-radius: @border-radius-base; +// opacity: 1; +// transition: all 0.3s; + +// &, +// a, +// a:hover { +// color: @tag-default-color; +// } + +// > a:first-child:last-child { +// display: inline-block; +// margin: 0 -8px; +// padding: 0 8px; +// } + +// &-close-icon { +// margin-left: 3px; +// color: @text-color-secondary; +// font-size: 10px; +// cursor: pointer; +// transition: all 0.3s; + +// &:hover { +// color: @heading-color; +// } +// } + +// &-has-color { +// border-color: transparent; +// &, +// a, +// a:hover, +// .@{iconfont-css-prefix}-close, +// .@{iconfont-css-prefix}-close:hover { +// color: @text-color-inverse; +// } +// } + +// &-checkable { +// background-color: transparent; +// border-color: transparent; +// cursor: pointer; + +// &:not(&-checked):hover { +// color: @primary-color; +// } + +// &:active, +// &-checked { +// color: @text-color-inverse; +// } + +// &-checked { +// background-color: @primary-6; +// } + +// &:active { +// background-color: @primary-7; +// } +// } + +// &-hidden { +// display: none; +// } + +// // 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); +// @lightColor: '@{color}-1'; +// @lightBorderColor: '@{color}-3'; +// @darkColor: '@{color}-6'; +// @textColor: '@{color}-7'; +// &-@{color} { +// color: @@textColor; +// background: @@lightColor; +// border-color: @@lightBorderColor; +// } +// &-@{color}-inverse { +// color: @text-color-inverse; +// background: @@darkColor; +// border-color: @@darkColor; +// } +// } + +// .make-status-color-classes(@status, @cssVariableType) { +// @bgColor: '@{cssVariableType}-color-deprecated-bg'; +// @borderColor: '@{cssVariableType}-color-deprecated-border'; +// @textColor: '@{cssVariableType}-color'; +// &-@{status} { +// color: @@textColor; +// background: @@bgColor; +// border-color: @@borderColor; +// } +// } + +// .make-color-classes(); + +// .make-status-color-classes(success, success); +// .make-status-color-classes(processing, info); +// .make-status-color-classes(error, error); +// .make-status-color-classes(warning, warning); + +// // To ensure that a space will be placed between character and `Icon`. +// > .@{iconfont-css-prefix} + span, +// > span + .@{iconfont-css-prefix} { +// margin-left: 7px; +// } +// } + +// @import './rtl'; diff --git a/components/tag/style/index.tsx b/components/tag/style/index.tsx index 3a3ab0de59..83541e7c66 100644 --- a/components/tag/style/index.tsx +++ b/components/tag/style/index.tsx @@ -1,2 +1,198 @@ -import '../../style/index.less'; -import './index.less'; +// deps-lint-skip-all +import { CSSInterpolation, CSSObject } from '@ant-design/cssinjs'; +import capitalize from '../../_util/capitalize'; +import { + PresetColorType, + DerivativeToken, + useStyleRegister, + useToken, + resetComponent, + UseComponentStyleResult, + PresetColorKeys, +} from '../../_util/theme'; + +interface TagToken extends DerivativeToken { + tagFontSize: number; + tagLineHeight: React.CSSProperties['lineHeight']; + tagDefaultBg: string; + tagDefaultColor: string; +} + +// ============================== Styles ============================== + +type CssVariableType = 'success' | 'info' | 'error' | 'warning'; + +const genTagStatusStyle = ( + prefixCls: string, + token: TagToken, + status: 'success' | 'processing' | 'error' | 'warning', + cssVariableType: CssVariableType, +): CSSInterpolation => { + const capitalizedCssVariableType = capitalize(cssVariableType); + return { + [`.${prefixCls}-${status}`]: { + color: token[`${cssVariableType}Color`], + background: token[`tmp${capitalizedCssVariableType}ColorDeprecatedBg`], + borderColor: token[`tmp${capitalizedCssVariableType}ColorDeprecatedBorder`], + }, + }; +}; + +// FIXME: special preset colors +const genTagColorStyle = (prefixCls: string, token: TagToken): CSSInterpolation => + PresetColorKeys.reduce((prev: CSSObject, colorKey: keyof PresetColorType) => { + const lightColor = token[`${colorKey}-1`]; + const lightBorderColor = token[`${colorKey}-3`]; + const darkColor = token[`${colorKey}-6`]; + const textColor = token[`${colorKey}-7`]; + return { + ...prev, + [`.${prefixCls}-${colorKey}`]: { + color: textColor, + background: lightColor, + borderColor: lightBorderColor, + }, + [`.${prefixCls}-${colorKey}-inverse`]: { + color: token.textColorInverse, + background: darkColor, + borderColor: darkColor, + }, + }; + }, {} as CSSObject); + +const genBaseStyle = ( + prefixCls: string, + iconPrefixCls: string, + token: TagToken, +): CSSInterpolation => ({ + // Result + [`.${prefixCls}`]: { + ...resetComponent(token), + display: 'inline-block', + height: 'auto', + marginInlineStart: token.marginXS, + // FIXME: hard code + padding: '0 7px', + fontSize: token.tagFontSize, + lineHeight: token.tagLineHeight, + whiteSpace: 'nowrap', + background: token.tagDefaultBg, + border: `${token.borderWidth}px ${token.borderStyle} ${token.borderColor}`, + borderRadius: token.borderRadius, + // FIXME: hard code + opacity: 1, + transition: `all ${token.duration}`, + + // RTL + '&&-rtl': { + direction: 'rtl', + textAlign: 'right', + }, + + '&, a, a:hover': { + color: token.tagDefaultColor, + }, + + [`.${prefixCls}-close-icon`]: { + // FIXME: hard code + marginInlineStart: 3, + color: token.textColorSecondary, + // FIXME: hard code + fontSize: 10, + cursor: 'pointer', + transition: `all ${token.duration}`, + + '&:hover': { + color: token.headingColor, + }, + }, + + [`&&-has-color`]: { + borderColor: 'transparent', + + [`&, a, a:hover, .${iconPrefixCls}-close, .${iconPrefixCls}-close:hover`]: { + color: token.textColorInverse, + }, + }, + + [`.${prefixCls}-checkable`]: { + backgroundColor: 'transparent', + borderColor: 'transparent', + cursor: 'pointer', + + '&:not(&-checked):hover': { + color: token.primaryColor, + }, + + '&:active, &-checked': { + color: token.textColorInverse, + }, + + '&-checked': { + backgroundColor: token.tmpPrimaryColor6, + }, + + '&:active': { + backgroundColor: token.tmpPrimaryColor7, + }, + }, + + [`.${prefixCls}-hidden`]: { + display: 'none', + }, + + // To ensure that a space will be placed between character and `Icon`. + [`> .${iconPrefixCls} + span, > span + .${iconPrefixCls}`]: { + // FIXME: hard code + marginInlineStart: 7, + }, + }, +}); + +export const genTagStyle = ( + prefixCls: string, + iconPrefixCls: string, + token: DerivativeToken, +): CSSInterpolation => { + const tagFontSize = token.fontSizeSM; + // FIXME: hard code + const tagLineHeight = '18px'; + const tagDefaultBg = token.backgroundLight; + const tagDefaultColor = token.textColor; + + const tagToken = { + ...token, + tagFontSize, + tagLineHeight, + tagDefaultBg, + tagDefaultColor, + }; + + return [ + genBaseStyle(prefixCls, iconPrefixCls, tagToken), + genTagColorStyle(prefixCls, tagToken), + genTagStatusStyle(prefixCls, tagToken, 'success', 'success'), + genTagStatusStyle(prefixCls, tagToken, 'processing', 'info'), + genTagStatusStyle(prefixCls, tagToken, 'error', 'error'), + genTagStatusStyle(prefixCls, tagToken, 'warning', 'warning'), + ]; +}; + +// ============================== Export ============================== +export function getStyle(prefixCls: string, iconPrefixCls: string, token: DerivativeToken) { + return [genTagStyle(prefixCls, iconPrefixCls, token)]; +} + +export default function useStyle( + prefixCls: string, + iconPrefixCls: string, +): UseComponentStyleResult { + const [theme, token, hashId] = useToken(); + + return [ + useStyleRegister({ theme, token, hashId, path: [prefixCls] }, () => + getStyle(prefixCls, iconPrefixCls, token), + ), + hashId, + ]; +}