Refactor/spin cssinjs (#34653)

* TODO: fix test case

* TODO: 样式、test文件的import

* TODO: 伪类不生效

* temp changed

* fix: disable eslint

* chore: update snapshots

* chore: 解决伪类不生效

* chore: update token prop name

* TODO: mark FIXME

* fix: recover snapshot

* fix: recover snapshot again

* chore: update snapshot

Co-authored-by: Zack Chang <zackchangjx@foxmail.com>
This commit is contained in:
Zack Chang 2022-03-25 14:26:09 +08:00 committed by GitHub
parent 1627207255
commit c8b2b28c95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 305 additions and 23 deletions

View File

@ -40,6 +40,7 @@ import Rate from '../../rate';
import Select from '../../select'; import Select from '../../select';
import Skeleton from '../../skeleton'; import Skeleton from '../../skeleton';
import Slider from '../../slider'; import Slider from '../../slider';
// eslint-disable-next-line import/no-named-as-default
import Spin from '../../spin'; import Spin from '../../spin';
import Statistic from '../../statistic'; import Statistic from '../../statistic';
import Steps from '../../steps'; import Steps from '../../steps';

View File

@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
// eslint-disable-next-line import/no-named-as-default
import Spin, { SpinProps } from '../spin'; import Spin, { SpinProps } from '../spin';
import useBreakpoint from '../grid/hooks/useBreakpoint'; import useBreakpoint from '../grid/hooks/useBreakpoint';
import { Breakpoint, responsiveArray } from '../_util/responsiveObserve'; import { Breakpoint, responsiveArray } from '../_util/responsiveObserve';

View File

@ -3,6 +3,7 @@ import classNames from 'classnames';
import RcMentions from 'rc-mentions'; import RcMentions from 'rc-mentions';
import { MentionsProps as RcMentionsProps } from 'rc-mentions/lib/Mentions'; import { MentionsProps as RcMentionsProps } from 'rc-mentions/lib/Mentions';
import { composeRef } from 'rc-util/lib/ref'; import { composeRef } from 'rc-util/lib/ref';
// eslint-disable-next-line import/no-named-as-default
import Spin from '../spin'; import Spin from '../spin';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import { FormItemInputContext } from '../form/context'; import { FormItemInputContext } from '../form/context';

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import Spin from '..'; // eslint-disable-next-line import/no-named-as-default
import Spin, { Spin as SpinClass } from '..';
import { sleep } from '../../../tests/utils'; import { sleep } from '../../../tests/utils';
describe('delay spinning', () => { describe('delay spinning', () => {
@ -24,8 +25,8 @@ describe('delay spinning', () => {
it('should cancel debounce function when unmount', async () => { it('should cancel debounce function when unmount', async () => {
const wrapper = mount(<Spin spinning delay={100} />); const wrapper = mount(<Spin spinning delay={100} />);
const spy = jest.spyOn(wrapper.instance().updateSpinning, 'cancel'); const spy = jest.spyOn(wrapper.find(SpinClass).instance().updateSpinning, 'cancel');
expect(wrapper.instance().updateSpinning.cancel).toEqual(expect.any(Function)); expect(wrapper.find(SpinClass).instance().updateSpinning.cancel).toEqual(expect.any(Function));
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
wrapper.unmount(); wrapper.unmount();
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { render, mount } from 'enzyme'; import { render, mount } from 'enzyme';
import Spin from '..'; // eslint-disable-next-line import/no-named-as-default
import Spin, { Spin as SpinClass } from '..';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest'; import rtlTest from '../../../tests/shared/rtlTest';
@ -26,9 +27,9 @@ describe('Spin', () => {
it('should be controlled by spinning', () => { it('should be controlled by spinning', () => {
const wrapper = mount(<Spin spinning={false} />); const wrapper = mount(<Spin spinning={false} />);
expect(wrapper.instance().state.spinning).toBe(false); expect(wrapper.find(SpinClass).instance().state.spinning).toBe(false);
wrapper.setProps({ spinning: true }); wrapper.setProps({ spinning: true });
expect(wrapper.instance().state.spinning).toBe(true); expect(wrapper.find(SpinClass).instance().state.spinning).toBe(true);
}); });
it('if indicator set null should not be render default indicator', () => { it('if indicator set null should not be render default indicator', () => {

View File

@ -2,9 +2,10 @@ import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import omit from 'rc-util/lib/omit'; import omit from 'rc-util/lib/omit';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; import { ConfigConsumer, ConfigConsumerProps, ConfigContext } from '../config-provider';
import { tuple } from '../_util/type'; import { tuple } from '../_util/type';
import { isValidElement, cloneElement } from '../_util/reactNode'; import { isValidElement, cloneElement } from '../_util/reactNode';
import useStyle from './style/index';
const SpinSizes = tuple('small', 'default', 'large'); const SpinSizes = tuple('small', 'default', 'large');
export type SpinSize = typeof SpinSizes[number]; export type SpinSize = typeof SpinSizes[number];
@ -22,6 +23,15 @@ export interface SpinProps {
indicator?: SpinIndicator; indicator?: SpinIndicator;
} }
export interface SpinClassProps extends SpinProps {
hashId: string;
spinPrefixCls: string;
}
export type SpinFCType = React.FC<SpinProps> & {
setDefaultIndicator: (indicator: React.ReactNode) => void;
};
export interface SpinState { export interface SpinState {
spinning?: boolean; spinning?: boolean;
notCssAnimationSupported?: boolean; notCssAnimationSupported?: boolean;
@ -30,7 +40,7 @@ export interface SpinState {
// Render indicator // Render indicator
let defaultIndicator: React.ReactNode = null; let defaultIndicator: React.ReactNode = null;
function renderIndicator(prefixCls: string, props: SpinProps): React.ReactNode { function renderIndicator(prefixCls: string, props: SpinClassProps): React.ReactNode {
const { indicator } = props; const { indicator } = props;
const dotClassName = `${prefixCls}-dot`; const dotClassName = `${prefixCls}-dot`;
@ -65,20 +75,16 @@ function shouldDelay(spinning?: boolean, delay?: number): boolean {
return !!spinning && !!delay && !isNaN(Number(delay)); return !!spinning && !!delay && !isNaN(Number(delay));
} }
class Spin extends React.Component<SpinProps, SpinState> { export class Spin extends React.Component<SpinClassProps, SpinState> {
static defaultProps = { static defaultProps = {
spinning: true, spinning: true,
size: 'default' as SpinSize, size: 'default' as SpinSize,
wrapperClassName: '', wrapperClassName: '',
}; };
static setDefaultIndicator(indicator: React.ReactNode) {
defaultIndicator = indicator;
}
originalUpdateSpinning: () => void; originalUpdateSpinning: () => void;
constructor(props: SpinProps) { constructor(props: SpinClassProps) {
super(props); super(props);
const { spinning, delay } = props; const { spinning, delay } = props;
@ -103,7 +109,7 @@ class Spin extends React.Component<SpinProps, SpinState> {
this.cancelExistingSpin(); this.cancelExistingSpin();
} }
debouncifyUpdateSpinning = (props?: SpinProps) => { debouncifyUpdateSpinning = (props?: SpinClassProps) => {
const { delay } = props || this.props; const { delay } = props || this.props;
if (delay) { if (delay) {
this.cancelExistingSpin(); this.cancelExistingSpin();
@ -130,9 +136,10 @@ class Spin extends React.Component<SpinProps, SpinState> {
return !!(this.props && typeof this.props.children !== 'undefined'); return !!(this.props && typeof this.props.children !== 'undefined');
} }
renderSpin = ({ getPrefixCls, direction }: ConfigConsumerProps) => { renderSpin = ({ direction }: ConfigConsumerProps) => {
const { const {
prefixCls: customizePrefixCls, spinPrefixCls: prefixCls,
hashId,
className, className,
size, size,
tip, tip,
@ -142,7 +149,6 @@ class Spin extends React.Component<SpinProps, SpinState> {
} = this.props; } = this.props;
const { spinning } = this.state; const { spinning } = this.state;
const prefixCls = getPrefixCls('spin', customizePrefixCls);
const spinClassName = classNames( const spinClassName = classNames(
prefixCls, prefixCls,
{ {
@ -153,10 +159,11 @@ class Spin extends React.Component<SpinProps, SpinState> {
[`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-rtl`]: direction === 'rtl',
}, },
className, className,
hashId,
); );
// fix https://fb.me/react-unknown-prop // fix https://fb.me/react-unknown-prop
const divProps = omit(restProps, ['spinning', 'delay', 'indicator']); const divProps = omit(restProps, ['spinning', 'delay', 'indicator', 'prefixCls']);
const spinElement = ( const spinElement = (
<div <div
@ -175,7 +182,10 @@ class Spin extends React.Component<SpinProps, SpinState> {
[`${prefixCls}-blur`]: spinning, [`${prefixCls}-blur`]: spinning,
}); });
return ( return (
<div {...divProps} className={classNames(`${prefixCls}-nested-loading`, wrapperClassName)}> <div
{...divProps}
className={classNames(`${prefixCls}-nested-loading`, wrapperClassName, hashId)}
>
{spinning && <div key="loading">{spinElement}</div>} {spinning && <div key="loading">{spinElement}</div>}
<div className={containerClassName} key="container"> <div className={containerClassName} key="container">
{this.props.children} {this.props.children}
@ -191,4 +201,28 @@ class Spin extends React.Component<SpinProps, SpinState> {
} }
} }
export default Spin; const SpinFC: SpinFCType = (props: SpinProps) => {
const { prefixCls: customizePrefixCls } = props;
const { getPrefixCls } = React.useContext(ConfigContext);
const spinPrefixCls = getPrefixCls('spin', customizePrefixCls);
const [wrapSSR, hashId] = useStyle(spinPrefixCls);
const spinClassProps: SpinClassProps = {
...props,
spinPrefixCls,
hashId,
};
return wrapSSR(<Spin {...spinClassProps} />);
};
SpinFC.setDefaultIndicator = (indicator: React.ReactNode) => {
defaultIndicator = indicator;
};
if (process.env.NODE_ENV !== 'production') {
SpinFC.displayName = 'Spin';
}
export default SpinFC;

View File

@ -1,2 +1,244 @@
import '../../style/index.less'; // deps-lint-skip-all
import './index.less'; import { CSSObject, Keyframes } from '@ant-design/cssinjs';
import {
useStyleRegister,
useToken,
resetComponent,
GenerateStyle,
UseComponentStyleResult,
} from '../../_util/theme';
import type { DerivativeToken } from '../../_util/theme';
interface SpinToken extends DerivativeToken {
spinCls: string;
spinDotDefault: string;
spinDotSize: number;
spinDotSizeSM: number;
spinDotSizeLG: number;
}
const antSpinMove = new Keyframes('antSpinMove', {
to: { opacity: 1 },
});
const antRotate = new Keyframes('antRotate', {
to: { transform: 'rotate(405deg)' },
});
const genSpinStyle: GenerateStyle<SpinToken> = (token: SpinToken, hashId: string): CSSObject => ({
[`${token.spinCls}`]: {
...resetComponent(token),
position: 'absolute',
display: 'none',
color: token.colorPrimary,
textAlign: 'center',
verticalAlign: 'middle',
opacity: 0,
transition: `transform ${token.motionDurationSlow} ${token.motionEaseInOutCirc}`,
'&-spinning': {
position: 'static',
display: 'inline-block',
opacity: 1,
},
'&-nested-loading': {
position: 'relative',
[`> div > ${token.spinCls}`]: {
position: 'absolute',
top: 0,
insetInlineStart: 0,
zIndex: 4,
display: 'block',
width: '100%',
height: '100%',
maxHeight: 400, // FIXME: hard code in v4
[`${token.spinCls}-dot`]: {
position: 'absolute',
top: '50%',
insetInlineStart: '50%',
margin: -token.spinDotSize / 2,
},
[`${token.spinCls}-text`]: {
position: 'absolute',
top: '50%',
width: '100%',
paddingTop: (token.spinDotSize - token.fontSize) / 2 + 2,
textShadow: `0 1px 2px ${token.colorBgComponent}`,
},
[`&${token.spinCls}-show-text ${token.spinCls}-dot`]: {
marginTop: -(token.spinDotSize / 2) - 10,
},
[`> div > ${token.spinCls}-sm`]: {
[`${token.spinCls}-dot`]: {
margin: -token.spinDotSizeSM / 2,
},
[`${token.spinCls}-text`]: {
paddingTop: (token.spinDotSizeSM - token.fontSize) / 2 + 2,
},
[`&${token.spinCls}-show-text ${token.spinCls}-dot`]: {
marginTop: -(token.spinDotSizeSM / 2) - 10,
},
},
[`> div > ${token.spinCls}-lg`]: {
[`${token.spinCls}-dot`]: {
margin: -(token.spinDotSizeLG / 2),
},
[`${token.spinCls}-text`]: {
paddingTop: (token.spinDotSizeLG - token.fontSize) / 2 + 2,
},
[`&${token.spinCls}-show-text ${token.spinCls}-dot`]: {
marginTop: -(token.spinDotSizeLG / 2) - 10,
},
},
},
[`${token.spinCls}-container`]: {
position: 'relative',
transition: `opacity ${token.motionDurationSlow}`,
'&::after': {
position: 'absolute',
top: 0,
insetInlineEnd: 0,
bottom: 0,
insetInlineStart: 0,
zIndex: 10,
width: '100%',
height: '100%',
background: token.colorBgComponent,
opacity: 0,
transition: `all ${token.motionDurationSlow}`,
content: '""',
pointerEvents: 'none',
},
},
[`${token.spinCls}-blur`]: {
clear: 'both',
opacity: 0.5,
userSelect: 'none',
pointerEvents: 'none',
[`&::after`]: {
opacity: 0.4,
pointerEvents: 'auto',
},
},
},
// tip
// ------------------------------
[`&-tip`]: {
color: token.spinDotDefault,
},
// dots
// ------------------------------
[`${token.spinCls}-dot`]: {
position: 'relative',
display: 'inline-block',
fontSize: token.spinDotSize,
width: '1em',
height: '1em',
'&-item': {
position: 'absolute',
display: 'block',
width: 9, // FIXME: hard code in v4
height: 9, // FIXME: hard code in v4
backgroundColor: token.colorPrimary,
borderRadius: '100%',
transform: 'scale(0.75)',
transformOrigin: '50% 50%',
opacity: 0.3,
animation: `${antSpinMove.getName(hashId)} 1s infinite linear alternate`,
'&:nth-child(1)': {
top: 0,
insetInlineStart: 0,
},
'&:nth-child(2)': {
top: 0,
insetInlineEnd: 0,
animationDelay: '0.4s',
},
'&:nth-child(3)': {
insetInlineEnd: 0,
bottom: 0,
animationDelay: '0.8s',
},
'&:nth-child(4)': {
bottom: 0,
insetInlineStart: 0,
animationDelay: '1.2s',
},
},
'&-spin': {
transform: 'rotate(45deg)',
animation: `${antRotate.getName(hashId)} 1.2s infinite linear`,
},
},
// Sizes
// ------------------------------
// small
[`&-sm ${token.spinCls}-dot`]: {
fontSize: token.spinDotSizeSM,
i: {
width: 6, // FIXME: hard code in v4
height: 6, // FIXME: hard code in v4
},
},
// large
[`&-lg ${token.spinCls}-dot`]: {
fontSize: token.spinDotSizeLG,
i: {
width: 14, // FIXME: hard code in v4
height: 14, // FIXME: hard code in v4
},
},
[`&${token.spinCls}-show-text ${token.spinCls}-text`]: {
display: 'block',
},
// animation
antSpinMove,
antRotate,
},
});
// ============================== Export ==============================
export default function useStyle(prefixCls: string): UseComponentStyleResult {
const [theme, token, hashId] = useToken();
const spinToken: SpinToken = {
...token,
spinCls: `.${prefixCls}`,
spinDotDefault: token.colorTextSecondary,
spinDotSize: 20, // FIXME: hard code in v4
spinDotSizeSM: 14, // FIXME: hard code in v4
spinDotSizeLG: 32, // FIXME: hard code in v4
};
return [
useStyleRegister({ theme, token, hashId, path: [prefixCls] }, () => [
genSpinStyle(spinToken, hashId),
]),
hashId,
];
}

View File

@ -4,6 +4,7 @@ import omit from 'rc-util/lib/omit';
import RcTable, { Summary } from 'rc-table'; import RcTable, { Summary } from 'rc-table';
import { TableProps as RcTableProps, INTERNAL_HOOKS } from 'rc-table/lib/Table'; import { TableProps as RcTableProps, INTERNAL_HOOKS } from 'rc-table/lib/Table';
import { convertChildrenToColumns } from 'rc-table/lib/hooks/useColumns'; import { convertChildrenToColumns } from 'rc-table/lib/hooks/useColumns';
// eslint-disable-next-line import/no-named-as-default
import Spin, { SpinProps } from '../spin'; import Spin, { SpinProps } from '../spin';
import Pagination from '../pagination'; import Pagination from '../pagination';
import { TooltipProps } from '../tooltip'; import { TooltipProps } from '../tooltip';