refactor: cssinjs for tag (#34422)

* feat: basic implements

* feat: add rtl

* feat: reorganize types

* test: add test cases for capitalize

* test: improve coverage

* chore: improve types

* chore: update

* chore: update
This commit is contained in:
vagusX 2022-03-15 15:33:50 +08:00 committed by GitHub
parent 8ac77e00e3
commit 11ffb1ba6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 459 additions and 121 deletions

View File

@ -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);
});
});

View File

@ -0,0 +1,8 @@
export default function capitalize<T extends string>(str: T): Capitalize<T> {
if (typeof str !== 'string') {
return str;
}
const ret = str.charAt(0).toUpperCase() + str.slice(1);
return ret as Capitalize<T>;
}

View File

@ -1,5 +1,22 @@
import { TinyColor } from '@ctrl/tinycolor'; 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 = { const defaultDesignToken: DesignToken = {
primaryColor: '#1890ff', primaryColor: '#1890ff',
@ -59,6 +76,9 @@ const defaultDesignToken: DesignToken = {
duration: 0.3, duration: 0.3,
zIndexDropdown: 1050, zIndexDropdown: 1050,
// preset color palettes
...presetColors,
}; };
export default defaultDesignToken; export default defaultDesignToken;

View File

@ -38,7 +38,39 @@ export {
slideRightOut, 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<keyof PresetColorType> = [
'blue',
'purple',
'cyan',
'green',
'magenta',
'pink',
'red',
'orange',
'yellow',
'volcano',
'geekblue',
'lime',
'gold',
];
export interface DesignToken extends PresetColorType {
primaryColor: string; primaryColor: string;
successColor: string; successColor: string;
warningColor: string; warningColor: string;
@ -93,8 +125,14 @@ export interface DesignToken {
boxShadow?: string; 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. */ /** This is temporary token definition since final token definition is not ready yet. */
export interface DerivativeToken extends Omit<DesignToken, 'duration'> { export interface DerivativeToken extends ColorPalettes, Omit<DesignToken, 'duration'> {
primaryHoverColor: string; primaryHoverColor: string;
primaryActiveColor: string; primaryActiveColor: string;
primaryOutlineColor: string; primaryOutlineColor: string;
@ -131,21 +169,53 @@ export interface DerivativeToken extends Omit<DesignToken, 'duration'> {
// TMP // TMP
tmpPrimaryColorWeak: string; tmpPrimaryColorWeak: string;
tmpPrimaryHoverColorWeak: 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 }; export { useStyleRegister };
// =============================== Derivative =============================== // =============================== Derivative ===============================
function derivative(designToken: DesignToken): DerivativeToken { function derivative(designToken: DesignToken): DerivativeToken {
const { primaryColor, errorColor, warningColor } = designToken; const { primaryColor, errorColor, warningColor, infoColor, successColor } = designToken;
const primaryColors = generate(primaryColor); const primaryColors = generate(primaryColor);
const errorColors = generate(errorColor); const errorColors = generate(errorColor);
const warningColors = generate(warningColor); const warningColors = generate(warningColor);
const infoColors = generate(infoColor);
const successColors = generate(successColor);
const paddingSM = (designToken.padding / 4) * 3; const paddingSM = (designToken.padding / 4) * 3;
const paddingXS = designToken.padding * 0.5; 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 { return {
// FIXME: Need design token // FIXME: Need design token
boxShadow: ` boxShadow: `
@ -155,8 +225,6 @@ function derivative(designToken: DesignToken): DerivativeToken {
...designToken, ...designToken,
tmpPrimaryColorWeak: primaryColors[2],
tmpPrimaryHoverColorWeak: primaryColors[0],
primaryHoverColor: primaryColors[4], primaryHoverColor: primaryColors[4],
primaryActiveColor: primaryColors[6], primaryActiveColor: primaryColors[6],
primaryOutlineColor: new TinyColor(primaryColor).setAlpha(0.2).toRgbString(), primaryOutlineColor: new TinyColor(primaryColor).setAlpha(0.2).toRgbString(),
@ -191,6 +259,24 @@ function derivative(designToken: DesignToken): DerivativeToken {
duration: `${designToken.duration}s`, duration: `${designToken.duration}s`,
durationMid: `${(designToken.duration / 3) * 2}s`, durationMid: `${(designToken.duration / 3) * 2}s`,
durationFast: `${designToken.duration / 3}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],
}; };
} }

View File

@ -1,6 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import useStyle from './style';
export interface CheckableTagProps { export interface CheckableTagProps {
prefixCls?: string; prefixCls?: string;
@ -24,7 +25,7 @@ const CheckableTag: React.FC<CheckableTagProps> = ({
onClick, onClick,
...restProps ...restProps
}) => { }) => {
const { getPrefixCls } = React.useContext(ConfigContext); const { getPrefixCls, iconPrefixCls } = React.useContext(ConfigContext);
const handleClick = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => { const handleClick = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
onChange?.(!checked); onChange?.(!checked);
@ -32,6 +33,9 @@ const CheckableTag: React.FC<CheckableTagProps> = ({
}; };
const prefixCls = getPrefixCls('tag', customizePrefixCls); const prefixCls = getPrefixCls('tag', customizePrefixCls);
// Style
const [wrapSSR, hashId] = useStyle(prefixCls, iconPrefixCls);
const cls = classNames( const cls = classNames(
prefixCls, prefixCls,
{ {
@ -39,9 +43,10 @@ const CheckableTag: React.FC<CheckableTagProps> = ({
[`${prefixCls}-checkable-checked`]: checked, [`${prefixCls}-checkable-checked`]: checked,
}, },
className, className,
hashId,
); );
return <span {...restProps} className={cls} onClick={handleClick} />; return wrapSSR(<span {...restProps} className={cls} onClick={handleClick} />);
}; };
export default CheckableTag; export default CheckableTag;

View File

@ -14,6 +14,8 @@ import {
import Wave from '../_util/wave'; import Wave from '../_util/wave';
import { LiteralUnion } from '../_util/type'; import { LiteralUnion } from '../_util/type';
import useStyle from './style';
export { CheckableTagProps } from './CheckableTag'; export { CheckableTagProps } from './CheckableTag';
export interface TagProps extends React.HTMLAttributes<HTMLSpanElement> { export interface TagProps extends React.HTMLAttributes<HTMLSpanElement> {
@ -51,7 +53,7 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
}, },
ref, ref,
) => { ) => {
const { getPrefixCls, direction } = React.useContext(ConfigContext); const { getPrefixCls, direction, iconPrefixCls } = React.useContext(ConfigContext);
const [visible, setVisible] = React.useState(true); const [visible, setVisible] = React.useState(true);
React.useEffect(() => { React.useEffect(() => {
@ -74,6 +76,9 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
const presetColor = isPresetColor(); const presetColor = isPresetColor();
const prefixCls = getPrefixCls('tag', customizePrefixCls); const prefixCls = getPrefixCls('tag', customizePrefixCls);
// Style
const [wrapSSR, hashId] = useStyle(prefixCls, iconPrefixCls);
const tagClassName = classNames( const tagClassName = classNames(
prefixCls, prefixCls,
{ {
@ -83,6 +88,7 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
[`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-rtl`]: direction === 'rtl',
}, },
className, className,
hashId,
); );
const handleCloseClick = (e: React.MouseEvent<HTMLElement>) => { const handleCloseClick = (e: React.MouseEvent<HTMLElement>) => {
@ -130,7 +136,7 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
</span> </span>
); );
return isNeedWave ? <Wave>{tagNode}</Wave> : tagNode; return wrapSSR(isNeedWave ? <Wave>{tagNode}</Wave> : tagNode);
}; };
const Tag = React.forwardRef<unknown, TagProps>(InternalTag) as TagType; const Tag = React.forwardRef<unknown, TagProps>(InternalTag) as TagType;

View File

@ -1,129 +1,129 @@
@import '../../style/themes/index'; // @import '../../style/themes/index';
@import '../../style/mixins/index'; // @import '../../style/mixins/index';
@tag-prefix-cls: ~'@{ant-prefix}-tag'; // @tag-prefix-cls: ~'@{ant-prefix}-tag';
.@{tag-prefix-cls} { // .@{tag-prefix-cls} {
.reset-component(); // .reset-component();
display: inline-block; // display: inline-block;
height: auto; // height: auto;
margin-right: 8px; // margin-right: 8px;
padding: 0 7px; // padding: 0 7px;
font-size: @tag-font-size; // font-size: @tag-font-size;
line-height: @tag-line-height; // line-height: @tag-line-height;
white-space: nowrap; // white-space: nowrap;
background: @tag-default-bg; // background: @tag-default-bg;
border: @border-width-base @border-style-base @border-color-base; // border: @border-width-base @border-style-base @border-color-base;
border-radius: @border-radius-base; // border-radius: @border-radius-base;
opacity: 1; // opacity: 1;
transition: all 0.3s; // transition: all 0.3s;
&, // &,
a, // a,
a:hover { // a:hover {
color: @tag-default-color; // color: @tag-default-color;
} // }
> a:first-child:last-child { // > a:first-child:last-child {
display: inline-block; // display: inline-block;
margin: 0 -8px; // margin: 0 -8px;
padding: 0 8px; // padding: 0 8px;
} // }
&-close-icon { // &-close-icon {
margin-left: 3px; // margin-left: 3px;
color: @text-color-secondary; // color: @text-color-secondary;
font-size: 10px; // font-size: 10px;
cursor: pointer; // cursor: pointer;
transition: all 0.3s; // transition: all 0.3s;
&:hover { // &:hover {
color: @heading-color; // color: @heading-color;
} // }
} // }
&-has-color { // &-has-color {
border-color: transparent; // border-color: transparent;
&, // &,
a, // a,
a:hover, // a:hover,
.@{iconfont-css-prefix}-close, // .@{iconfont-css-prefix}-close,
.@{iconfont-css-prefix}-close:hover { // .@{iconfont-css-prefix}-close:hover {
color: @text-color-inverse; // color: @text-color-inverse;
} // }
} // }
&-checkable { // &-checkable {
background-color: transparent; // background-color: transparent;
border-color: transparent; // border-color: transparent;
cursor: pointer; // cursor: pointer;
&:not(&-checked):hover { // &:not(&-checked):hover {
color: @primary-color; // color: @primary-color;
} // }
&:active, // &:active,
&-checked { // &-checked {
color: @text-color-inverse; // color: @text-color-inverse;
} // }
&-checked { // &-checked {
background-color: @primary-6; // background-color: @primary-6;
} // }
&:active { // &:active {
background-color: @primary-7; // background-color: @primary-7;
} // }
} // }
&-hidden { // &-hidden {
display: none; // display: none;
} // }
// mixin to iterate over colors and create CSS class for each one // // 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: length(@preset-colors)) when (@i > 0) {
.make-color-classes(@i - 1); // .make-color-classes(@i - 1);
@color: extract(@preset-colors, @i); // @color: extract(@preset-colors, @i);
@lightColor: '@{color}-1'; // @lightColor: '@{color}-1';
@lightBorderColor: '@{color}-3'; // @lightBorderColor: '@{color}-3';
@darkColor: '@{color}-6'; // @darkColor: '@{color}-6';
@textColor: '@{color}-7'; // @textColor: '@{color}-7';
&-@{color} { // &-@{color} {
color: @@textColor; // color: @@textColor;
background: @@lightColor; // background: @@lightColor;
border-color: @@lightBorderColor; // border-color: @@lightBorderColor;
} // }
&-@{color}-inverse { // &-@{color}-inverse {
color: @text-color-inverse; // color: @text-color-inverse;
background: @@darkColor; // background: @@darkColor;
border-color: @@darkColor; // border-color: @@darkColor;
} // }
} // }
.make-status-color-classes(@status, @cssVariableType) { // .make-status-color-classes(@status, @cssVariableType) {
@bgColor: '@{cssVariableType}-color-deprecated-bg'; // @bgColor: '@{cssVariableType}-color-deprecated-bg';
@borderColor: '@{cssVariableType}-color-deprecated-border'; // @borderColor: '@{cssVariableType}-color-deprecated-border';
@textColor: '@{cssVariableType}-color'; // @textColor: '@{cssVariableType}-color';
&-@{status} { // &-@{status} {
color: @@textColor; // color: @@textColor;
background: @@bgColor; // background: @@bgColor;
border-color: @@borderColor; // border-color: @@borderColor;
} // }
} // }
.make-color-classes(); // .make-color-classes();
.make-status-color-classes(success, success); // .make-status-color-classes(success, success);
.make-status-color-classes(processing, info); // .make-status-color-classes(processing, info);
.make-status-color-classes(error, error); // .make-status-color-classes(error, error);
.make-status-color-classes(warning, warning); // .make-status-color-classes(warning, warning);
// To ensure that a space will be placed between character and `Icon`. // // To ensure that a space will be placed between character and `Icon`.
> .@{iconfont-css-prefix} + span, // > .@{iconfont-css-prefix} + span,
> span + .@{iconfont-css-prefix} { // > span + .@{iconfont-css-prefix} {
margin-left: 7px; // margin-left: 7px;
} // }
} // }
@import './rtl'; // @import './rtl';

View File

@ -1,2 +1,198 @@
import '../../style/index.less'; // deps-lint-skip-all
import './index.less'; 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>(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,
];
}