mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-24 02:59:58 +08:00
refactor: Button with cssinjs (#33890)
* init * chore: cssinjs base button * chore: more style * chore: mix style * chore: size * chore: more style * misc * chore: re-structrue * chore: icon onlt * chore: back of disabled * chore: loading status * chore: loading motion * chore: add active style * docs: site prepare dynamic theme * chore: bump antd cssinjs * chore: color type check * chore: bump cssinjs version * chore: clean up useless ts def * chore: ignore button style * chore: rename ci * chore: update cssinjs ver * chore: ssr default wrapper * chore: bump cssinjs version * chore: ssr support * chore: fix script * test: fix node snapshot * chore: move cssinjs pkg size from css to js * test: coverage
This commit is contained in:
parent
32c68591ce
commit
912ffb15d0
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -492,7 +492,7 @@ jobs:
|
||||
run: npx lessc --js ./dist/antd.variable.less
|
||||
|
||||
- name: lessc component
|
||||
run: npx lessc --js ./es/button/style/index.less
|
||||
run: npx lessc --js ./es/input/style/index.less
|
||||
|
||||
- name: lessc mixins
|
||||
run: npx lessc --js ./es/style/mixins/index.less
|
||||
|
33
components/_util/theme/default.tsx
Normal file
33
components/_util/theme/default.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { TinyColor } from '@ctrl/tinycolor';
|
||||
import type { DesignToken } from '.';
|
||||
|
||||
const defaultDesignToken: DesignToken = {
|
||||
primaryColor: '#1890ff',
|
||||
errorColor: '#ff4d4f',
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/20210
|
||||
lineHeight: 1.5715,
|
||||
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
borderRadius: 2,
|
||||
borderColor: new TinyColor({ h: 0, s: 0, v: 85 }).toHexString(),
|
||||
|
||||
easeInOut: `cubic-bezier(0.645, 0.045, 0.355, 1)`,
|
||||
|
||||
fontSize: 14,
|
||||
textColor: new TinyColor('#000').setAlpha(0.85).toRgbString(),
|
||||
textColorDisabled: new TinyColor('#000').setAlpha(0.25).toRgbString(),
|
||||
|
||||
height: 32,
|
||||
|
||||
padding: 16,
|
||||
margin: 16,
|
||||
|
||||
componentBackground: '#fff',
|
||||
componentBackgroundDisabled: new TinyColor({ h: 0, s: 0, v: 96 }).toHexString(),
|
||||
|
||||
duration: '0.3s',
|
||||
};
|
||||
|
||||
export default defaultDesignToken;
|
112
components/_util/theme/index.tsx
Normal file
112
components/_util/theme/index.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import React from 'react';
|
||||
import { generate } from '@ant-design/colors';
|
||||
import { CSSObject, Theme, useCacheToken, useStyleRegister } from '@ant-design/cssinjs';
|
||||
import defaultDesignToken from './default';
|
||||
import version from '../../version';
|
||||
import { ConfigContext } from '../../config-provider';
|
||||
|
||||
export interface DesignToken {
|
||||
primaryColor: string;
|
||||
errorColor: string;
|
||||
lineHeight: number;
|
||||
borderWidth: number;
|
||||
borderStyle: string;
|
||||
borderRadius: number;
|
||||
borderColor: string;
|
||||
easeInOut: string;
|
||||
|
||||
fontSize: number;
|
||||
textColor: string;
|
||||
textColorDisabled: string;
|
||||
|
||||
height: number;
|
||||
|
||||
padding: number;
|
||||
margin: number;
|
||||
|
||||
componentBackground: string;
|
||||
componentBackgroundDisabled: string;
|
||||
|
||||
duration: string;
|
||||
}
|
||||
|
||||
/** This is temporary token definition since final token definition is not ready yet. */
|
||||
export interface DerivativeToken extends DesignToken {
|
||||
primaryHoverColor: string;
|
||||
primaryActiveColor: string;
|
||||
errorHoverColor: string;
|
||||
errorActiveColor: string;
|
||||
|
||||
linkColor: string;
|
||||
fontSizeSM: number;
|
||||
fontSizeLG: number;
|
||||
heightSM: number;
|
||||
heightLG: number;
|
||||
paddingXS: number;
|
||||
marginXS: number;
|
||||
}
|
||||
|
||||
export { useStyleRegister };
|
||||
|
||||
// =============================== Derivative ===============================
|
||||
function derivative(designToken: DesignToken): DerivativeToken {
|
||||
const primaryColors = generate(designToken.primaryColor);
|
||||
const errorColors = generate(designToken.errorColor);
|
||||
|
||||
return {
|
||||
...designToken,
|
||||
|
||||
primaryHoverColor: primaryColors[4],
|
||||
primaryActiveColor: primaryColors[6],
|
||||
|
||||
errorHoverColor: errorColors[4],
|
||||
errorActiveColor: errorColors[6],
|
||||
|
||||
linkColor: designToken.primaryColor,
|
||||
fontSizeSM: designToken.fontSize - 2,
|
||||
fontSizeLG: designToken.fontSize + 2,
|
||||
heightSM: designToken.height * 0.75,
|
||||
heightLG: designToken.height * 1.25,
|
||||
paddingXS: designToken.padding * 0.5,
|
||||
marginXS: designToken.margin * 0.5,
|
||||
};
|
||||
}
|
||||
|
||||
// ================================ Context =================================
|
||||
export const ThemeContext = React.createContext(
|
||||
new Theme<DesignToken, DerivativeToken>(derivative),
|
||||
);
|
||||
|
||||
export const DesignTokenContext = React.createContext<{
|
||||
token: Partial<DesignToken>;
|
||||
hashed?: string | boolean;
|
||||
}>({
|
||||
token: defaultDesignToken,
|
||||
});
|
||||
|
||||
// ================================== Hook ==================================
|
||||
export function useToken() {
|
||||
const { iconPrefixCls } = React.useContext(ConfigContext);
|
||||
const { token: rootDesignToken, hashed } = React.useContext(DesignTokenContext);
|
||||
const theme = React.useContext(ThemeContext);
|
||||
|
||||
const salt = `${version}-${hashed || ''}`;
|
||||
|
||||
const [token, hashId] = useCacheToken(theme, [defaultDesignToken, rootDesignToken], {
|
||||
salt,
|
||||
});
|
||||
return [theme, token, iconPrefixCls, hashed ? hashId : ''];
|
||||
}
|
||||
|
||||
// ================================== Util ==================================
|
||||
export function withPrefix(
|
||||
style: CSSObject,
|
||||
prefixCls: string,
|
||||
additionalClsList: string[] = [],
|
||||
): CSSObject {
|
||||
const fullClsList = [prefixCls, ...additionalClsList].filter(cls => cls).map(cls => `.${cls}`);
|
||||
|
||||
return {
|
||||
[fullClsList.join('')]: style,
|
||||
};
|
||||
}
|
2
components/button/_style/index.tsx
Normal file
2
components/button/_style/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
import '../../style/index.less';
|
||||
import './index.less';
|
@ -12,6 +12,9 @@ import SizeContext, { SizeType } from '../config-provider/SizeContext';
|
||||
import LoadingIcon from './LoadingIcon';
|
||||
import { cloneElement } from '../_util/reactNode';
|
||||
|
||||
// CSSINJS
|
||||
import useStyle from './style';
|
||||
|
||||
const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/;
|
||||
const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar);
|
||||
function isString(str: any) {
|
||||
@ -151,10 +154,15 @@ const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (pr
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const { getPrefixCls, autoInsertSpaceInButton, direction } = React.useContext(ConfigContext);
|
||||
const prefixCls = getPrefixCls('btn', customizePrefixCls);
|
||||
|
||||
// Style
|
||||
const wrapSSR = useStyle(prefixCls);
|
||||
|
||||
const size = React.useContext(SizeContext);
|
||||
const [innerLoading, setLoading] = React.useState<Loading>(!!loading);
|
||||
const [hasTwoCNChar, setHasTwoCNChar] = React.useState(false);
|
||||
const { getPrefixCls, autoInsertSpaceInButton, direction } = React.useContext(ConfigContext);
|
||||
const buttonRef = (ref as any) || React.createRef<HTMLElement>();
|
||||
|
||||
const isNeedInserted = () =>
|
||||
@ -225,7 +233,6 @@ const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (pr
|
||||
"`link` or `text` button can't be a `ghost` button.",
|
||||
);
|
||||
|
||||
const prefixCls = getPrefixCls('btn', customizePrefixCls);
|
||||
const autoInsertSpace = autoInsertSpaceInButton !== false;
|
||||
|
||||
const sizeClassNameMap = { large: 'lg', small: 'sm', middle: undefined };
|
||||
@ -265,15 +272,15 @@ const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (pr
|
||||
|
||||
const linkButtonRestProps = omit(rest as AnchorButtonProps & { navigate: any }, ['navigate']);
|
||||
if (linkButtonRestProps.href !== undefined) {
|
||||
return (
|
||||
return wrapSSR(
|
||||
<a {...linkButtonRestProps} className={classes} onClick={handleClick} ref={buttonRef}>
|
||||
{iconNode}
|
||||
{kids}
|
||||
</a>
|
||||
</a>,
|
||||
);
|
||||
}
|
||||
|
||||
const buttonNode = (
|
||||
let buttonNode = (
|
||||
<button
|
||||
{...(rest as NativeButtonProps)}
|
||||
type={htmlType}
|
||||
@ -286,11 +293,11 @@ const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (pr
|
||||
</button>
|
||||
);
|
||||
|
||||
if (isUnborderedButtonType(type)) {
|
||||
return buttonNode;
|
||||
if (!isUnborderedButtonType(type)) {
|
||||
buttonNode = <Wave disabled={!!innerLoading}>{buttonNode}</Wave>;
|
||||
}
|
||||
|
||||
return <Wave disabled={!!innerLoading}>{buttonNode}</Wave>;
|
||||
return wrapSSR(buttonNode);
|
||||
};
|
||||
|
||||
const Button = React.forwardRef<unknown, ButtonProps>(InternalButton) as CompoundedComponent;
|
||||
|
@ -1,2 +1,391 @@
|
||||
import '../../style/index.less';
|
||||
import './index.less';
|
||||
// deps-lint-skip-all
|
||||
import { CSSInterpolation, CSSObject } from '@ant-design/cssinjs';
|
||||
import { TinyColor } from '@ctrl/tinycolor';
|
||||
import { DerivativeToken, useStyleRegister, useToken, withPrefix } from '../../_util/theme';
|
||||
|
||||
// ============================== Shared ==============================
|
||||
const genSharedButtonStyle = (
|
||||
prefixCls: string,
|
||||
iconPrefixCls: string,
|
||||
token: DerivativeToken,
|
||||
): CSSObject => ({
|
||||
outline: 'none',
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
fontWeight: 400,
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: 'center',
|
||||
backgroundImage: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
border: `${token.borderWidth}px ${token.borderStyle} transparent`,
|
||||
cursor: 'pointer',
|
||||
transition: `all ${token.duration} ${token.easeInOut}`,
|
||||
userSelect: 'none',
|
||||
touchAction: 'manipulation',
|
||||
lineHeight: token.lineHeight,
|
||||
color: token.textColor,
|
||||
|
||||
'> span': {
|
||||
display: 'inline-block',
|
||||
},
|
||||
|
||||
// Leave a space between icon and text.
|
||||
[`> .${iconPrefixCls} + span, > span + .${iconPrefixCls}`]: {
|
||||
marginInlineStart: token.marginXS,
|
||||
},
|
||||
|
||||
[`&.${prefixCls}-block`]: {
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
const genHoverActiveButtonStyle = (hoverStyle: CSSObject, activeStyle: CSSObject): CSSObject => ({
|
||||
'&:not(:disabled)': {
|
||||
'&:hover, &:focus': hoverStyle,
|
||||
'&:active': activeStyle,
|
||||
},
|
||||
});
|
||||
|
||||
// ============================== Shape ===============================
|
||||
const genCircleButtonStyle = (token: DerivativeToken): CSSObject => ({
|
||||
minWidth: token.height,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
borderRadius: '50%',
|
||||
});
|
||||
|
||||
const genRoundButtonStyle = (token: DerivativeToken): CSSObject => ({
|
||||
borderRadius: token.height,
|
||||
paddingLeft: token.height / 2,
|
||||
paddingRight: token.height / 2,
|
||||
width: 'auto',
|
||||
});
|
||||
|
||||
// =============================== Type ===============================
|
||||
const genGhostButtonStyle = (
|
||||
prefixCls: string,
|
||||
textColor: string | false,
|
||||
borderColor: string | false,
|
||||
textColorDisabled: string | false,
|
||||
borderColorDisabled: string | false,
|
||||
): CSSObject => ({
|
||||
[`&.${prefixCls}-background-ghost`]: {
|
||||
color: textColor || undefined,
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: borderColor || undefined,
|
||||
|
||||
'&:disabled': {
|
||||
cursor: 'not-allowed',
|
||||
color: textColorDisabled || undefined,
|
||||
borderColor: borderColorDisabled || undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const genSolidDisabledButtonStyle = (token: DerivativeToken): CSSObject => ({
|
||||
'&:disabled': {
|
||||
cursor: 'not-allowed',
|
||||
borderColor: token.borderColor,
|
||||
color: token.textColorDisabled,
|
||||
backgroundColor: token.componentBackgroundDisabled,
|
||||
boxShadow: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
const genSolidButtonStyle = (token: DerivativeToken): CSSObject => ({
|
||||
borderRadius: token.borderRadius,
|
||||
|
||||
...genSolidDisabledButtonStyle(token),
|
||||
});
|
||||
|
||||
const genPureDisabledButtonStyle = (token: DerivativeToken): CSSObject => ({
|
||||
'&:disabled': {
|
||||
cursor: 'not-allowed',
|
||||
color: token.textColorDisabled,
|
||||
},
|
||||
});
|
||||
|
||||
// Type: Default
|
||||
const genDefaultButtonStyle = (prefixCls: string, token: DerivativeToken): CSSObject => ({
|
||||
...genSolidButtonStyle(token),
|
||||
|
||||
backgroundColor: token.componentBackground,
|
||||
borderColor: token.borderColor,
|
||||
|
||||
boxShadow: '0 2px 0 rgba(0, 0, 0, 0.015)',
|
||||
|
||||
...genHoverActiveButtonStyle(
|
||||
{
|
||||
color: token.primaryHoverColor,
|
||||
borderColor: token.primaryHoverColor,
|
||||
},
|
||||
{
|
||||
color: token.primaryActiveColor,
|
||||
borderColor: token.primaryActiveColor,
|
||||
},
|
||||
),
|
||||
|
||||
...genGhostButtonStyle(
|
||||
prefixCls,
|
||||
token.componentBackground,
|
||||
token.componentBackground,
|
||||
token.textColorDisabled,
|
||||
token.borderColor,
|
||||
),
|
||||
|
||||
[`&.${prefixCls}-dangerous`]: {
|
||||
color: token.errorColor,
|
||||
borderColor: token.errorColor,
|
||||
|
||||
...genHoverActiveButtonStyle(
|
||||
{
|
||||
color: token.errorHoverColor,
|
||||
borderColor: token.errorHoverColor,
|
||||
},
|
||||
{
|
||||
color: token.errorActiveColor,
|
||||
borderColor: token.errorActiveColor,
|
||||
},
|
||||
),
|
||||
|
||||
...genGhostButtonStyle(
|
||||
prefixCls,
|
||||
token.errorColor,
|
||||
token.errorColor,
|
||||
token.textColorDisabled,
|
||||
token.borderColor,
|
||||
),
|
||||
...genSolidDisabledButtonStyle(token),
|
||||
},
|
||||
});
|
||||
|
||||
// Type: Primary
|
||||
const genPrimaryButtonStyle = (prefixCls: string, token: DerivativeToken): CSSObject => ({
|
||||
...genSolidButtonStyle(token),
|
||||
|
||||
color: '#FFF',
|
||||
backgroundColor: token.primaryColor,
|
||||
|
||||
boxShadow: '0 2px 0 rgba(0, 0, 0, 0.045)',
|
||||
|
||||
...genHoverActiveButtonStyle(
|
||||
{
|
||||
backgroundColor: token.primaryHoverColor,
|
||||
},
|
||||
{
|
||||
backgroundColor: token.primaryActiveColor,
|
||||
},
|
||||
),
|
||||
|
||||
...genGhostButtonStyle(
|
||||
prefixCls,
|
||||
token.primaryColor,
|
||||
token.primaryColor,
|
||||
token.textColorDisabled,
|
||||
token.borderColor,
|
||||
),
|
||||
|
||||
[`&.${prefixCls}-dangerous`]: {
|
||||
backgroundColor: token.errorColor,
|
||||
|
||||
...genHoverActiveButtonStyle(
|
||||
{
|
||||
backgroundColor: token.errorHoverColor,
|
||||
},
|
||||
{
|
||||
backgroundColor: token.errorActiveColor,
|
||||
},
|
||||
),
|
||||
|
||||
...genGhostButtonStyle(
|
||||
prefixCls,
|
||||
token.errorColor,
|
||||
token.errorColor,
|
||||
token.textColorDisabled,
|
||||
token.borderColor,
|
||||
),
|
||||
...genSolidDisabledButtonStyle(token),
|
||||
},
|
||||
});
|
||||
|
||||
// Type: Dashed
|
||||
const genDashedButtonStyle = (prefixCls: string, token: DerivativeToken): CSSObject => ({
|
||||
...genDefaultButtonStyle(prefixCls, token),
|
||||
|
||||
borderStyle: 'dashed',
|
||||
});
|
||||
|
||||
// Type: Link
|
||||
const genLinkButtonStyle = (prefixCls: string, token: DerivativeToken): CSSObject => ({
|
||||
color: token.linkColor,
|
||||
|
||||
...genHoverActiveButtonStyle(
|
||||
{
|
||||
color: token.primaryHoverColor,
|
||||
},
|
||||
{
|
||||
color: token.primaryActiveColor,
|
||||
},
|
||||
),
|
||||
|
||||
...genPureDisabledButtonStyle(token),
|
||||
|
||||
[`&.${prefixCls}-dangerous`]: {
|
||||
color: token.errorColor,
|
||||
|
||||
...genHoverActiveButtonStyle(
|
||||
{
|
||||
color: token.errorHoverColor,
|
||||
},
|
||||
{
|
||||
color: token.errorActiveColor,
|
||||
},
|
||||
),
|
||||
|
||||
...genPureDisabledButtonStyle(token),
|
||||
},
|
||||
});
|
||||
|
||||
// Type: Text
|
||||
const genTextButtonStyle = (prefixCls: string, token: DerivativeToken): CSSObject => {
|
||||
const backgroundColor = new TinyColor({ r: 0, g: 0, b: 0, a: 0.018 });
|
||||
|
||||
return {
|
||||
...genHoverActiveButtonStyle(
|
||||
{
|
||||
backgroundColor: backgroundColor.toRgbString(),
|
||||
},
|
||||
{
|
||||
backgroundColor: backgroundColor
|
||||
.clone()
|
||||
.setAlpha(backgroundColor.getAlpha() * 1.5)
|
||||
.toRgbString(),
|
||||
},
|
||||
),
|
||||
|
||||
...genPureDisabledButtonStyle(token),
|
||||
|
||||
[`&.${prefixCls}-dangerous`]: {
|
||||
color: token.errorColor,
|
||||
|
||||
...genPureDisabledButtonStyle(token),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const genTypeButtonStyle = (prefixCls: string, token: DerivativeToken): CSSInterpolation => [
|
||||
withPrefix(genDefaultButtonStyle(prefixCls, token), `${prefixCls}-default`, []),
|
||||
withPrefix(genPrimaryButtonStyle(prefixCls, token), `${prefixCls}-primary`, []),
|
||||
withPrefix(genDashedButtonStyle(prefixCls, token), `${prefixCls}-dashed`, []),
|
||||
withPrefix(genLinkButtonStyle(prefixCls, token), `${prefixCls}-link`, []),
|
||||
withPrefix(genTextButtonStyle(prefixCls, token), `${prefixCls}-text`, []),
|
||||
];
|
||||
|
||||
// =============================== Size ===============================
|
||||
const genSizeButtonStyle = (
|
||||
prefixCls: string,
|
||||
iconPrefixCls: string,
|
||||
sizePrefixCls: string,
|
||||
token: DerivativeToken,
|
||||
): CSSInterpolation => {
|
||||
const paddingVertical = Math.max(
|
||||
0,
|
||||
(token.height - token.fontSize * token.lineHeight) / 2 - token.borderWidth,
|
||||
);
|
||||
const paddingHorizontal = token.padding - token.borderWidth;
|
||||
|
||||
const iconOnlyCls = `.${prefixCls}-icon-only`;
|
||||
|
||||
return [
|
||||
// Size
|
||||
withPrefix(
|
||||
{
|
||||
fontSize: token.fontSize,
|
||||
height: token.height,
|
||||
padding: `${paddingVertical}px ${paddingHorizontal}px`,
|
||||
|
||||
[`&${iconOnlyCls}`]: {
|
||||
width: token.height,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
|
||||
'> span': {
|
||||
transform: 'scale(1.143)', // 14px -> 16px
|
||||
},
|
||||
},
|
||||
|
||||
// Loading
|
||||
[`&.${prefixCls}-loading`]: {
|
||||
opacity: 0.65,
|
||||
cursor: 'default',
|
||||
},
|
||||
|
||||
[`.${prefixCls}-loading-icon`]: {
|
||||
transition: `width ${token.duration} ${token.easeInOut}, opacity ${token.duration} ${token.easeInOut}`,
|
||||
},
|
||||
|
||||
[`&:not(${iconOnlyCls}) .${prefixCls}-loading-icon > .${iconPrefixCls}`]: {
|
||||
marginInlineEnd: token.marginXS,
|
||||
},
|
||||
},
|
||||
prefixCls,
|
||||
[sizePrefixCls],
|
||||
),
|
||||
|
||||
// Shape - patch prefixCls again to override solid border radius style
|
||||
withPrefix(genCircleButtonStyle(token), `${prefixCls}-circle`, [prefixCls, sizePrefixCls]),
|
||||
withPrefix(genRoundButtonStyle(token), `${prefixCls}-round`, [prefixCls, sizePrefixCls]),
|
||||
];
|
||||
};
|
||||
|
||||
const genSizeBaseButtonStyle = (
|
||||
prefixCls: string,
|
||||
iconPrefixCls: string,
|
||||
token: DerivativeToken,
|
||||
): CSSInterpolation => genSizeButtonStyle(prefixCls, iconPrefixCls, '', token);
|
||||
|
||||
const genSizeSmallButtonStyle = (
|
||||
prefixCls: string,
|
||||
iconPrefixCls: string,
|
||||
token: DerivativeToken,
|
||||
): CSSInterpolation => {
|
||||
const largeToken: DerivativeToken = {
|
||||
...token,
|
||||
height: token.heightSM,
|
||||
padding: token.paddingXS,
|
||||
};
|
||||
|
||||
return genSizeButtonStyle(prefixCls, iconPrefixCls, `${prefixCls}-sm`, largeToken);
|
||||
};
|
||||
|
||||
const genSizeLargeButtonStyle = (
|
||||
prefixCls: string,
|
||||
iconPrefixCls: string,
|
||||
token: DerivativeToken,
|
||||
): CSSInterpolation => {
|
||||
const largeToken: DerivativeToken = {
|
||||
...token,
|
||||
height: token.heightLG,
|
||||
fontSize: token.fontSizeLG,
|
||||
};
|
||||
|
||||
return genSizeButtonStyle(prefixCls, iconPrefixCls, `${prefixCls}-lg`, largeToken);
|
||||
};
|
||||
|
||||
// ============================== Export ==============================
|
||||
export default function useStyle(prefixCls: string) {
|
||||
const [theme, token, iconPrefixCls, hashId] = useToken();
|
||||
|
||||
return useStyleRegister({ theme, token, hashId, path: [prefixCls] }, () => [
|
||||
// Shared
|
||||
withPrefix(genSharedButtonStyle(prefixCls, iconPrefixCls, token), prefixCls),
|
||||
|
||||
// Size
|
||||
genSizeSmallButtonStyle(prefixCls, iconPrefixCls, token),
|
||||
genSizeBaseButtonStyle(prefixCls, iconPrefixCls, token),
|
||||
genSizeLargeButtonStyle(prefixCls, iconPrefixCls, token),
|
||||
|
||||
// Group (type, ghost, danger, disabled, loading)
|
||||
genTypeButtonStyle(prefixCls, token),
|
||||
]);
|
||||
}
|
||||
|
49
components/config-provider/__tests__/cssinjs.test.tsx
Normal file
49
components/config-provider/__tests__/cssinjs.test.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import ConfigProvider from '..';
|
||||
import Button from '../../button';
|
||||
|
||||
describe('ConfigProvider.DynamicTheme', () => {
|
||||
beforeEach(() => {
|
||||
Array.from(document.querySelectorAll('style')).forEach(style => {
|
||||
style.parentNode?.removeChild(style);
|
||||
});
|
||||
});
|
||||
|
||||
it('customize primary color', () => {
|
||||
mount(
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
primaryColor: '#f00',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button />
|
||||
</ConfigProvider>,
|
||||
);
|
||||
|
||||
const dynamicStyles = Array.from(document.querySelectorAll('style[data-css-hash]'));
|
||||
|
||||
expect(
|
||||
dynamicStyles.some(style => {
|
||||
const { innerHTML } = style;
|
||||
return (
|
||||
innerHTML.includes('.ant-btn-primary') && innerHTML.includes('background-color:#f00')
|
||||
);
|
||||
}),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('not crash on null token', () => {
|
||||
expect(() => {
|
||||
mount(
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: null as any,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
@ -4,6 +4,8 @@ import { Locale } from '../locale-provider';
|
||||
import { SizeType } from './SizeContext';
|
||||
import { RequiredMark } from '../form/Form';
|
||||
|
||||
export const defaultIconPrefixCls = 'anticon';
|
||||
|
||||
export interface Theme {
|
||||
primaryColor?: string;
|
||||
infoColor?: string;
|
||||
@ -58,6 +60,8 @@ export const ConfigContext = React.createContext<ConfigConsumerProps>({
|
||||
getPrefixCls: defaultGetPrefixCls,
|
||||
|
||||
renderEmpty: defaultRenderEmpty,
|
||||
|
||||
iconPrefixCls: defaultIconPrefixCls,
|
||||
});
|
||||
|
||||
export const ConfigConsumer = ConfigContext.Consumer;
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
DirectionType,
|
||||
ConfigConsumerProps,
|
||||
Theme,
|
||||
defaultIconPrefixCls,
|
||||
} from './context';
|
||||
import SizeContext, { SizeContextProvider, SizeType } from './SizeContext';
|
||||
import message from '../message';
|
||||
@ -20,6 +21,8 @@ import notification from '../notification';
|
||||
import { RequiredMark } from '../form/Form';
|
||||
import { registerTheme } from './cssVariables';
|
||||
import defaultLocale from '../locale/default';
|
||||
import { DesignToken, DesignTokenContext } from '../_util/theme';
|
||||
import defaultThemeToken from '../_util/theme/default';
|
||||
|
||||
export {
|
||||
RenderEmptyHandler,
|
||||
@ -80,6 +83,9 @@ export interface ConfigProviderProps {
|
||||
};
|
||||
virtual?: boolean;
|
||||
dropdownMatchSelectWidth?: boolean;
|
||||
theme?: {
|
||||
token?: Partial<DesignToken>;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProviderChildrenProps extends ConfigProviderProps {
|
||||
@ -88,7 +94,7 @@ interface ProviderChildrenProps extends ConfigProviderProps {
|
||||
}
|
||||
|
||||
export const defaultPrefixCls = 'ant';
|
||||
export const defaultIconPrefixCls = 'anticon';
|
||||
export { defaultIconPrefixCls };
|
||||
let globalPrefixCls: string;
|
||||
let globalIconPrefixCls: string;
|
||||
|
||||
@ -159,6 +165,7 @@ const ProviderChildren: React.FC<ProviderChildrenProps> = props => {
|
||||
legacyLocale,
|
||||
parentContext,
|
||||
iconPrefixCls,
|
||||
theme = {},
|
||||
} = props;
|
||||
|
||||
const getPrefixCls = React.useCallback(
|
||||
@ -248,10 +255,28 @@ const ProviderChildren: React.FC<ProviderChildrenProps> = props => {
|
||||
childNode = <SizeContextProvider size={componentSize}>{childNode}</SizeContextProvider>;
|
||||
}
|
||||
|
||||
// ================================ Dynamic theme ================================
|
||||
const memoTheme = React.useMemo(
|
||||
() => ({
|
||||
token: {
|
||||
...defaultThemeToken,
|
||||
...theme?.token,
|
||||
},
|
||||
}),
|
||||
[theme?.token],
|
||||
);
|
||||
if (theme?.token) {
|
||||
childNode = (
|
||||
<DesignTokenContext.Provider value={memoTheme}>{childNode}</DesignTokenContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// =================================== Render ===================================
|
||||
return <ConfigContext.Provider value={memoedConfig}>{childNode}</ConfigContext.Provider>;
|
||||
};
|
||||
|
||||
const ConfigProvider: React.FC<ConfigProviderProps> & {
|
||||
/** @private internal Usage. do not use in your production */
|
||||
ConfigContext: typeof ConfigContext;
|
||||
SizeContext: typeof SizeContext;
|
||||
config: typeof setGlobalConfig;
|
||||
@ -284,7 +309,6 @@ const ConfigProvider: React.FC<ConfigProviderProps> & {
|
||||
);
|
||||
};
|
||||
|
||||
/** @private internal Usage. do not use in your production */
|
||||
ConfigProvider.ConfigContext = ConfigContext;
|
||||
ConfigProvider.SizeContext = SizeContext;
|
||||
ConfigProvider.config = setGlobalConfig;
|
||||
|
@ -1,7 +1,7 @@
|
||||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
@import '../../input/style/mixin';
|
||||
@import '../../button/style/mixin';
|
||||
@import '../../button/_style/mixin';
|
||||
@import '../../grid/style/mixin';
|
||||
@import './components';
|
||||
@import './inline';
|
||||
|
@ -1,7 +1,7 @@
|
||||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
@import '../../input/style/mixin';
|
||||
@import '../../button/style/mixin';
|
||||
@import '../../button/_style/mixin';
|
||||
@import '../../grid/style/mixin';
|
||||
|
||||
@form-prefix-cls: ~'@{ant-prefix}-form';
|
||||
|
@ -1,6 +1,6 @@
|
||||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
@import '../../button/style/mixin';
|
||||
@import '../../button/_style/mixin';
|
||||
@import './mixin';
|
||||
|
||||
@search-prefix: ~'@{ant-prefix}-input-search';
|
||||
|
@ -88,13 +88,14 @@
|
||||
"site:theme-dark": "cross-env ESBUILD=1 ANT_THEME=dark bisheng build -c ./site/bisheng.config.js",
|
||||
"site:theme-compact": "cross-env ESBUILD=1 ANT_THEME=compact bisheng build -c ./site/bisheng.config.js",
|
||||
"site": "npm run site:theme && cross-env NODE_ICU_DATA=node_modules/full-icu ESBUILD=1 bisheng build --ssr -c ./site/bisheng.config.js",
|
||||
"site-tmp": "cross-env NODE_ICU_DATA=node_modules/full-icu ESBUILD=1 bisheng build --ssr -c ./site/bisheng.config.js",
|
||||
"sort": "npx sort-package-json",
|
||||
"sort-api": "antd-tools run sort-api-table",
|
||||
"start": "antd-tools run clean && cross-env NODE_ENV=development concurrently \"bisheng start -c ./site/bisheng.config.js\"",
|
||||
"test": "jest --config .jest.js --cache=false",
|
||||
"test:update": "jest --config .jest.js --cache=false -u",
|
||||
"test-all": "sh -e ./scripts/test-all.sh",
|
||||
"test-node": "jest --config .jest.node.js --cache=false",
|
||||
"test-node": "npm run version && jest --config .jest.node.js --cache=false",
|
||||
"tsc": "tsc --noEmit",
|
||||
"site:test": "jest --config .jest.site.js --cache=false --force-exit",
|
||||
"test-image": "npm run dist && docker-compose run tests",
|
||||
@ -112,6 +113,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^6.0.0",
|
||||
"@ant-design/cssinjs": "~0.0.0-alpha.10",
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@ant-design/react-slick": "~0.28.1",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@ -157,7 +159,7 @@
|
||||
"devDependencies": {
|
||||
"@ant-design/bisheng-plugin": "^3.0.1",
|
||||
"@ant-design/hitu": "^0.0.0-alpha.13",
|
||||
"@ant-design/tools": "^14.0.2",
|
||||
"@ant-design/tools": "^14.1.0",
|
||||
"@docsearch/css": "^3.0.0-alpha.39",
|
||||
"@docsearch/react": "^3.0.0-alpha.39",
|
||||
"@qixian.cs/github-contributors-list": "^1.0.3",
|
||||
@ -296,7 +298,7 @@
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": "./dist/antd.min.js",
|
||||
"maxSize": "280 kB"
|
||||
"maxSize": "350 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/antd.min.css",
|
||||
|
@ -1,6 +1,7 @@
|
||||
module.exports = {
|
||||
locale: 'en-US',
|
||||
messages: {
|
||||
'app.theme.switch.dynamic': 'Dynamic Theme',
|
||||
'app.theme.switch.default': 'Default theme',
|
||||
'app.theme.switch.dark': 'Dark theme',
|
||||
'app.theme.switch.compact': 'Compact theme',
|
||||
|
@ -227,10 +227,11 @@ a {
|
||||
position: fixed;
|
||||
right: 32px;
|
||||
bottom: 102px;
|
||||
z-index: 2147483640;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
row-gap: 16px;
|
||||
|
||||
&-tooltip {
|
||||
.ant-tooltip-inner {
|
||||
|
102
site/theme/template/Layout/DynamicTheme.tsx
Normal file
102
site/theme/template/Layout/DynamicTheme.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import * as React from 'react';
|
||||
import { TinyColor } from '@ctrl/tinycolor';
|
||||
import { Drawer, Form, Input, Button, InputNumber } from 'antd';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { BugOutlined } from '@ant-design/icons';
|
||||
import { DesignToken } from '../../../../components/_util/theme';
|
||||
import defaultTheme from '../../../../components/_util/theme/default';
|
||||
|
||||
export interface ThemeConfigProps {
|
||||
defaultToken: DesignToken;
|
||||
onChangeTheme: (theme: DesignToken) => void;
|
||||
}
|
||||
|
||||
export default ({ onChangeTheme, defaultToken }: ThemeConfigProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const keys = Object.keys(defaultTheme);
|
||||
|
||||
const onFinish = (nextToken: DesignToken) => {
|
||||
onChangeTheme(nextToken);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
right: 0,
|
||||
bottom: 32,
|
||||
fontSize: 16,
|
||||
borderRadius: '4px 0 0 4px',
|
||||
background: '#FFF',
|
||||
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)',
|
||||
padding: '8px 16px 8px 12px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
<BugOutlined /> Dynamic Theme
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
zIndex={10001}
|
||||
visible={visible}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
title={formatMessage({ id: 'app.theme.switch.dynamic' })}
|
||||
extra={
|
||||
<Button onClick={form.submit} type="primary">
|
||||
Submit
|
||||
</Button>
|
||||
}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={defaultToken}
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
{keys.map((key: keyof typeof defaultToken) => {
|
||||
const originValue = defaultToken[key];
|
||||
const originValueType = typeof originValue;
|
||||
|
||||
let node: React.ReactElement;
|
||||
|
||||
switch (originValueType) {
|
||||
case 'number':
|
||||
node = <InputNumber />;
|
||||
break;
|
||||
|
||||
default:
|
||||
node = <Input />;
|
||||
}
|
||||
|
||||
const rules: any[] = [{ required: true }];
|
||||
const originColor = new TinyColor(originValue);
|
||||
if (originValueType === 'string' && originColor.isValid) {
|
||||
rules.push({
|
||||
validator: async (_: any, value: string) => {
|
||||
if (!new TinyColor(value).isValid) {
|
||||
throw new Error('Invalidate color type');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item key={key} label={key} name={key} rules={rules} validateFirst>
|
||||
{node}
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
</Form>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
@ -15,6 +15,9 @@ import SiteContext from './SiteContext';
|
||||
import enLocale from '../../en-US';
|
||||
import cnLocale from '../../zh-CN';
|
||||
import * as utils from '../utils';
|
||||
import defaultDesignToken from '../../../../components/_util/theme/default';
|
||||
|
||||
import DynamicTheme from './DynamicTheme';
|
||||
|
||||
if (typeof window !== 'undefined' && navigator.serviceWorker) {
|
||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
@ -78,6 +81,7 @@ export default class Layout extends React.Component {
|
||||
setTheme: this.setTheme,
|
||||
direction: 'ltr',
|
||||
setIframeTheme: this.setIframeTheme,
|
||||
designToken: defaultDesignToken,
|
||||
};
|
||||
}
|
||||
|
||||
@ -206,7 +210,8 @@ export default class Layout extends React.Component {
|
||||
|
||||
render() {
|
||||
const { children, helmetContext = {}, ...restProps } = this.props;
|
||||
const { appLocale, direction, isMobile, theme, setTheme, setIframeTheme } = this.state;
|
||||
const { appLocale, direction, isMobile, theme, setTheme, setIframeTheme, designToken } =
|
||||
this.state;
|
||||
const title =
|
||||
appLocale.locale === 'zh-CN'
|
||||
? 'Ant Design - 一套企业级 UI 设计语言和 React 组件库'
|
||||
@ -248,9 +253,22 @@ export default class Layout extends React.Component {
|
||||
<ConfigProvider
|
||||
locale={appLocale.locale === 'zh-CN' ? zhCN : null}
|
||||
direction={direction}
|
||||
theme={{
|
||||
token: designToken,
|
||||
}}
|
||||
>
|
||||
<Header {...restProps} changeDirection={this.changeDirection} />
|
||||
{children}
|
||||
|
||||
<DynamicTheme
|
||||
defaultToken={designToken}
|
||||
onChangeTheme={newToken => {
|
||||
console.log('Change Theme:', newToken);
|
||||
this.setState({
|
||||
designToken: newToken,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
</IntlProvider>
|
||||
</HelmetProvider>
|
||||
|
@ -1,6 +1,7 @@
|
||||
module.exports = {
|
||||
locale: 'zh-CN',
|
||||
messages: {
|
||||
'app.theme.switch.dynamic': '动态主题',
|
||||
'app.theme.switch.default': '默认主题',
|
||||
'app.theme.switch.dark': '暗黑主题',
|
||||
'app.theme.switch.compact': '紧凑主题',
|
||||
|
@ -4,6 +4,7 @@ import glob from 'glob';
|
||||
import { render } from 'enzyme';
|
||||
import MockDate from 'mockdate';
|
||||
import moment from 'moment';
|
||||
import { StyleProvider, createCache } from '@ant-design/cssinjs';
|
||||
import { TriggerProps } from 'rc-trigger';
|
||||
import { excludeWarning } from './excludeWarning';
|
||||
|
||||
@ -85,6 +86,9 @@ function baseText(doInject: boolean, component: string, options: Options = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
// Inject cssinjs cache to avoid create <style /> element
|
||||
demo = <StyleProvider cache={createCache()}>{demo}</StyleProvider>;
|
||||
|
||||
const wrapper = render(demo);
|
||||
|
||||
// Convert aria related content
|
||||
|
Loading…
Reference in New Issue
Block a user