Browse Source

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
pull/34511/head
vagusX 3 years ago
committed by GitHub
parent
commit
11ffb1ba6f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      components/_util/__tests__/capitalize.test.ts
  2. 8
      components/_util/capitalize.ts
  3. 22
      components/_util/theme/default.tsx
  4. 96
      components/_util/theme/index.tsx
  5. 9
      components/tag/CheckableTag.tsx
  6. 10
      components/tag/index.tsx
  7. 258
      components/tag/style/index.less
  8. 200
      components/tag/style/index.tsx

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

8
components/_util/capitalize.ts

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

22
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;

96
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<keyof PresetColorType> = [
'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<DesignToken, 'duration'> {
export interface DerivativeToken extends ColorPalettes, Omit<DesignToken, 'duration'> {
primaryHoverColor: string;
primaryActiveColor: string;
primaryOutlineColor: string;
@ -131,21 +169,53 @@ export interface DerivativeToken extends Omit<DesignToken, 'duration'> {
// 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],
};
}

9
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<CheckableTagProps> = ({
onClick,
...restProps
}) => {
const { getPrefixCls } = React.useContext(ConfigContext);
const { getPrefixCls, iconPrefixCls } = React.useContext(ConfigContext);
const handleClick = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
onChange?.(!checked);
@ -32,6 +33,9 @@ const CheckableTag: React.FC<CheckableTagProps> = ({
};
const prefixCls = getPrefixCls('tag', customizePrefixCls);
// Style
const [wrapSSR, hashId] = useStyle(prefixCls, iconPrefixCls);
const cls = classNames(
prefixCls,
{
@ -39,9 +43,10 @@ const CheckableTag: React.FC<CheckableTagProps> = ({
[`${prefixCls}-checkable-checked`]: checked,
},
className,
hashId,
);
return <span {...restProps} className={cls} onClick={handleClick} />;
return wrapSSR(<span {...restProps} className={cls} onClick={handleClick} />);
};
export default CheckableTag;

10
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<HTMLSpanElement> {
@ -51,7 +53,7 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
},
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<HTMLSpanElement, TagProps> = (
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<HTMLSpanElement, TagProps> = (
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
hashId,
);
const handleCloseClick = (e: React.MouseEvent<HTMLElement>) => {
@ -130,7 +136,7 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
</span>
);
return isNeedWave ? <Wave>{tagNode}</Wave> : tagNode;
return wrapSSR(isNeedWave ? <Wave>{tagNode}</Wave> : tagNode);
};
const Tag = React.forwardRef<unknown, TagProps>(InternalTag) as TagType;

258
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';

200
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>(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,
];
}

Loading…
Cancel
Save