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:
二货机器人 2022-02-18 14:17:32 +08:00 committed by GitHub
parent 32c68591ce
commit 912ffb15d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 770 additions and 21 deletions

View File

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

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

View 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,
};
}

View File

@ -0,0 +1,2 @@
import '../../style/index.less';
import './index.less';

View File

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

View File

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

View 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();
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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",

View File

@ -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',

View File

@ -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 {

View 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>
</>
);
};

View File

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

View File

@ -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': '紧凑主题',

View File

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