mirror of
https://github.com/ant-design/ant-design.git
synced 2025-01-18 22:36:31 +08:00
feat: Add skeleton button (#19699)
* #18237 add skeleton button * update snapshot * refactoring demo code * lint code * update test snapshots * add test * update snapshot * fix deps lint issue * set avatar siz and update test snapshots * fix lint issue * remove button and just keep skeleton * add active switch for button demo * add size and shape radio for skeleton button demo * add button tests * add skeleton tests * update doc * update test snapshots * omit avatar and button props * refactoring skeleton and update test snapshots
This commit is contained in:
parent
f32235d074
commit
060b7ded20
@ -1,47 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import SkeletonElement, { SkeletonElementProps } from './SkeletonElement';
|
||||
|
||||
export interface SkeletonAvatarProps {
|
||||
prefixCls?: string;
|
||||
className?: string;
|
||||
style?: object;
|
||||
size?: 'large' | 'small' | 'default' | number;
|
||||
export interface AvatarProps extends Omit<SkeletonElementProps, 'shape'> {
|
||||
shape?: 'circle' | 'square';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
class SkeletonAvatar extends React.Component<SkeletonAvatarProps, any> {
|
||||
static defaultProps: Partial<SkeletonAvatarProps> = {
|
||||
class SkeletonAvatar extends React.Component<AvatarProps, any> {
|
||||
static defaultProps: Partial<SkeletonElementProps> = {
|
||||
size: 'large',
|
||||
};
|
||||
|
||||
render() {
|
||||
const { prefixCls, className, style, size, shape } = this.props;
|
||||
|
||||
const sizeCls = classNames({
|
||||
[`${prefixCls}-lg`]: size === 'large',
|
||||
[`${prefixCls}-sm`]: size === 'small',
|
||||
});
|
||||
|
||||
const shapeCls = classNames({
|
||||
[`${prefixCls}-circle`]: shape === 'circle',
|
||||
[`${prefixCls}-square`]: shape === 'square',
|
||||
});
|
||||
|
||||
const sizeStyle: React.CSSProperties =
|
||||
typeof size === 'number'
|
||||
? {
|
||||
width: size,
|
||||
height: size,
|
||||
lineHeight: `${size}px`,
|
||||
}
|
||||
: {};
|
||||
return (
|
||||
<span
|
||||
className={classNames(prefixCls, className, sizeCls, shapeCls)}
|
||||
style={{ ...sizeStyle, ...style }}
|
||||
/>
|
||||
);
|
||||
return <SkeletonElement {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
37
components/skeleton/Button.tsx
Normal file
37
components/skeleton/Button.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import omit from 'omit.js';
|
||||
import classNames from 'classnames';
|
||||
import SkeletonElement, { SkeletonElementProps } from './SkeletonElement';
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
|
||||
interface SkeletonButtonProps extends Omit<SkeletonElementProps, 'size'> {
|
||||
active?: boolean;
|
||||
size?: 'large' | 'small' | 'default';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
class SkeletonButton extends React.Component<SkeletonButtonProps, any> {
|
||||
static defaultProps: Partial<SkeletonButtonProps> = {
|
||||
size: 'default',
|
||||
};
|
||||
|
||||
renderSkeletonButton = ({ getPrefixCls }: ConfigConsumerProps) => {
|
||||
const { prefixCls: customizePrefixCls, className, active } = this.props;
|
||||
const prefixCls = getPrefixCls('skeleton', customizePrefixCls);
|
||||
const otherProps = omit(this.props, ['prefixCls']);
|
||||
const cls = classNames(prefixCls, className, `${prefixCls}-element`, {
|
||||
[`${prefixCls}-active`]: active,
|
||||
});
|
||||
return (
|
||||
<div className={cls}>
|
||||
<SkeletonElement prefixCls={`${prefixCls}-button`} {...otherProps} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <ConfigConsumer>{this.renderSkeletonButton}</ConfigConsumer>;
|
||||
}
|
||||
}
|
||||
|
||||
export default SkeletonButton;
|
164
components/skeleton/Skeleton.tsx
Normal file
164
components/skeleton/Skeleton.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Avatar, { AvatarProps } from './Avatar';
|
||||
import Title, { SkeletonTitleProps } from './Title';
|
||||
import Paragraph, { SkeletonParagraphProps } from './Paragraph';
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
import SkeletonButton from './Button';
|
||||
|
||||
export interface SkeletonProps {
|
||||
active?: boolean;
|
||||
loading?: boolean;
|
||||
prefixCls?: string;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
avatar?: AvatarProps | boolean;
|
||||
title?: SkeletonTitleProps | boolean;
|
||||
paragraph?: SkeletonParagraphProps | boolean;
|
||||
}
|
||||
|
||||
function getComponentProps<T>(prop: T | boolean | undefined): T | {} {
|
||||
if (prop && typeof prop === 'object') {
|
||||
return prop;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function getAvatarBasicProps(hasTitle: boolean, hasParagraph: boolean): AvatarProps {
|
||||
if (hasTitle && !hasParagraph) {
|
||||
return { shape: 'square' };
|
||||
}
|
||||
|
||||
return { shape: 'circle' };
|
||||
}
|
||||
|
||||
function getTitleBasicProps(hasAvatar: boolean, hasParagraph: boolean): SkeletonTitleProps {
|
||||
if (!hasAvatar && hasParagraph) {
|
||||
return { width: '38%' };
|
||||
}
|
||||
|
||||
if (hasAvatar && hasParagraph) {
|
||||
return { width: '50%' };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function getParagraphBasicProps(hasAvatar: boolean, hasTitle: boolean): SkeletonParagraphProps {
|
||||
const basicProps: SkeletonParagraphProps = {};
|
||||
|
||||
// Width
|
||||
if (!hasAvatar || !hasTitle) {
|
||||
basicProps.width = '61%';
|
||||
}
|
||||
|
||||
// Rows
|
||||
if (!hasAvatar && hasTitle) {
|
||||
basicProps.rows = 3;
|
||||
} else {
|
||||
basicProps.rows = 2;
|
||||
}
|
||||
|
||||
return basicProps;
|
||||
}
|
||||
|
||||
class Skeleton extends React.Component<SkeletonProps, any> {
|
||||
static Button: typeof SkeletonButton;
|
||||
|
||||
static defaultProps: Partial<SkeletonProps> = {
|
||||
avatar: false,
|
||||
title: true,
|
||||
paragraph: true,
|
||||
};
|
||||
|
||||
renderSkeleton = ({ getPrefixCls }: ConfigConsumerProps) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
loading,
|
||||
className,
|
||||
children,
|
||||
avatar,
|
||||
title,
|
||||
paragraph,
|
||||
active,
|
||||
} = this.props;
|
||||
|
||||
const prefixCls = getPrefixCls('skeleton', customizePrefixCls);
|
||||
|
||||
if (loading || !('loading' in this.props)) {
|
||||
const hasAvatar = !!avatar;
|
||||
const hasTitle = !!title;
|
||||
const hasParagraph = !!paragraph;
|
||||
|
||||
// Avatar
|
||||
let avatarNode;
|
||||
if (hasAvatar) {
|
||||
const avatarProps: AvatarProps = {
|
||||
prefixCls: `${prefixCls}-avatar`,
|
||||
...getAvatarBasicProps(hasTitle, hasParagraph),
|
||||
...getComponentProps(avatar),
|
||||
};
|
||||
|
||||
avatarNode = (
|
||||
<div className={`${prefixCls}-header`}>
|
||||
<Avatar {...avatarProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let contentNode;
|
||||
if (hasTitle || hasParagraph) {
|
||||
// Title
|
||||
let $title;
|
||||
if (hasTitle) {
|
||||
const titleProps: SkeletonTitleProps = {
|
||||
prefixCls: `${prefixCls}-title`,
|
||||
...getTitleBasicProps(hasAvatar, hasParagraph),
|
||||
...getComponentProps(title),
|
||||
};
|
||||
|
||||
$title = <Title {...titleProps} />;
|
||||
}
|
||||
|
||||
// Paragraph
|
||||
let paragraphNode;
|
||||
if (hasParagraph) {
|
||||
const paragraphProps: SkeletonParagraphProps = {
|
||||
prefixCls: `${prefixCls}-paragraph`,
|
||||
...getParagraphBasicProps(hasAvatar, hasTitle),
|
||||
...getComponentProps(paragraph),
|
||||
};
|
||||
|
||||
paragraphNode = <Paragraph {...paragraphProps} />;
|
||||
}
|
||||
|
||||
contentNode = (
|
||||
<div className={`${prefixCls}-content`}>
|
||||
{$title}
|
||||
{paragraphNode}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const cls = classNames(prefixCls, className, {
|
||||
[`${prefixCls}-with-avatar`]: hasAvatar,
|
||||
[`${prefixCls}-active`]: active,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cls}>
|
||||
{avatarNode}
|
||||
{contentNode}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
render() {
|
||||
return <ConfigConsumer>{this.renderSkeleton}</ConfigConsumer>;
|
||||
}
|
||||
}
|
||||
|
||||
export default Skeleton;
|
45
components/skeleton/SkeletonElement.tsx
Normal file
45
components/skeleton/SkeletonElement.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export interface SkeletonElementProps {
|
||||
prefixCls?: string;
|
||||
className?: string;
|
||||
style?: object;
|
||||
size?: 'large' | 'small' | 'default' | number;
|
||||
shape?: 'circle' | 'square' | 'round';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
class SkeletonElement extends React.Component<SkeletonElementProps, any> {
|
||||
render() {
|
||||
const { prefixCls, className, style, size, shape } = this.props;
|
||||
|
||||
const sizeCls = classNames({
|
||||
[`${prefixCls}-lg`]: size === 'large',
|
||||
[`${prefixCls}-sm`]: size === 'small',
|
||||
});
|
||||
|
||||
const shapeCls = classNames({
|
||||
[`${prefixCls}-circle`]: shape === 'circle',
|
||||
[`${prefixCls}-square`]: shape === 'square',
|
||||
[`${prefixCls}-round`]: shape === 'round',
|
||||
});
|
||||
|
||||
const sizeStyle: React.CSSProperties =
|
||||
typeof size === 'number'
|
||||
? {
|
||||
width: size,
|
||||
height: size,
|
||||
lineHeight: `${size}px`,
|
||||
}
|
||||
: {};
|
||||
return (
|
||||
<span
|
||||
className={classNames(prefixCls, className, sizeCls, shapeCls)}
|
||||
style={{ ...sizeStyle, ...style }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SkeletonElement;
|
@ -48,6 +48,223 @@ exports[`renders ./components/skeleton/demo/basic.md correctly 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/skeleton/demo/button.md correctly 1`] = `
|
||||
<div>
|
||||
<form
|
||||
class="ant-form ant-form-inline"
|
||||
style="margin-bottom:16px"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Active"
|
||||
>
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<button
|
||||
aria-checked="false"
|
||||
class="ant-switch"
|
||||
role="switch"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="ant-switch-inner"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-row ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Size"
|
||||
>
|
||||
Size
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-radio-group ant-radio-group-outline"
|
||||
>
|
||||
<label
|
||||
class="ant-radio-button-wrapper ant-radio-button-wrapper-checked"
|
||||
>
|
||||
<span
|
||||
class="ant-radio-button ant-radio-button-checked"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="ant-radio-button-input"
|
||||
type="radio"
|
||||
value="default"
|
||||
/>
|
||||
<span
|
||||
class="ant-radio-button-inner"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
Default
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="ant-radio-button-wrapper"
|
||||
>
|
||||
<span
|
||||
class="ant-radio-button"
|
||||
>
|
||||
<input
|
||||
class="ant-radio-button-input"
|
||||
type="radio"
|
||||
value="large"
|
||||
/>
|
||||
<span
|
||||
class="ant-radio-button-inner"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
Large
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="ant-radio-button-wrapper"
|
||||
>
|
||||
<span
|
||||
class="ant-radio-button"
|
||||
>
|
||||
<input
|
||||
class="ant-radio-button-input"
|
||||
type="radio"
|
||||
value="small"
|
||||
/>
|
||||
<span
|
||||
class="ant-radio-button-inner"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
Small
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-row ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Shape"
|
||||
>
|
||||
Shape
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-radio-group ant-radio-group-outline"
|
||||
>
|
||||
<label
|
||||
class="ant-radio-button-wrapper ant-radio-button-wrapper-checked"
|
||||
>
|
||||
<span
|
||||
class="ant-radio-button ant-radio-button-checked"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="ant-radio-button-input"
|
||||
type="radio"
|
||||
value="default"
|
||||
/>
|
||||
<span
|
||||
class="ant-radio-button-inner"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
Default
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="ant-radio-button-wrapper"
|
||||
>
|
||||
<span
|
||||
class="ant-radio-button"
|
||||
>
|
||||
<input
|
||||
class="ant-radio-button-input"
|
||||
type="radio"
|
||||
value="round"
|
||||
/>
|
||||
<span
|
||||
class="ant-radio-button-inner"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
Round
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="ant-radio-button-wrapper"
|
||||
>
|
||||
<span
|
||||
class="ant-radio-button"
|
||||
>
|
||||
<input
|
||||
class="ant-radio-button-input"
|
||||
type="radio"
|
||||
value="circle"
|
||||
/>
|
||||
<span
|
||||
class="ant-radio-button-inner"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
Circle
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
class="ant-skeleton ant-skeleton-element"
|
||||
>
|
||||
<span
|
||||
class="ant-skeleton-button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/skeleton/demo/children.md correctly 1`] = `
|
||||
<div
|
||||
class="article"
|
||||
|
@ -169,6 +169,76 @@ exports[`Skeleton avatar size 4`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Skeleton button active 1`] = `
|
||||
<div
|
||||
class="ant-skeleton ant-skeleton-element ant-skeleton-active"
|
||||
>
|
||||
<span
|
||||
class="ant-skeleton-button"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Skeleton button shape 1`] = `
|
||||
<div
|
||||
class="ant-skeleton ant-skeleton-element"
|
||||
>
|
||||
<span
|
||||
class="ant-skeleton-button"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Skeleton button shape 2`] = `
|
||||
<div
|
||||
class="ant-skeleton ant-skeleton-element"
|
||||
>
|
||||
<span
|
||||
class="ant-skeleton-button ant-skeleton-button-round"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Skeleton button shape 3`] = `
|
||||
<div
|
||||
class="ant-skeleton ant-skeleton-element"
|
||||
>
|
||||
<span
|
||||
class="ant-skeleton-button ant-skeleton-button-circle"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Skeleton button size 1`] = `
|
||||
<div
|
||||
class="ant-skeleton ant-skeleton-element"
|
||||
>
|
||||
<span
|
||||
class="ant-skeleton-button"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Skeleton button size 2`] = `
|
||||
<div
|
||||
class="ant-skeleton ant-skeleton-element"
|
||||
>
|
||||
<span
|
||||
class="ant-skeleton-button ant-skeleton-button-lg"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Skeleton button size 3`] = `
|
||||
<div
|
||||
class="ant-skeleton ant-skeleton-element"
|
||||
>
|
||||
<span
|
||||
class="ant-skeleton-button ant-skeleton-button-sm"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Skeleton paragraph rows 1`] = `
|
||||
<div
|
||||
class="ant-skeleton"
|
||||
@ -245,6 +315,41 @@ exports[`Skeleton paragraph width 2`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Skeleton should square avatar 1`] = `
|
||||
<div
|
||||
class="ant-skeleton ant-skeleton-with-avatar"
|
||||
>
|
||||
<div
|
||||
class="ant-skeleton-header"
|
||||
>
|
||||
<span
|
||||
class="ant-skeleton-avatar ant-skeleton-avatar-lg ant-skeleton-avatar-square"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-skeleton-content"
|
||||
>
|
||||
<h3
|
||||
class="ant-skeleton-title"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Skeleton should without avatar and paragraph 1`] = `
|
||||
<div
|
||||
class="ant-skeleton"
|
||||
>
|
||||
<div
|
||||
class="ant-skeleton-content"
|
||||
>
|
||||
<h3
|
||||
class="ant-skeleton-title"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Skeleton title width 1`] = `
|
||||
<div
|
||||
class="ant-skeleton"
|
||||
|
@ -10,9 +10,20 @@ describe('Skeleton', () => {
|
||||
Bamboo
|
||||
</Skeleton>,
|
||||
);
|
||||
const genSkeletonButton = props => mount(<Skeleton.Button {...props} />);
|
||||
|
||||
mountTest(Skeleton);
|
||||
|
||||
it('should without avatar and paragraph', () => {
|
||||
const wrapperSmall = genSkeleton({ avatar: false, paragraph: false });
|
||||
expect(wrapperSmall.render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should square avatar', () => {
|
||||
const wrapperSmall = genSkeleton({ avatar: true, paragraph: false });
|
||||
expect(wrapperSmall.render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('avatar', () => {
|
||||
it('size', () => {
|
||||
const wrapperSmall = genSkeleton({ avatar: { size: 'small' } });
|
||||
@ -53,4 +64,27 @@ describe('Skeleton', () => {
|
||||
expect(wrapperList.render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('button', () => {
|
||||
it('active', () => {
|
||||
const wrapper = genSkeletonButton({ active: true });
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
});
|
||||
it('size', () => {
|
||||
const wrapperDefault = genSkeletonButton({ size: 'default' });
|
||||
expect(wrapperDefault.render()).toMatchSnapshot();
|
||||
const wrapperLarge = genSkeletonButton({ size: 'large' });
|
||||
expect(wrapperLarge.render()).toMatchSnapshot();
|
||||
const wrapperSmall = genSkeletonButton({ size: 'small' });
|
||||
expect(wrapperSmall.render()).toMatchSnapshot();
|
||||
});
|
||||
it('shape', () => {
|
||||
const wrapperDefault = genSkeletonButton({ shape: 'default' });
|
||||
expect(wrapperDefault.render()).toMatchSnapshot();
|
||||
const wrapperRound = genSkeletonButton({ shape: 'round' });
|
||||
expect(wrapperRound.render()).toMatchSnapshot();
|
||||
const wrapperCircle = genSkeletonButton({ shape: 'circle' });
|
||||
expect(wrapperCircle.render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
68
components/skeleton/demo/button.md
Normal file
68
components/skeleton/demo/button.md
Normal file
@ -0,0 +1,68 @@
|
||||
---
|
||||
order: 2
|
||||
title:
|
||||
zh-CN: 骨架按钮
|
||||
en-US: Skeleton Button
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
骨架按钮。
|
||||
|
||||
## en-US
|
||||
|
||||
Skeleton Button.
|
||||
|
||||
```jsx
|
||||
import { Skeleton, Switch, Form, Radio } from 'antd';
|
||||
|
||||
class Demo extends React.Component {
|
||||
state = {
|
||||
active: false,
|
||||
size: 'default',
|
||||
shape: 'default',
|
||||
};
|
||||
|
||||
handleActiveChange = checked => {
|
||||
this.setState({ active: checked });
|
||||
};
|
||||
|
||||
handleSizeChange = e => {
|
||||
this.setState({ size: e.target.value });
|
||||
};
|
||||
|
||||
handleShapeChange = e => {
|
||||
this.setState({ shape: e.target.value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { active, size, shape } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<Form layout="inline" style={{ marginBottom: 16 }}>
|
||||
<Form.Item label="Active">
|
||||
<Switch checked={active} onChange={this.handleActiveChange} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Size">
|
||||
<Radio.Group value={size} onChange={this.handleSizeChange}>
|
||||
<Radio.Button value="default">Default</Radio.Button>
|
||||
<Radio.Button value="large">Large</Radio.Button>
|
||||
<Radio.Button value="small">Small</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="Shape">
|
||||
<Radio.Group value={shape} onChange={this.handleShapeChange}>
|
||||
<Radio.Button value="default">Default</Radio.Button>
|
||||
<Radio.Button value="round">Round</Radio.Button>
|
||||
<Radio.Button value="circle">Circle</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Skeleton.Button {...this.state} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<Demo />, mountNode);
|
||||
```
|
@ -45,3 +45,11 @@ Provide a placeholder while you wait for content to load, or to visualise conten
|
||||
| --- | --- | --- | --- |
|
||||
| rows | Set the row count of paragraph | number | - |
|
||||
| width | Set the width of paragraph. When width is an Array, it can set the width of each row. Otherwise only set the last row width | number \| string \| Array<number \| string> | - |
|
||||
|
||||
### SkeletonButtonProps
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| -------- | ----------------------- | ------------------------------------ | ------- |
|
||||
| active | Show animation effect | boolean | false |
|
||||
| size | Set the size of button | Enum{ 'large', 'small', 'default' } | - |
|
||||
| shape | Set the shape of button | Enum{ 'circle', 'round', 'default' } | - |
|
||||
|
@ -1,161 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Avatar, { SkeletonAvatarProps } from './Avatar';
|
||||
import Title, { SkeletonTitleProps } from './Title';
|
||||
import Paragraph, { SkeletonParagraphProps } from './Paragraph';
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
import Skeleton from './Skeleton';
|
||||
import SkeletonButton from './Button';
|
||||
|
||||
export interface SkeletonProps {
|
||||
active?: boolean;
|
||||
loading?: boolean;
|
||||
prefixCls?: string;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
avatar?: SkeletonAvatarProps | boolean;
|
||||
title?: SkeletonTitleProps | boolean;
|
||||
paragraph?: SkeletonParagraphProps | boolean;
|
||||
}
|
||||
|
||||
function getComponentProps<T>(prop: T | boolean | undefined): T | {} {
|
||||
if (prop && typeof prop === 'object') {
|
||||
return prop;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function getAvatarBasicProps(hasTitle: boolean, hasParagraph: boolean): SkeletonAvatarProps {
|
||||
if (hasTitle && !hasParagraph) {
|
||||
return { shape: 'square' };
|
||||
}
|
||||
|
||||
return { shape: 'circle' };
|
||||
}
|
||||
|
||||
function getTitleBasicProps(hasAvatar: boolean, hasParagraph: boolean): SkeletonTitleProps {
|
||||
if (!hasAvatar && hasParagraph) {
|
||||
return { width: '38%' };
|
||||
}
|
||||
|
||||
if (hasAvatar && hasParagraph) {
|
||||
return { width: '50%' };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function getParagraphBasicProps(hasAvatar: boolean, hasTitle: boolean): SkeletonParagraphProps {
|
||||
const basicProps: SkeletonParagraphProps = {};
|
||||
|
||||
// Width
|
||||
if (!hasAvatar || !hasTitle) {
|
||||
basicProps.width = '61%';
|
||||
}
|
||||
|
||||
// Rows
|
||||
if (!hasAvatar && hasTitle) {
|
||||
basicProps.rows = 3;
|
||||
} else {
|
||||
basicProps.rows = 2;
|
||||
}
|
||||
|
||||
return basicProps;
|
||||
}
|
||||
|
||||
class Skeleton extends React.Component<SkeletonProps, any> {
|
||||
static defaultProps: Partial<SkeletonProps> = {
|
||||
avatar: false,
|
||||
title: true,
|
||||
paragraph: true,
|
||||
};
|
||||
|
||||
renderSkeleton = ({ getPrefixCls }: ConfigConsumerProps) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
loading,
|
||||
className,
|
||||
children,
|
||||
avatar,
|
||||
title,
|
||||
paragraph,
|
||||
active,
|
||||
} = this.props;
|
||||
|
||||
const prefixCls = getPrefixCls('skeleton', customizePrefixCls);
|
||||
|
||||
if (loading || !('loading' in this.props)) {
|
||||
const hasAvatar = !!avatar;
|
||||
const hasTitle = !!title;
|
||||
const hasParagraph = !!paragraph;
|
||||
|
||||
// Avatar
|
||||
let avatarNode;
|
||||
if (hasAvatar) {
|
||||
const avatarProps: SkeletonAvatarProps = {
|
||||
prefixCls: `${prefixCls}-avatar`,
|
||||
...getAvatarBasicProps(hasTitle, hasParagraph),
|
||||
...getComponentProps(avatar),
|
||||
};
|
||||
|
||||
avatarNode = (
|
||||
<div className={`${prefixCls}-header`}>
|
||||
<Avatar {...avatarProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let contentNode;
|
||||
if (hasTitle || hasParagraph) {
|
||||
// Title
|
||||
let $title;
|
||||
if (hasTitle) {
|
||||
const titleProps: SkeletonTitleProps = {
|
||||
prefixCls: `${prefixCls}-title`,
|
||||
...getTitleBasicProps(hasAvatar, hasParagraph),
|
||||
...getComponentProps(title),
|
||||
};
|
||||
|
||||
$title = <Title {...titleProps} />;
|
||||
}
|
||||
|
||||
// Paragraph
|
||||
let paragraphNode;
|
||||
if (hasParagraph) {
|
||||
const paragraphProps: SkeletonParagraphProps = {
|
||||
prefixCls: `${prefixCls}-paragraph`,
|
||||
...getParagraphBasicProps(hasAvatar, hasTitle),
|
||||
...getComponentProps(paragraph),
|
||||
};
|
||||
|
||||
paragraphNode = <Paragraph {...paragraphProps} />;
|
||||
}
|
||||
|
||||
contentNode = (
|
||||
<div className={`${prefixCls}-content`}>
|
||||
{$title}
|
||||
{paragraphNode}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const cls = classNames(prefixCls, className, {
|
||||
[`${prefixCls}-with-avatar`]: hasAvatar,
|
||||
[`${prefixCls}-active`]: active,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cls}>
|
||||
{avatarNode}
|
||||
{contentNode}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
render() {
|
||||
return <ConfigConsumer>{this.renderSkeleton}</ConfigConsumer>;
|
||||
}
|
||||
}
|
||||
export { SkeletonProps } from './Skeleton';
|
||||
|
||||
Skeleton.Button = SkeletonButton;
|
||||
export default Skeleton;
|
||||
|
@ -46,3 +46,11 @@ cols: 1
|
||||
| --- | --- | --- | --- |
|
||||
| rows | 设置段落占位图的行数 | number | - |
|
||||
| width | 设置段落占位图的宽度,若为数组时则为对应的每行宽度,反之则是最后一行的宽度 | number \| string \| Array<number \| string> | - |
|
||||
|
||||
### SkeletonButtonProps
|
||||
|
||||
| 属性 | 说明 | 类型 | 默认值 |
|
||||
| ------ | ---------------- | ------------------------------------ | ------ |
|
||||
| active | 是否展示动画效果 | boolean | false |
|
||||
| size | 设置按钮的大小 | Enum{ 'large', 'small', 'default' } | - |
|
||||
| shape | 指定按钮的形状 | Enum{ 'circle', 'round', 'default' } | - |
|
||||
|
@ -5,6 +5,7 @@
|
||||
@skeleton-avatar-prefix-cls: ~'@{skeleton-prefix-cls}-avatar';
|
||||
@skeleton-title-prefix-cls: ~'@{skeleton-prefix-cls}-title';
|
||||
@skeleton-paragraph-prefix-cls: ~'@{skeleton-prefix-cls}-paragraph';
|
||||
@skeleton-button-prefix-cls: ~'@{skeleton-prefix-cls}-button';
|
||||
|
||||
@skeleton-to-color: shade(@skeleton-color, 5%);
|
||||
|
||||
@ -23,14 +24,14 @@
|
||||
vertical-align: top;
|
||||
background: @skeleton-color;
|
||||
|
||||
.avatar-size(@avatar-size-base);
|
||||
.skeleton-element-avatar-size(@avatar-size-base);
|
||||
|
||||
&-lg {
|
||||
.avatar-size(@avatar-size-lg);
|
||||
.skeleton-element-avatar-size(@avatar-size-lg);
|
||||
}
|
||||
|
||||
&-sm {
|
||||
.avatar-size(@avatar-size-sm);
|
||||
.skeleton-element-avatar-size(@avatar-size-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -96,19 +97,63 @@
|
||||
.@{skeleton-avatar-prefix-cls} {
|
||||
.skeleton-color();
|
||||
}
|
||||
|
||||
.@{skeleton-button-prefix-cls} {
|
||||
.skeleton-color();
|
||||
}
|
||||
}
|
||||
|
||||
// Skeleton element
|
||||
&-element {
|
||||
display: inline-block;
|
||||
// Button
|
||||
.@{skeleton-button-prefix-cls} {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
background: @skeleton-color;
|
||||
border-radius: @border-radius-base;
|
||||
|
||||
.skeleton-element-button-size(@btn-height-base);
|
||||
|
||||
&-lg {
|
||||
.skeleton-element-button-size(@btn-height-lg);
|
||||
}
|
||||
|
||||
&-sm {
|
||||
.skeleton-element-button-size(@btn-height-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-size(@size) {
|
||||
.skeleton-element-avatar-size(@size) {
|
||||
width: @size;
|
||||
height: @size;
|
||||
line-height: @size;
|
||||
.skeleton-element-common-size(@size);
|
||||
|
||||
&.@{skeleton-avatar-prefix-cls}-circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-element-button-size(@size) {
|
||||
width: @size * 2;
|
||||
.skeleton-element-common-size(@size);
|
||||
|
||||
&.@{skeleton-button-prefix-cls}-circle {
|
||||
width: @size;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&.@{skeleton-button-prefix-cls}-round {
|
||||
border-radius: @size;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-element-common-size(@size) {
|
||||
height: @size;
|
||||
line-height: @size;
|
||||
}
|
||||
|
||||
.skeleton-color() {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
|
Loading…
Reference in New Issue
Block a user