chore: auto merge branches (#49189)

chore: merge feature into master
This commit is contained in:
github-actions[bot] 2024-06-03 04:09:04 +00:00 committed by GitHub
commit 9e367246d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
114 changed files with 4458 additions and 1724 deletions

View File

@ -3,7 +3,7 @@ import React, { useState } from 'react';
import Avatar from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render } from '../../../tests/utils';
import { fireEvent, render, waitFakeTimer } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import useBreakpoint from '../../grid/hooks/useBreakpoint';
@ -222,4 +222,83 @@ describe('Avatar Render', () => {
expect(container.querySelector('.ant-avatar-sm')).toBeTruthy();
expect(container.querySelector('.ant-avatar-lg')).toBeTruthy();
});
it('Avatar.Group support max series props and prompt to deprecated', async () => {
const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
jest.useFakeTimers();
const { container } = render(
<Avatar.Group maxCount={2} maxStyle={{ color: 'blue' }} maxPopoverPlacement="bottom">
<Avatar>A</Avatar>
<Avatar>B</Avatar>
<Avatar>C</Avatar>
<Avatar>D</Avatar>
</Avatar.Group>,
);
const avatars = container?.querySelectorAll<HTMLSpanElement>('.ant-avatar-group .ant-avatar');
fireEvent.mouseEnter(avatars?.[2]);
await waitFakeTimer();
/* check style */
expect(container.querySelector('.ant-popover-open')).toBeTruthy();
expect(container.querySelector('.ant-popover-open')).toHaveStyle('color: blue');
/* check count */
expect(avatars.length).toBe(3);
/* check popover */
const popover = container.querySelector('.ant-avatar-group-popover');
expect(popover).toBeTruthy();
expect(popover).toHaveClass('ant-popover-placement-bottom');
expect(errSpy).toHaveBeenNthCalledWith(
1,
'Warning: [antd: Avatar.Group] `maxCount` is deprecated. Please use `max={{ count: number }}` instead.',
);
expect(errSpy).toHaveBeenNthCalledWith(
2,
'Warning: [antd: Avatar.Group] `maxStyle` is deprecated. Please use `max={{ style: CSSProperties }}` instead.',
);
expect(errSpy).toHaveBeenNthCalledWith(
3,
'Warning: [antd: Avatar.Group] `maxPopoverPlacement` is deprecated. Please use `max={{ popover: PopoverProps }}` instead.',
);
});
it('Avatar.Group support max object props', () => {
const { container } = render(
<Avatar.Group
max={{
count: 2,
popover: {
placement: 'bottomRight',
overlayClassName: 'wanpan-111',
overlayStyle: { background: 'red' },
content: 'Avatar.Group',
open: true,
},
style: {
color: 'blue',
},
}}
>
<Avatar>A</Avatar>
<Avatar>B</Avatar>
<Avatar>C</Avatar>
<Avatar>D</Avatar>
</Avatar.Group>,
);
/* check count */
expect(container.querySelectorAll('.ant-avatar-group .ant-avatar').length).toBe(3);
/* check popover */
const popover = container.querySelector('.ant-avatar-group-popover');
expect(popover).toBeTruthy();
expect(popover).toHaveStyle('background: red');
expect(popover).toHaveClass('wanpan-111 ant-popover-placement-bottomRight');
expect(container.querySelector('.ant-popover-inner-content')).toHaveTextContent('Avatar.Group');
/* check style */
expect(container.querySelector('.ant-popover-open')).toHaveStyle('color: blue');
});
});

View File

@ -7,7 +7,7 @@ demoTest('avatar');
rootPropsTest(
'avatar',
(Avatar, props) => (
<Avatar.Group {...props} maxCount={1}>
<Avatar.Group {...props} max={{ count: 1 }}>
<Avatar>Bamboo</Avatar>
<Avatar>Light</Avatar>
</Avatar.Group>

View File

@ -28,7 +28,12 @@ const App: React.FC = () => (
</Avatar>
</Space>
<Space>
<Avatar.Group maxCount={2} maxStyle={{ color: '#f56a00', backgroundColor: '#fde3cf' }}>
<Avatar.Group
max={{
count: 2,
style: { color: '#f56a00', backgroundColor: '#fde3cf' },
}}
>
<Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=2" />
<Avatar style={{ backgroundColor: '#f56a00' }}>K</Avatar>
<Tooltip title="Ant User" placement="top">

View File

@ -15,7 +15,12 @@ const App: React.FC = () => (
<Avatar style={{ backgroundColor: '#1677ff' }} icon={<AntDesignOutlined />} />
</Avatar.Group>
<Divider />
<Avatar.Group maxCount={2} maxStyle={{ color: '#f56a00', backgroundColor: '#fde3cf' }}>
<Avatar.Group
max={{
count: 2,
style: { color: '#f56a00', backgroundColor: '#fde3cf' },
}}
>
<Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=2" />
<Avatar style={{ backgroundColor: '#f56a00' }}>K</Avatar>
<Tooltip title="Ant User" placement="top">
@ -25,9 +30,11 @@ const App: React.FC = () => (
</Avatar.Group>
<Divider />
<Avatar.Group
maxCount={2}
size="large"
maxStyle={{ color: '#f56a00', backgroundColor: '#fde3cf' }}
max={{
count: 2,
style: { color: '#f56a00', backgroundColor: '#fde3cf' },
}}
>
<Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=3" />
<Avatar style={{ backgroundColor: '#f56a00' }}>K</Avatar>
@ -38,10 +45,12 @@ const App: React.FC = () => (
</Avatar.Group>
<Divider />
<Avatar.Group
maxCount={2}
maxPopoverTrigger="click"
size="large"
maxStyle={{ color: '#f56a00', backgroundColor: '#fde3cf', cursor: 'pointer' }}
max={{
count: 2,
style: { color: '#f56a00', backgroundColor: '#fde3cf', cursor: 'pointer' },
popover: { trigger: 'click' },
}}
>
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
<Avatar style={{ backgroundColor: '#f56a00' }}>K</Avatar>

View File

@ -3,8 +3,10 @@ import classNames from 'classnames';
import toArray from 'rc-util/lib/Children/toArray';
import { cloneElement } from '../_util/reactNode';
import { devUseWarning } from '../_util/warning';
import { ConfigContext } from '../config-provider';
import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
import type { PopoverProps } from '../popover';
import Popover from '../popover';
import Avatar from './avatar';
import AvatarContext from './AvatarContext';
@ -32,10 +34,19 @@ export interface GroupProps {
children?: React.ReactNode;
style?: React.CSSProperties;
prefixCls?: string;
/** @deprecated Please use `max={{ count: number }}` */
maxCount?: number;
/** @deprecated Please use `max={{ style: CSSProperties }}` */
maxStyle?: React.CSSProperties;
/** @deprecated Please use `max={{ popover: PopoverProps }}` */
maxPopoverPlacement?: 'top' | 'bottom';
/** @deprecated Please use `max={{ popover: PopoverProps }}` */
maxPopoverTrigger?: 'hover' | 'focus' | 'click';
max?: {
count?: number;
style?: React.CSSProperties;
popover?: PopoverProps;
};
/*
* Size of avatar, options: `large`, `small`, `default`
* or a custom number size
@ -55,11 +66,24 @@ const Group: React.FC<GroupProps> = (props) => {
maxStyle,
size,
shape,
maxPopoverPlacement = 'top',
maxPopoverTrigger = 'hover',
maxPopoverPlacement,
maxPopoverTrigger,
children,
max,
} = props;
if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Avatar.Group');
warning.deprecated(!maxCount, 'maxCount', 'max={{ count: number }}');
warning.deprecated(!maxStyle, 'maxStyle', 'max={{ style: CSSProperties }}');
warning.deprecated(
!maxPopoverPlacement,
'maxPopoverPlacement',
'max={{ popover: PopoverProps }}',
);
warning.deprecated(!maxPopoverTrigger, 'maxPopoverTrigger', 'max={{ popover: PopoverProps }}');
}
const prefixCls = getPrefixCls('avatar', customizePrefixCls);
const groupPrefixCls = `${prefixCls}-group`;
const rootCls = useCSSVarCls(prefixCls);
@ -81,22 +105,30 @@ const Group: React.FC<GroupProps> = (props) => {
cloneElement(child, { key: `avatar-key-${index}` }),
);
const mergeCount = max?.count || maxCount;
const numOfChildren = childrenWithProps.length;
if (maxCount && maxCount < numOfChildren) {
const childrenShow = childrenWithProps.slice(0, maxCount);
const childrenHidden = childrenWithProps.slice(maxCount, numOfChildren);
if (mergeCount && mergeCount < numOfChildren) {
const childrenShow = childrenWithProps.slice(0, mergeCount);
const childrenHidden = childrenWithProps.slice(mergeCount, numOfChildren);
const mergeStyle = max?.style || maxStyle;
const mergePopoverTrigger = max?.popover?.trigger || maxPopoverTrigger || 'hover';
const mergePopoverPlacement = max?.popover?.placement || maxPopoverPlacement || 'top';
const mergeProps = {
content: childrenHidden,
...max?.popover,
overlayClassName: classNames(`${groupPrefixCls}-popover`, max?.popover?.overlayClassName),
placement: mergePopoverPlacement,
trigger: mergePopoverTrigger,
};
childrenShow.push(
<Popover
key="avatar-popover-key"
content={childrenHidden}
trigger={maxPopoverTrigger}
placement={maxPopoverPlacement}
overlayClassName={`${groupPrefixCls}-popover`}
destroyTooltipOnHide
>
<Avatar style={maxStyle}>{`+${numOfChildren - maxCount}`}</Avatar>
<Popover key="avatar-popover-key" destroyTooltipOnHide {...mergeProps}>
<Avatar style={mergeStyle}>{`+${numOfChildren - mergeCount}`}</Avatar>
</Popover>,
);
return wrapCSSVar(
<AvatarContextProvider shape={shape} size={size}>
<div className={cls} style={style}>

View File

@ -49,10 +49,7 @@ Common props ref[Common props](/docs/react/common-props)
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| maxCount | Max avatars to show | number | - | |
| maxPopoverPlacement | The placement of excess avatar Popover | `top` \| `bottom` | `top` | |
| maxPopoverTrigger | Set the trigger of excess avatar Popover | `hover` \| `focus` \| `click` | `hover` | 4.17.0 |
| maxStyle | The style of excess avatar style | CSSProperties | - | |
| max | Set maximum display related configurations, Before `5.18.0` you can use [parameters](https://github.com/ant-design/ant-design/blob/9d134859becbdae5b9ce276f6d9af4264691d81f/components/avatar/group.tsx#L35-L38) | `{ count?: number; style?: CSSProperties; popover?: PopoverProps }` | - | 5.18.0 |
| size | The size of the avatar | number \| `large` \| `small` \| `default` \| { xs: number, sm: number, ...} | `default` | 4.8.0 |
| shape | The shape of the avatar | `circle` \| `square` | `circle` | 5.8.0 |

View File

@ -54,10 +54,7 @@ group:
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| maxCount | 显示的最大头像个数 | number | - | |
| maxPopoverPlacement | 多余头像气泡弹出位置 | `top` \| `bottom` | `top` | |
| maxPopoverTrigger | 设置多余头像 Popover 的触发方式 | `hover` \| `focus` \| `click` | `hover` | 4.17.0 |
| maxStyle | 多余头像样式 | CSSProperties | - | |
| max | 设置最多显示相关配置,`5.18.0` 前可使用 [参数](https://github.com/ant-design/ant-design/blob/9d134859becbdae5b9ce276f6d9af4264691d81f/components/avatar/group.tsx#L35-L38) | `{ count?: number; style?: CSSProperties; popover?: PopoverProps }` | - | 5.18.0 |
| size | 设置头像的大小 | number \| `large` \| `small` \| `default` \| { xs: number, sm: number, ...} | `default` | 4.8.0 |
| shape | 设置头像的形状 | `circle` \| `square` | `circle` | 5.8.0 |

View File

@ -3,7 +3,6 @@ import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import classNames from 'classnames';
import CSSMotion from 'rc-motion';
import type { ButtonProps } from './button';
import IconWrapper from './IconWrapper';
type InnerLoadingIconProps = {
@ -11,14 +10,11 @@ type InnerLoadingIconProps = {
className?: string;
style?: React.CSSProperties;
iconClassName?: string;
} & Pick<ButtonProps, 'iconPosition'>;
};
const InnerLoadingIcon = forwardRef<HTMLSpanElement, InnerLoadingIconProps>((props, ref) => {
const { prefixCls, className, style, iconClassName, iconPosition = 'start' } = props;
const mergedIconCls = classNames(className, {
[`${prefixCls}-loading-icon-end`]: iconPosition === 'end',
[`${prefixCls}-loading-icon`]: iconPosition === 'start',
});
const { prefixCls, className, style, iconClassName } = props;
const mergedIconCls = classNames(`${prefixCls}-loading-icon`, className);
return (
<IconWrapper prefixCls={prefixCls} className={mergedIconCls} style={style} ref={ref}>
@ -33,7 +29,7 @@ export type LoadingIconProps = {
loading?: boolean | object;
className?: string;
style?: React.CSSProperties;
} & Pick<ButtonProps, 'iconPosition'>;
};
const getCollapsedWidth = (): React.CSSProperties => ({
width: 0,
@ -48,18 +44,11 @@ const getRealWidth = (node: HTMLElement): React.CSSProperties => ({
});
const LoadingIcon: React.FC<LoadingIconProps> = (props) => {
const { prefixCls, loading, existIcon, className, style, iconPosition } = props;
const { prefixCls, loading, existIcon, className, style } = props;
const visible = !!loading;
if (existIcon) {
return (
<InnerLoadingIcon
prefixCls={prefixCls}
className={className}
style={style}
iconPosition={iconPosition}
/>
);
return <InnerLoadingIcon prefixCls={prefixCls} className={className} style={style} />;
}
return (
@ -83,7 +72,6 @@ const LoadingIcon: React.FC<LoadingIconProps> = (props) => {
style={{ ...style, ...motionStyle }}
ref={ref}
iconClassName={motionCls}
iconPosition={iconPosition}
/>
)}
</CSSMotion>

View File

@ -1645,14 +1645,11 @@ Array [
</span>
</button>
<button
class="ant-btn ant-btn-primary"
class="ant-btn ant-btn-primary ant-btn-icon-end"
type="button"
>
<span>
Search
</span>
<span
class="ant-btn-icon ant-btn-icon-end"
class="ant-btn-icon"
>
<span
aria-label="search"
@ -1674,6 +1671,9 @@ Array [
</svg>
</span>
</span>
<span>
Search
</span>
</button>
<button
class="ant-btn ant-btn-circle ant-btn-default ant-btn-icon-only"
@ -1723,14 +1723,11 @@ Array [
</div>
</div>
<button
class="ant-btn ant-btn-default"
class="ant-btn ant-btn-default ant-btn-icon-end"
type="button"
>
<span>
Search
</span>
<span
class="ant-btn-icon ant-btn-icon-end"
class="ant-btn-icon"
>
<span
aria-label="search"
@ -1752,6 +1749,9 @@ Array [
</svg>
</span>
</span>
<span>
Search
</span>
</button>
</div>
<div
@ -1805,14 +1805,11 @@ Array [
</div>
</div>
<button
class="ant-btn ant-btn-text"
class="ant-btn ant-btn-text ant-btn-icon-end"
type="button"
>
<span>
Search
</span>
<span
class="ant-btn-icon ant-btn-icon-end"
class="ant-btn-icon"
>
<span
aria-label="search"
@ -1834,6 +1831,9 @@ Array [
</svg>
</span>
</span>
<span>
Search
</span>
</button>
<button
class="ant-btn ant-btn-circle ant-btn-dashed ant-btn-icon-only"
@ -1883,14 +1883,11 @@ Array [
</div>
</div>
<button
class="ant-btn ant-btn-dashed"
class="ant-btn ant-btn-dashed ant-btn-icon-end"
type="button"
>
<span>
Search
</span>
<span
class="ant-btn-icon ant-btn-icon-end"
class="ant-btn-icon"
>
<span
aria-label="search"
@ -1912,9 +1909,12 @@ Array [
</svg>
</span>
</span>
<span>
Search
</span>
</button>
<a
class="ant-btn ant-btn-default ant-btn-icon-only"
class="ant-btn ant-btn-default ant-btn-icon-only ant-btn-icon-end"
href="https://www.google.com"
tabindex="0"
>
@ -1943,14 +1943,11 @@ Array [
</span>
</a>
<button
class="ant-btn ant-btn-primary ant-btn-loading"
class="ant-btn ant-btn-primary ant-btn-loading ant-btn-icon-end"
type="button"
>
<span>
Loading
</span>
<span
class="ant-btn-icon ant-btn-loading-icon-end"
class="ant-btn-icon ant-btn-loading-icon"
style="width: 0px; opacity: 0; transform: scale(0);"
>
<span
@ -1973,6 +1970,9 @@ Array [
</svg>
</span>
</span>
<span>
Loading
</span>
</button>
</div>
</div>,

View File

@ -1451,14 +1451,11 @@ Array [
</span>
</button>
<button
class="ant-btn ant-btn-primary"
class="ant-btn ant-btn-primary ant-btn-icon-end"
type="button"
>
<span>
Search
</span>
<span
class="ant-btn-icon ant-btn-icon-end"
class="ant-btn-icon"
>
<span
aria-label="search"
@ -1480,6 +1477,9 @@ Array [
</svg>
</span>
</span>
<span>
Search
</span>
</button>
<button
class="ant-btn ant-btn-circle ant-btn-default ant-btn-icon-only"
@ -1510,14 +1510,11 @@ Array [
</span>
</button>
<button
class="ant-btn ant-btn-default"
class="ant-btn ant-btn-default ant-btn-icon-end"
type="button"
>
<span>
Search
</span>
<span
class="ant-btn-icon ant-btn-icon-end"
class="ant-btn-icon"
>
<span
aria-label="search"
@ -1539,6 +1536,9 @@ Array [
</svg>
</span>
</span>
<span>
Search
</span>
</button>
</div>
<div
@ -1573,14 +1573,11 @@ Array [
</span>
</button>
<button
class="ant-btn ant-btn-text"
class="ant-btn ant-btn-text ant-btn-icon-end"
type="button"
>
<span>
Search
</span>
<span
class="ant-btn-icon ant-btn-icon-end"
class="ant-btn-icon"
>
<span
aria-label="search"
@ -1602,6 +1599,9 @@ Array [
</svg>
</span>
</span>
<span>
Search
</span>
</button>
<button
class="ant-btn ant-btn-circle ant-btn-dashed ant-btn-icon-only"
@ -1632,14 +1632,11 @@ Array [
</span>
</button>
<button
class="ant-btn ant-btn-dashed"
class="ant-btn ant-btn-dashed ant-btn-icon-end"
type="button"
>
<span>
Search
</span>
<span
class="ant-btn-icon ant-btn-icon-end"
class="ant-btn-icon"
>
<span
aria-label="search"
@ -1661,9 +1658,12 @@ Array [
</svg>
</span>
</span>
<span>
Search
</span>
</button>
<a
class="ant-btn ant-btn-default ant-btn-icon-only"
class="ant-btn ant-btn-default ant-btn-icon-only ant-btn-icon-end"
href="https://www.google.com"
tabindex="0"
>
@ -1692,14 +1692,11 @@ Array [
</span>
</a>
<button
class="ant-btn ant-btn-primary ant-btn-loading"
class="ant-btn ant-btn-primary ant-btn-loading ant-btn-icon-end"
type="button"
>
<span>
Loading
</span>
<span
class="ant-btn-icon ant-btn-loading-icon-end"
class="ant-btn-icon ant-btn-loading-icon"
>
<span
aria-label="loading"
@ -1721,6 +1718,9 @@ Array [
</svg>
</span>
</span>
<span>
Loading
</span>
</button>
</div>
</div>,

View File

@ -222,6 +222,7 @@ const InternalCompoundedButton = React.forwardRef<
[`${prefixCls}-block`]: block,
[`${prefixCls}-dangerous`]: danger,
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-icon-end`]: iconPosition === 'end',
},
compactItemClassnames,
className,
@ -231,11 +232,7 @@ const InternalCompoundedButton = React.forwardRef<
const fullStyle: React.CSSProperties = { ...button?.style, ...customStyle };
const isIconPositionEnd = iconPosition === 'end' && children && children !== 0 && iconType;
const iconClasses = classNames(customClassNames?.icon, button?.classNames?.icon, {
[`${prefixCls}-icon-end`]: isIconPositionEnd,
});
const iconClasses = classNames(customClassNames?.icon, button?.classNames?.icon);
const iconStyle: React.CSSProperties = {
...(styles?.icon || {}),
...(button?.styles?.icon || {}),
@ -247,30 +244,12 @@ const InternalCompoundedButton = React.forwardRef<
{icon}
</IconWrapper>
) : (
<LoadingIcon
existIcon={!!icon}
prefixCls={prefixCls}
loading={innerLoading}
iconPosition={iconPosition}
/>
<LoadingIcon existIcon={!!icon} prefixCls={prefixCls} loading={innerLoading} />
);
const kids =
children || children === 0 ? spaceChildren(children, needInserted && mergedInsertSpace) : null;
const genButtonContent = (iconComponent: React.ReactNode, kidsComponent: React.ReactNode) =>
iconPosition === 'start' ? (
<>
{iconComponent}
{kidsComponent}
</>
) : (
<>
{kidsComponent}
{iconComponent}
</>
);
if (linkButtonRestProps.href !== undefined) {
return wrapCSSVar(
<a
@ -284,7 +263,8 @@ const InternalCompoundedButton = React.forwardRef<
ref={buttonRef as React.Ref<HTMLAnchorElement>}
tabIndex={mergedDisabled ? -1 : 0}
>
{genButtonContent(iconNode, kids)}
{iconNode}
{kids}
</a>,
);
}
@ -299,8 +279,8 @@ const InternalCompoundedButton = React.forwardRef<
disabled={mergedDisabled}
ref={buttonRef as React.Ref<HTMLButtonElement>}
>
{genButtonContent(iconNode, kids)}
{iconNode}
{kids}
{/* Styles: compact */}
{!!compactItemClassnames && <CompactCmp key="compact" prefixCls={prefixCls} />}
</button>

View File

@ -17,7 +17,10 @@ const genSharedButtonStyle: GenerateStyle<ButtonToken, CSSObject> = (token): CSS
[componentCls]: {
outline: 'none',
position: 'relative',
display: 'inline-block',
display: 'inline-flex',
gap: token.marginXS,
alignItems: 'center',
justifyContent: 'center',
fontWeight,
whiteSpace: 'nowrap',
textAlign: 'center',
@ -39,25 +42,7 @@ const genSharedButtonStyle: GenerateStyle<ButtonToken, CSSObject> = (token): CSS
},
[`${componentCls}-icon`]: {
lineHeight: 0,
// iconPosition in end
[`&-end`]: {
marginInlineStart: token.marginXS,
},
},
// Leave a space between icon and text.
[`> ${iconCls} + span, > span + ${iconCls}`]: {
marginInlineStart: token.marginXS,
},
[`&:not(${componentCls}-icon-only) > ${componentCls}-icon`]: {
[`&${componentCls}-loading-icon, &:not(:last-child)`]: {
marginInlineEnd: token.marginXS,
},
[`&${componentCls}-loading-icon-end`]: {
marginInlineStart: token.marginXS,
},
lineHeight: 1,
},
'> a': {
@ -77,9 +62,9 @@ const genSharedButtonStyle: GenerateStyle<ButtonToken, CSSObject> = (token): CSS
letterSpacing: '0.34em',
},
// make `btn-icon-only` not too narrow
[`&-icon-only${componentCls}-compact-item`]: {
flex: 'none',
// iconPosition="end"
'&-icon-end': {
flexDirection: 'row-reverse',
},
},
};
@ -425,15 +410,18 @@ const genButtonStyle = (token: ButtonToken, prefixCls: string = ''): CSSInterpol
borderRadius,
[`&${iconOnlyCls}`]: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: controlHeight,
paddingInlineStart: 0,
paddingInlineEnd: 0,
paddingInline: 0,
// make `btn-icon-only` not too narrow
[`&${componentCls}-compact-item`]: {
flex: 'none',
},
[`&${componentCls}-round`]: {
width: 'auto',
},
[iconCls]: {
fontSize: token.buttonIconOnlyFontSize,
},

View File

@ -140,7 +140,7 @@ const Card = React.forwardRef<HTMLDivElement, CardProps>((props, ref) => {
const moduleClass = (moduleName: CardClassNamesModule) =>
classNames(card?.classNames?.[moduleName], customClassNames?.[moduleName]);
const moduleStyle = (moduleName: CardStylesModule) => ({
const moduleStyle = (moduleName: CardStylesModule): React.CSSProperties => ({
...card?.styles?.[moduleName],
...customStyles?.[moduleName],
});

View File

@ -21792,196 +21792,196 @@ exports[`ConfigProvider components Popover prefixCls 1`] = `
exports[`ConfigProvider components Progress configProvider 1`] = `
<div
aria-valuenow="0"
class="config-progress config-progress-status-normal config-progress-line config-progress-show-info config-progress-default"
class="config-progress config-progress-status-normal config-progress-line config-progress-line-align-end config-progress-line-position-outer config-progress-show-info config-progress-default"
role="progressbar"
>
<div
class="config-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="config-progress-inner"
>
<div
class="config-progress-bg"
class="config-progress-bg config-progress-bg-outer"
style="width: 0%; height: 8px; --progress-percent: 0;"
/>
</div>
<span
class="config-progress-text config-progress-text-end config-progress-text-outer"
title="0%"
>
0%
</span>
</div>
<span
class="config-progress-text"
title="0%"
>
0%
</span>
</div>
`;
exports[`ConfigProvider components Progress configProvider componentDisabled 1`] = `
<div
aria-valuenow="0"
class="config-progress config-progress-status-normal config-progress-line config-progress-show-info config-progress-default"
class="config-progress config-progress-status-normal config-progress-line config-progress-line-align-end config-progress-line-position-outer config-progress-show-info config-progress-default"
role="progressbar"
>
<div
class="config-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="config-progress-inner"
>
<div
class="config-progress-bg"
class="config-progress-bg config-progress-bg-outer"
style="width: 0%; height: 8px; --progress-percent: 0;"
/>
</div>
<span
class="config-progress-text config-progress-text-end config-progress-text-outer"
title="0%"
>
0%
</span>
</div>
<span
class="config-progress-text"
title="0%"
>
0%
</span>
</div>
`;
exports[`ConfigProvider components Progress configProvider componentSize large 1`] = `
<div
aria-valuenow="0"
class="config-progress config-progress-status-normal config-progress-line config-progress-show-info config-progress-default"
class="config-progress config-progress-status-normal config-progress-line config-progress-line-align-end config-progress-line-position-outer config-progress-show-info config-progress-default"
role="progressbar"
>
<div
class="config-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="config-progress-inner"
>
<div
class="config-progress-bg"
class="config-progress-bg config-progress-bg-outer"
style="width: 0%; height: 8px; --progress-percent: 0;"
/>
</div>
<span
class="config-progress-text config-progress-text-end config-progress-text-outer"
title="0%"
>
0%
</span>
</div>
<span
class="config-progress-text"
title="0%"
>
0%
</span>
</div>
`;
exports[`ConfigProvider components Progress configProvider componentSize middle 1`] = `
<div
aria-valuenow="0"
class="config-progress config-progress-status-normal config-progress-line config-progress-show-info config-progress-default"
class="config-progress config-progress-status-normal config-progress-line config-progress-line-align-end config-progress-line-position-outer config-progress-show-info config-progress-default"
role="progressbar"
>
<div
class="config-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="config-progress-inner"
>
<div
class="config-progress-bg"
class="config-progress-bg config-progress-bg-outer"
style="width: 0%; height: 8px; --progress-percent: 0;"
/>
</div>
<span
class="config-progress-text config-progress-text-end config-progress-text-outer"
title="0%"
>
0%
</span>
</div>
<span
class="config-progress-text"
title="0%"
>
0%
</span>
</div>
`;
exports[`ConfigProvider components Progress configProvider componentSize small 1`] = `
<div
aria-valuenow="0"
class="config-progress config-progress-status-normal config-progress-line config-progress-show-info config-progress-default"
class="config-progress config-progress-status-normal config-progress-line config-progress-line-align-end config-progress-line-position-outer config-progress-show-info config-progress-default"
role="progressbar"
>
<div
class="config-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="config-progress-inner"
>
<div
class="config-progress-bg"
class="config-progress-bg config-progress-bg-outer"
style="width: 0%; height: 8px; --progress-percent: 0;"
/>
</div>
<span
class="config-progress-text config-progress-text-end config-progress-text-outer"
title="0%"
>
0%
</span>
</div>
<span
class="config-progress-text"
title="0%"
>
0%
</span>
</div>
`;
exports[`ConfigProvider components Progress normal 1`] = `
<div
aria-valuenow="0"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-show-info ant-progress-default"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-line-align-end ant-progress-line-position-outer ant-progress-show-info ant-progress-default"
role="progressbar"
>
<div
class="ant-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="ant-progress-inner"
>
<div
class="ant-progress-bg"
class="ant-progress-bg ant-progress-bg-outer"
style="width: 0%; height: 8px; --progress-percent: 0;"
/>
</div>
<span
class="ant-progress-text ant-progress-text-end ant-progress-text-outer"
title="0%"
>
0%
</span>
</div>
<span
class="ant-progress-text"
title="0%"
>
0%
</span>
</div>
`;
exports[`ConfigProvider components Progress prefixCls 1`] = `
<div
aria-valuenow="0"
class="prefix-Progress prefix-Progress-status-normal prefix-Progress-line prefix-Progress-show-info prefix-Progress-default"
class="prefix-Progress prefix-Progress-status-normal prefix-Progress-line prefix-Progress-line-align-end prefix-Progress-line-position-outer prefix-Progress-show-info prefix-Progress-default"
role="progressbar"
>
<div
class="prefix-Progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="prefix-Progress-inner"
>
<div
class="prefix-Progress-bg"
class="prefix-Progress-bg prefix-Progress-bg-outer"
style="width: 0%; height: 8px; --progress-percent: 0;"
/>
</div>
<span
class="prefix-Progress-text prefix-Progress-text-end prefix-Progress-text-outer"
title="0%"
>
0%
</span>
</div>
<span
class="prefix-Progress-text"
title="0%"
>
0%
</span>
</div>
`;
@ -25791,20 +25791,24 @@ exports[`ConfigProvider components Spin configProvider 1`] = `
class="config-spin config-spin-spinning"
>
<span
class="config-spin-dot config-spin-dot-spin"
class="config-spin-dot-holder"
>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<span
class="config-spin-dot config-spin-dot-spin"
>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
</span>
</span>
</div>
`;
@ -25816,20 +25820,24 @@ exports[`ConfigProvider components Spin configProvider componentDisabled 1`] = `
class="config-spin config-spin-spinning"
>
<span
class="config-spin-dot config-spin-dot-spin"
class="config-spin-dot-holder"
>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<span
class="config-spin-dot config-spin-dot-spin"
>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
</span>
</span>
</div>
`;
@ -25841,20 +25849,24 @@ exports[`ConfigProvider components Spin configProvider componentSize large 1`] =
class="config-spin config-spin-spinning"
>
<span
class="config-spin-dot config-spin-dot-spin"
class="config-spin-dot-holder"
>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<span
class="config-spin-dot config-spin-dot-spin"
>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
</span>
</span>
</div>
`;
@ -25866,20 +25878,24 @@ exports[`ConfigProvider components Spin configProvider componentSize middle 1`]
class="config-spin config-spin-spinning"
>
<span
class="config-spin-dot config-spin-dot-spin"
class="config-spin-dot-holder"
>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<span
class="config-spin-dot config-spin-dot-spin"
>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
</span>
</span>
</div>
`;
@ -25891,20 +25907,24 @@ exports[`ConfigProvider components Spin configProvider componentSize small 1`] =
class="config-spin config-spin-spinning"
>
<span
class="config-spin-dot config-spin-dot-spin"
class="config-spin-dot-holder"
>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<span
class="config-spin-dot config-spin-dot-spin"
>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
<i
class="config-spin-dot-item"
/>
</span>
</span>
</div>
`;
@ -25916,20 +25936,24 @@ exports[`ConfigProvider components Spin normal 1`] = `
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
`;
@ -25941,20 +25965,24 @@ exports[`ConfigProvider components Spin prefixCls 1`] = `
class="prefix-Spin prefix-Spin-spinning"
>
<span
class="prefix-Spin-dot prefix-Spin-dot-spin"
class="prefix-Spin-dot-holder"
>
<i
class="prefix-Spin-dot-item"
/>
<i
class="prefix-Spin-dot-item"
/>
<i
class="prefix-Spin-dot-item"
/>
<i
class="prefix-Spin-dot-item"
/>
<span
class="prefix-Spin-dot prefix-Spin-dot-spin"
>
<i
class="prefix-Spin-dot-item"
/>
<i
class="prefix-Spin-dot-item"
/>
<i
class="prefix-Spin-dot-item"
/>
<i
class="prefix-Spin-dot-item"
/>
</span>
</span>
</div>
`;

View File

@ -12,6 +12,7 @@ import type { FlexProps } from '../flex/interface';
import type { FloatButtonGroupProps } from '../float-button/interface';
import type { FormProps } from '../form/Form';
import type { InputProps, TextAreaProps } from '../input';
import type { ListItemProps } from '../list';
import type { Locale } from '../locale';
import type { MenuProps } from '../menu';
import type { ModalProps } from '../modal';
@ -169,6 +170,10 @@ export type SpaceConfig = ComponentStyleConfig & Pick<SpaceProps, 'size' | 'clas
export type PopupOverflow = 'viewport' | 'scroll';
export interface ListConfig extends ComponentStyleConfig {
item?: Pick<ListItemProps, 'classNames' | 'styles'>;
}
export interface WaveConfig {
/**
* @descCN `false`
@ -227,7 +232,7 @@ export interface ConfigConsumerProps {
statistic?: ComponentStyleConfig;
image?: ImageConfig;
layout?: ComponentStyleConfig;
list?: ComponentStyleConfig;
list?: ListConfig;
mentions?: ComponentStyleConfig;
modal?: ModalConfig;
progress?: ComponentStyleConfig;

View File

@ -126,7 +126,7 @@ const {
| input | Set Input common props | { autoComplete?: string, className?: string, style?: React.CSSProperties, allowClear?: boolean \| { clearIcon?: ReactNode } } | - | 4.2.0, allowClear: 5.15.0 |
| textArea | Set TextArea common props | { autoComplete?: string, className?: string, style?: React.CSSProperties, allowClear?: boolean \| { clearIcon?: ReactNode } } | - | 5.15.0 |
| layout | Set Layout common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| list | Set List common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| list | Set List common props | { className?: string, style?: React.CSSProperties, item?:{ classNames: [ListItemProps\["classNames"\]](/components/list-cn#listitem), styles: [ListItemProps\["styles"\]](/components/list-cn#listitem) } } | - | 5.7.0 |
| menu | Set Menu common props | { className?: string, style?: React.CSSProperties, expandIcon?: ReactNode \| props => ReactNode } | - | 5.7.0, expandIcon: 5.15.0 |
| mentions | Set Mentions common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| message | Set Message common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |

View File

@ -30,6 +30,7 @@ import type {
FormConfig,
ImageConfig,
InputConfig,
ListConfig,
MenuConfig,
ModalConfig,
NotificationConfig,
@ -172,7 +173,7 @@ export interface ConfigProviderProps {
steps?: ComponentStyleConfig;
image?: ImageConfig;
layout?: ComponentStyleConfig;
list?: ComponentStyleConfig;
list?: ListConfig;
mentions?: ComponentStyleConfig;
modal?: ModalConfig;
progress?: ComponentStyleConfig;

View File

@ -128,7 +128,7 @@ const {
| input | 设置 Input 组件的通用属性 | { autoComplete?: string, className?: string, style?: React.CSSProperties, allowClear?: boolean \| { clearIcon?: ReactNode } } | - | 5.7.0, allowClear: 5.15.0 |
| textArea | 设置 TextArea 组件的通用属性 | { autoComplete?: string, className?: string, style?: React.CSSProperties, allowClear?: boolean \| { clearIcon?: ReactNode } } | - | 5.15.0 |
| layout | 设置 Layout 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| list | 设置 List 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| list | 设置 List 组件的通用属性 | { className?: string, style?: React.CSSProperties, item?:{ classNames: [ListItemProps\["classNames"\]](/components/list-cn#listitem), styles: [ListItemProps\["styles"\]](/components/list-cn#listitem) } } | - | 5.7.0 |
| menu | 设置 Menu 组件的通用属性 | { className?: string, style?: React.CSSProperties, expandIcon?: ReactNode \| props => ReactNode } | - | 5.7.0, expandIcon: 5.15.0 |
| mentions | 设置 Mentions 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| message | 设置 Message 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |

View File

@ -5,8 +5,7 @@ import type { DrawerProps as RCDrawerProps } from 'rc-drawer';
import useClosable, { pickClosable } from '../_util/hooks/useClosable';
import type { ClosableType } from '../_util/hooks/useClosable';
import { ConfigContext } from '../config-provider';
import Spin from '../spin';
import type { SpinProps } from '../spin';
import Skeleton from '../skeleton';
export interface DrawerClassNames extends NonNullable<RCDrawerProps['classNames']> {
header?: string;
@ -40,7 +39,7 @@ export interface DrawerPanelProps {
children?: React.ReactNode;
classNames?: DrawerClassNames;
styles?: DrawerStyles;
loading?: boolean | Omit<SpinProps, 'fullscreen' | 'tip'>;
loading?: boolean;
/** @deprecated Please use `styles.header` instead */
headerStyle?: React.CSSProperties;
@ -91,19 +90,6 @@ const DrawerPanel: React.FC<DrawerPanelProps> = (props) => {
},
);
// >>>>>>>>> Spinning
let spinProps: SpinProps | undefined;
if (typeof loading === 'boolean') {
spinProps = {
spinning: loading,
};
} else if (typeof loading === 'object') {
spinProps = {
spinning: true,
...loading,
};
}
const headerNode = React.useMemo<React.ReactNode>(() => {
if (!title && !mergedClosable) {
return null;
@ -156,15 +142,6 @@ const DrawerPanel: React.FC<DrawerPanelProps> = (props) => {
);
}, [footer, footerStyle, prefixCls]);
if (spinProps?.spinning) {
return (
<Spin
{...spinProps}
className={classNames(spinProps.className, `${prefixCls}-content-spin`)}
/>
);
}
return (
<>
{headerNode}
@ -174,13 +151,18 @@ const DrawerPanel: React.FC<DrawerPanelProps> = (props) => {
drawerClassNames?.body,
drawerContext?.classNames?.body,
)}
style={{
...drawerContext?.styles?.body,
...bodyStyle,
...drawerStyles?.body,
}}
style={{ ...drawerContext?.styles?.body, ...bodyStyle, ...drawerStyles?.body }}
>
{children}
{loading ? (
<Skeleton
active
title={false}
paragraph={{ rows: 5 }}
className={`${prefixCls}-body-skeleton`}
/>
) : (
children
)}
</div>
{footerNode}
</>

View File

@ -190,20 +190,14 @@ describe('Drawer', () => {
expect(wrapper.firstChild).toMatchSnapshot();
});
it('have a custom loading', () => {
const loadingContent = 'Custom Loading...';
const { container: wrapper } = render(
<Drawer
open
loading={{ indicator: <span>{loadingContent}</span>, spinning: true }}
getContainer={false}
>
const { container } = render(
<Drawer open loading getContainer={false}>
Here is content of Drawer
</Drawer>,
);
triggerMotion();
const [loadingWrapper] = wrapper.getElementsByClassName('ant-spin-dot');
expect(loadingWrapper).toHaveTextContent(loadingContent);
const wrapper = container.querySelector<HTMLDivElement>('.ant-skeleton');
expect(wrapper).toBeTruthy();
});
});
@ -406,4 +400,15 @@ describe('Drawer', () => {
expect(baseElement.querySelector('.custom-close')).not.toBeNull();
expect(baseElement.querySelector('*[aria-label="Close"]')).not.toBeNull();
});
it('drawerRender', () => {
const { container } = render(
<Drawer open getContainer={false} drawerRender={(dom) => <div id="test">{dom}</div>}>
Here is content of Drawer
</Drawer>,
);
triggerMotion();
expect(container.querySelector('#test')).toBeTruthy();
});
});

View File

@ -24,26 +24,61 @@ exports[`Drawer Drawer loading have a spinner 1`] = `
role="dialog"
>
<div
aria-busy="true"
aria-live="polite"
class="ant-spin ant-spin-spinning ant-drawer-content-spin"
class="ant-drawer-header ant-drawer-header-close-only"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
<div
class="ant-drawer-header-title"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
<button
aria-label="Close"
class="ant-drawer-close"
type="button"
>
<span
aria-label="close"
class="anticon anticon-close"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
</button>
</div>
</div>
<div
class="ant-drawer-body"
>
<div
class="ant-skeleton ant-skeleton-active ant-drawer-body-skeleton"
>
<div
class="ant-skeleton-content"
>
<ul
class="ant-skeleton-paragraph"
>
<li />
<li />
<li />
<li />
<li
style="width: 61%;"
/>
</ul>
</div>
</div>
</div>
</div>
</div>

View File

@ -2814,26 +2814,68 @@ Array [
role="dialog"
>
<div
aria-busy="true"
aria-live="polite"
class="ant-spin ant-spin-spinning ant-drawer-content-spin"
class="ant-drawer-header"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
<div
class="ant-drawer-header-title"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
<button
aria-label="Close"
class="ant-drawer-close"
type="button"
>
<span
aria-label="close"
class="anticon anticon-close"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
</button>
<div
class="ant-drawer-title"
>
<p>
Loading Drawer
</p>
</div>
</div>
</div>
<div
class="ant-drawer-body"
>
<div
class="ant-skeleton ant-skeleton-active ant-drawer-body-skeleton"
>
<div
class="ant-skeleton-content"
>
<ul
class="ant-skeleton-paragraph"
>
<li />
<li />
<li />
<li />
<li
style="width: 61%;"
/>
</ul>
</div>
</div>
</div>
</div>
</div>

View File

@ -17,6 +17,10 @@ A Drawer is a panel that is typically overlaid on top of a page and slides in fr
- Processing subtasks. When subtasks are too heavy for a Popover and we still want to keep the subtasks in the context of the main task, Drawer comes very handy.
- When the same Form is needed in multiple places.
> Notes for developers
>
> Since the `5.17.0`, we provided the `loading` prop by the Spin. However, since the `5.18.0` version, we have fixed this design error and replaced the Spin with the Skeleton, and also modified the type of `loading` prop, which can only accept `boolean` type.
## Examples
<!-- prettier-ignore -->
@ -69,11 +73,12 @@ v5 use `rootClassName` & `rootStyle` to config wrapper style instead of `classNa
| styles | Semantic structure style | [Record<SemanticDOM, CSSProperties>](#semantic-dom) | - | 5.10.0 |
| size | preset size of drawer, default `378px` and large `736px` | 'default' \| 'large' | 'default' | 4.17.0 |
| title | The title for Drawer | ReactNode | - | |
| loading | Show spinning indicator | boolean | false | 5.17.0 |
| loading | Show the Skeleton | boolean | false | 5.17.0 |
| open | Whether the Drawer dialog is visible or not | boolean | false | |
| width | Width of the Drawer dialog | string \| number | 378 | |
| zIndex | The `z-index` of the Drawer | number | 1000 | |
| onClose | Specify a callback that will be called when a user clicks mask, close button or Cancel button | function(e) | - | |
| drawerRender | Custom drawer content render | (node: ReactNode) => ReactNode | - | 5.19.0 |
## Semantic DOM

View File

@ -17,6 +17,10 @@ demo:
- 当需要一个附加的面板来控制父窗体内容,这个面板在需要时呼出。比如,控制界面展示样式,往界面中添加内容。
- 当需要在当前任务流中插入临时任务,创建或预览附加内容。比如展示协议条款,创建子对象。
> 开发者注意事项:
>
> 自 `5.17.0` 版本,我们提供了 `loading` 属性,内置 Spin 组件作为加载状态,但是自 `5.18.0` 版本开始,我们修复了设计失误,将内置的 Spin 组件替换成了 Skeleton 组件,同时收窄了 `loading` api 的类型范围,只能接收 boolean 类型。
## 代码演示
<!-- prettier-ignore -->
@ -68,11 +72,12 @@ v5 使用 `rootClassName` 与 `rootStyle` 来配置最外层元素样式。原 v
| style | 设计 Drawer 容器样式,如果你只需要设置内容部分请使用 `bodyStyle` | CSSProperties | - | |
| styles | 语义化结构 style | [Record<SemanticDOM, CSSProperties>](#semantic-dom) | - | 5.10.0 |
| title | 标题 | ReactNode | - | |
| loading | 显示旋转指示器 | boolean | false | 5.17.0 |
| loading | 显示骨架屏 | boolean | false | 5.17.0 |
| open | Drawer 是否可见 | boolean | - |
| width | 宽度 | string \| number | 378 | |
| zIndex | 设置 Drawer 的 `z-index` | number | 1000 | |
| onClose | 点击遮罩层或左上角叉或取消按钮的回调 | function(e) | - | |
| drawerRender | 自定义渲染抽屉 | (node: ReactNode) => ReactNode | - | 5.19.0 |
## Semantic DOM

View File

@ -147,13 +147,6 @@ const genDrawerStyle: GenerateStyle<DrawerToken> = (token) => {
overflow: 'auto',
background: colorBgElevated,
pointerEvents: 'auto',
[`${componentCls}-content-spin`]: {
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
},
// Header
@ -230,6 +223,12 @@ const genDrawerStyle: GenerateStyle<DrawerToken> = (token) => {
minHeight: 0,
padding: paddingLG,
overflow: 'auto',
[`${componentCls}-body-skeleton`]: {
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
},
},
// Footer

View File

@ -3,7 +3,7 @@ import { useMemo } from 'react';
import classNames from 'classnames';
import FieldForm, { List, useWatch } from 'rc-field-form';
import type { FormProps as RcFormProps } from 'rc-field-form/lib/Form';
import type { InternalNamePath, ValidateErrorEntity } from 'rc-field-form/lib/interface';
import type { FormRef, InternalNamePath, ValidateErrorEntity } from 'rc-field-form/lib/interface';
import type { Options } from 'scroll-into-view-if-needed';
import { ConfigContext } from '../config-provider';
@ -29,6 +29,7 @@ export type RequiredMark =
| 'optional'
| ((labelNode: React.ReactNode, info: { required: boolean }) => React.ReactNode);
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
export type FormItemLayout = 'horizontal' | 'vertical';
export interface FormProps<Values = any> extends Omit<RcFormProps<Values>, 'form'> {
prefixCls?: string;
@ -51,7 +52,7 @@ export interface FormProps<Values = any> extends Omit<RcFormProps<Values>, 'form
variant?: Variant;
}
const InternalForm: React.ForwardRefRenderFunction<FormInstance, FormProps> = (props, ref) => {
const InternalForm: React.ForwardRefRenderFunction<FormRef, FormProps> = (props, ref) => {
const contextDisabled = React.useContext(DisabledContext);
const { getPrefixCls, direction, form: contextForm } = React.useContext(ConfigContext);
@ -159,7 +160,11 @@ const InternalForm: React.ForwardRefRenderFunction<FormInstance, FormProps> = (p
],
);
React.useImperativeHandle(ref, () => wrapForm);
const nativeElementRef = React.useRef<FormRef>(null);
React.useImperativeHandle(ref, () => ({
...wrapForm,
nativeElement: nativeElementRef.current?.nativeElement,
}));
const scrollToField = (options: boolean | Options, fieldName: InternalNamePath) => {
if (options) {
@ -203,6 +208,7 @@ const InternalForm: React.ForwardRefRenderFunction<FormInstance, FormProps> = (p
name={name}
onFinishFailed={onInternalFinishFailed}
form={wrapForm}
ref={nativeElementRef}
style={{ ...contextForm?.style, ...style }}
className={formClassName}
/>
@ -214,8 +220,8 @@ const InternalForm: React.ForwardRefRenderFunction<FormInstance, FormProps> = (p
);
};
const Form = React.forwardRef<FormInstance, FormProps>(InternalForm) as (<Values = any>(
props: React.PropsWithChildren<FormProps<Values>> & React.RefAttributes<FormInstance<Values>>,
const Form = React.forwardRef<FormRef, FormProps>(InternalForm) as (<Values = any>(
props: React.PropsWithChildren<FormProps<Values>> & React.RefAttributes<FormRef<Values>>,
) => React.ReactElement) &
Pick<React.FC, 'displayName'>;

View File

@ -47,11 +47,13 @@ export default function ItemHolder(props: ItemHolderProps) {
required,
isRequired,
onSubItemMetaChange,
layout,
...restProps
} = props;
const itemPrefixCls = `${prefixCls}-item`;
const { requiredMark } = React.useContext(FormContext);
const { requiredMark, vertical: formVertical } = React.useContext(FormContext);
const vertical = formVertical || layout === 'vertical';
// ======================== Margin ========================
const itemRef = React.useRef<HTMLDivElement>(null);
@ -99,6 +101,9 @@ export default function ItemHolder(props: ItemHolderProps) {
[`${itemPrefixCls}-has-error`]: mergedValidateStatus === 'error',
[`${itemPrefixCls}-is-validating`]: mergedValidateStatus === 'validating',
[`${itemPrefixCls}-hidden`]: hidden,
// Layout
[`${itemPrefixCls}-${layout}`]: layout,
});
return (
@ -145,6 +150,7 @@ export default function ItemHolder(props: ItemHolderProps) {
requiredMark={requiredMark}
required={required ?? isRequired}
prefixCls={prefixCls}
vertical={vertical}
/>
{/* Input Group */}
<FormItemInput

View File

@ -11,7 +11,7 @@ import { devUseWarning } from '../../_util/warning';
import { ConfigContext } from '../../config-provider';
import useCSSVarCls from '../../config-provider/hooks/useCSSVarCls';
import { FormContext, NoStyleItemContext } from '../context';
import type { FormInstance } from '../Form';
import type { FormInstance, FormItemLayout } from '../Form';
import type { FormItemInputProps } from '../FormItemInput';
import type { FormItemLabelProps, LabelTooltipType } from '../FormItemLabel';
import useChildren from '../hooks/useChildren';
@ -102,6 +102,7 @@ export interface FormItemProps<Values = any>
tooltip?: LabelTooltipType;
/** @deprecated No need anymore */
fieldKey?: React.Key | React.Key[];
layout?: FormItemLayout;
}
function genEmptyMeta(): Meta {
@ -132,6 +133,7 @@ function InternalFormItem<Values = any>(props: FormItemProps<Values>): React.Rea
validateTrigger,
hidden,
help,
layout,
} = props;
const { getPrefixCls } = React.useContext(ConfigContext);
const { name: formName } = React.useContext(FormContext);
@ -273,6 +275,7 @@ function InternalFormItem<Values = any>(props: FormItemProps<Values>): React.Rea
warnings={mergedWarnings}
meta={meta}
onSubItemMetaChange={onSubItemMetaChange}
layout={layout}
>
{baseChildren}
</ItemHolder>

View File

@ -44,6 +44,7 @@ export interface FormItemLabelProps {
*/
requiredMark?: RequiredMark;
tooltip?: LabelTooltipType;
vertical?: boolean;
}
const FormItemLabel: React.FC<FormItemLabelProps & { required?: boolean; prefixCls: string }> = ({
@ -56,11 +57,11 @@ const FormItemLabel: React.FC<FormItemLabelProps & { required?: boolean; prefixC
required,
requiredMark,
tooltip,
vertical,
}) => {
const [formLocale] = useLocale('Form');
const {
vertical,
labelAlign: contextLabelAlign,
labelCol: contextLabelCol,
labelWrap,

View File

@ -8790,6 +8790,176 @@ exports[`renders components/form/demo/layout-can-wrap.tsx extend context correct
exports[`renders components/form/demo/layout-can-wrap.tsx extend context correctly 2`] = `[]`;
exports[`renders components/form/demo/layout-multiple.tsx extend context correctly 1`] = `
Array [
<form
class="ant-form ant-form-horizontal"
id="layout-multiple-horizontal"
>
<div
class="ant-form-item"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-4 ant-form-item-label"
>
<label
class="ant-form-item-required"
for="layout-multiple-horizontal_horizontal"
title="horizontal"
>
horizontal
</label>
</div>
<div
class="ant-col ant-col-20 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
aria-required="true"
class="ant-input ant-input-outlined"
id="layout-multiple-horizontal_horizontal"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item ant-form-item-vertical"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-24 ant-form-item-label"
>
<label
class="ant-form-item-required"
for="layout-multiple-horizontal_vertical"
title="vertical"
>
vertical
</label>
</div>
<div
class="ant-col ant-col-24 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
aria-required="true"
class="ant-input ant-input-outlined"
id="layout-multiple-horizontal_vertical"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
</form>,
<br />,
<form
class="ant-form ant-form-vertical"
id="layout-multiple-vertical"
>
<div
class="ant-form-item"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-4 ant-form-item-label"
>
<label
class="ant-form-item-required"
for="layout-multiple-vertical_vertical"
title="vertical"
>
vertical
</label>
</div>
<div
class="ant-col ant-col-20 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
aria-required="true"
class="ant-input ant-input-outlined"
id="layout-multiple-vertical_vertical"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item ant-form-item-horizontal"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-4 ant-form-item-label"
>
<label
class="ant-form-item-required"
for="layout-multiple-vertical_horizontal"
title="horizontal"
>
horizontal
</label>
</div>
<div
class="ant-col ant-col-20 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
aria-required="true"
class="ant-input ant-input-outlined"
id="layout-multiple-vertical_horizontal"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
</form>,
]
`;
exports[`renders components/form/demo/layout-multiple.tsx extend context correctly 2`] = `[]`;
exports[`renders components/form/demo/nest-messages.tsx extend context correctly 1`] = `
<form
class="ant-form ant-form-horizontal"

View File

@ -5138,6 +5138,174 @@ exports[`renders components/form/demo/layout-can-wrap.tsx correctly 1`] = `
</form>
`;
exports[`renders components/form/demo/layout-multiple.tsx correctly 1`] = `
Array [
<form
class="ant-form ant-form-horizontal"
id="layout-multiple-horizontal"
>
<div
class="ant-form-item"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-4 ant-form-item-label"
>
<label
class="ant-form-item-required"
for="layout-multiple-horizontal_horizontal"
title="horizontal"
>
horizontal
</label>
</div>
<div
class="ant-col ant-col-20 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
aria-required="true"
class="ant-input ant-input-outlined"
id="layout-multiple-horizontal_horizontal"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item ant-form-item-vertical"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-24 ant-form-item-label"
>
<label
class="ant-form-item-required"
for="layout-multiple-horizontal_vertical"
title="vertical"
>
vertical
</label>
</div>
<div
class="ant-col ant-col-24 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
aria-required="true"
class="ant-input ant-input-outlined"
id="layout-multiple-horizontal_vertical"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
</form>,
<br />,
<form
class="ant-form ant-form-vertical"
id="layout-multiple-vertical"
>
<div
class="ant-form-item"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-4 ant-form-item-label"
>
<label
class="ant-form-item-required"
for="layout-multiple-vertical_vertical"
title="vertical"
>
vertical
</label>
</div>
<div
class="ant-col ant-col-20 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
aria-required="true"
class="ant-input ant-input-outlined"
id="layout-multiple-vertical_vertical"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item ant-form-item-horizontal"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-4 ant-form-item-label"
>
<label
class="ant-form-item-required"
for="layout-multiple-vertical_horizontal"
title="horizontal"
>
horizontal
</label>
</div>
<div
class="ant-col ant-col-20 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
aria-required="true"
class="ant-input ant-input-outlined"
id="layout-multiple-vertical_horizontal"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
</form>,
]
`;
exports[`renders components/form/demo/nest-messages.tsx correctly 1`] = `
<form
class="ant-form ant-form-horizontal"

View File

@ -1085,6 +1085,124 @@ exports[`Form form should support disabled 1`] = `
</form>
`;
exports[`Form form.item should support layout 1`] = `
<form
class="ant-form ant-form-horizontal"
>
<div
class="ant-form-item"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-4 ant-form-item-label"
>
<label
class=""
for="name"
title="name"
>
name
</label>
</div>
<div
class="ant-col ant-col-14 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input ant-input-outlined"
id="name"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item ant-form-item-horizontal"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-4 ant-form-item-label"
>
<label
class=""
for="horizontal"
title="horizontal"
>
horizontal
</label>
</div>
<div
class="ant-col ant-col-14 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input ant-input-outlined"
id="horizontal"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item ant-form-item-vertical"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-4 ant-form-item-label"
>
<label
class=""
for="vertical"
title="vertical"
>
vertical
</label>
</div>
<div
class="ant-col ant-col-14 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input ant-input-outlined"
id="vertical"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
</form>
`;
exports[`Form rtl render component should be rendered correctly in RTL direction 1`] = `
<form
class="ant-form ant-form-horizontal ant-form-rtl"

View File

@ -1320,6 +1320,24 @@ describe('Form', () => {
expect(container.firstChild).toMatchSnapshot();
});
it('form.item should support layout', () => {
const App: React.FC = () => (
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 14 }} layout="horizontal">
<Form.Item label="name" name="name">
<Input />
</Form.Item>
<Form.Item label="horizontal" name="horizontal" layout="horizontal">
<Input />
</Form.Item>
<Form.Item label="vertical" name="vertical" layout="vertical">
<Input />
</Form.Item>
</Form>
);
const { container } = render(<App />);
expect(container.firstChild).toMatchSnapshot();
});
it('_internalItemRender api test', () => {
const { container } = render(
<Form>

View File

@ -1,4 +1,5 @@
import React from 'react';
import type { FormRef } from 'rc-field-form/lib/interface';
import Form from '..';
import { fireEvent, render } from '../../../tests/utils';
@ -86,4 +87,11 @@ describe('Form.Ref', () => {
fireEvent.click(container.querySelector('.ref-remove')!);
expect(onRef).toHaveBeenCalledWith(undefined, null);
});
it('should have nativeForm', () => {
const formRef = React.createRef<FormRef>();
const { container } = render(<Form ref={formRef} />);
expect(container.querySelector('.ant-form')).toBe(formRef.current?.nativeElement);
});
});

View File

@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react';
import type { FormInstance } from 'antd';
import React, { useState } from 'react';
import { Button, Form, Input, Modal, Radio } from 'antd';
interface Values {
@ -8,85 +7,8 @@ interface Values {
modifier?: string;
}
interface CollectionCreateFormProps {
initialValues: Values;
onFormInstanceReady: (instance: FormInstance<Values>) => void;
}
const CollectionCreateForm: React.FC<CollectionCreateFormProps> = ({
initialValues,
onFormInstanceReady,
}) => {
const [form] = Form.useForm();
useEffect(() => {
onFormInstanceReady(form);
}, []);
return (
<Form layout="vertical" form={form} name="form_in_modal" initialValues={initialValues}>
<Form.Item
name="title"
label="Title"
rules={[{ required: true, message: 'Please input the title of collection!' }]}
>
<Input />
</Form.Item>
<Form.Item name="description" label="Description">
<Input type="textarea" />
</Form.Item>
<Form.Item name="modifier" className="collection-create-form_last-form-item">
<Radio.Group>
<Radio value="public">Public</Radio>
<Radio value="private">Private</Radio>
</Radio.Group>
</Form.Item>
</Form>
);
};
interface CollectionCreateFormModalProps {
open: boolean;
onCreate: (values: Values) => void;
onCancel: () => void;
initialValues: Values;
}
const CollectionCreateFormModal: React.FC<CollectionCreateFormModalProps> = ({
open,
onCreate,
onCancel,
initialValues,
}) => {
const [formInstance, setFormInstance] = useState<FormInstance>();
return (
<Modal
open={open}
title="Create a new collection"
okText="Create"
cancelText="Cancel"
okButtonProps={{ autoFocus: true }}
onCancel={onCancel}
destroyOnClose
onOk={async () => {
try {
const values = await formInstance?.validateFields();
formInstance?.resetFields();
onCreate(values);
} catch (error) {
console.log('Failed:', error);
}
}}
>
<CollectionCreateForm
initialValues={initialValues}
onFormInstanceReady={(instance) => {
setFormInstance(instance);
}}
/>
</Modal>
);
};
const App: React.FC = () => {
const [form] = Form.useForm();
const [formValues, setFormValues] = useState<Values>();
const [open, setOpen] = useState(false);
@ -102,12 +24,44 @@ const App: React.FC = () => {
New Collection
</Button>
<pre>{JSON.stringify(formValues, null, 2)}</pre>
<CollectionCreateFormModal
<Modal
open={open}
onCreate={onCreate}
title="Create a new collection"
okText="Create"
cancelText="Cancel"
okButtonProps={{ autoFocus: true, htmlType: 'submit' }}
onCancel={() => setOpen(false)}
initialValues={{ modifier: 'public' }}
/>
destroyOnClose
modalRender={(dom) => (
<Form
layout="vertical"
form={form}
name="form_in_modal"
initialValues={{ modifier: 'public' }}
clearOnDestroy
onFinish={(values) => onCreate(values)}
>
{dom}
</Form>
)}
>
<Form.Item
name="title"
label="Title"
rules={[{ required: true, message: 'Please input the title of collection!' }]}
>
<Input />
</Form.Item>
<Form.Item name="description" label="Description">
<Input type="textarea" />
</Form.Item>
<Form.Item name="modifier" className="collection-create-form_last-form-item">
<Radio.Group>
<Radio value="public">Public</Radio>
<Radio value="private">Private</Radio>
</Radio.Group>
</Form.Item>
</Modal>
</>
);
};

View File

@ -0,0 +1,7 @@
## zh-CN
`Form.Item` 上单独定义 `layout`,可以做到一个表单多种布局。
## en-US
Defining a separate `layout` on `Form.Item` can achieve multiple layouts for a single form.

View File

@ -0,0 +1,48 @@
import React from 'react';
import { Form, Input } from 'antd';
const App: React.FC = () => (
<>
<Form
name="layout-multiple-horizontal"
layout="horizontal"
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
>
<Form.Item label="horizontal" name="horizontal" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item
layout="vertical"
label="vertical"
name="vertical"
rules={[{ required: true }]}
labelCol={{ span: 24 }}
wrapperCol={{ span: 24 }}
>
<Input />
</Form.Item>
</Form>
<br />
<Form
name="layout-multiple-vertical"
layout="vertical"
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
>
<Form.Item label="vertical" name="vertical" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item
layout="horizontal"
label="horizontal"
name="horizontal"
rules={[{ required: true }]}
>
<Input />
</Form.Item>
</Form>
</>
);
export default App;

View File

@ -18,6 +18,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*ylFATY6w-ygAAA
<code src="./demo/basic.tsx">Basic Usage</code>
<code src="./demo/control-hooks.tsx">Form methods</code>
<code src="./demo/layout.tsx">Form Layout</code>
<code src="./demo/layout-multiple.tsx">Form mix layout</code>
<code src="./demo/disabled.tsx">Form disabled</code>
<code src="./demo/variant.tsx" version="5.13.0">Form variants</code>
<code src="./demo/required-mark.tsx">Required style</code>
@ -89,6 +90,8 @@ Common props ref[Common props](/docs/react/common-props)
| onFinish | Trigger after submitting the form and verifying data successfully | function(values) | - | |
| onFinishFailed | Trigger after submitting the form and verifying data failed | function({ values, errorFields, outOfDate }) | - | |
| onValuesChange | Trigger when value updated | function(changedValues, allValues) | - | |
| clearOnDestroy | Clear form values when the form is uninstalled | boolean | false | 5.18.0 |
| layout | Form item layout | `horizontal` \| `vertical` | - | 5.18.0 |
### validateMessages

View File

@ -19,6 +19,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*ylFATY6w-ygAAA
<code src="./demo/basic.tsx">基本使用</code>
<code src="./demo/control-hooks.tsx">表单方法调用</code>
<code src="./demo/layout.tsx">表单布局</code>
<code src="./demo/layout-multiple.tsx">表单混合布局</code>
<code src="./demo/disabled.tsx">表单禁用</code>
<code src="./demo/variant.tsx" version="5.13.0">表单变体</code>
<code src="./demo/required-mark.tsx">必选样式</code>
@ -90,6 +91,8 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*ylFATY6w-ygAAA
| onFinish | 提交表单且数据验证成功后回调事件 | function(values) | - | |
| onFinishFailed | 提交表单且数据验证失败后回调事件 | function({ values, errorFields, outOfDate }) | - | |
| onValuesChange | 字段值更新时触发回调事件 | function(changedValues, allValues) | - | |
| clearOnDestroy | 当表单被卸载时清空表单值 | boolean | false | 5.18.0 |
| layout | 表单项布局 | `horizontal` \| `vertical` \| | - | 5.18.0 |
### validateMessages

View File

@ -171,6 +171,7 @@ const genFormItemStyle: GenerateStyle<FormToken> = (token) => {
iconCls,
componentCls,
rootPrefixCls,
antCls,
labelRequiredMarkColor,
labelColor,
labelFontSize,
@ -192,7 +193,7 @@ const genFormItemStyle: GenerateStyle<FormToken> = (token) => {
},
[`&-hidden,
&-hidden.${rootPrefixCls}-row`]: {
&-hidden${antCls}-row`]: {
// https://github.com/ant-design/ant-design/issues/26141
display: 'none',
},
@ -385,11 +386,11 @@ const genFormItemStyle: GenerateStyle<FormToken> = (token) => {
};
};
const genHorizontalStyle: GenerateStyle<FormToken> = (token) => {
const { componentCls, formItemCls } = token;
const genHorizontalStyle = (token: FormToken, className: string): CSSObject => {
const { formItemCls } = token;
return {
[`${componentCls}-horizontal`]: {
[`${className}-horizontal`]: {
[`${formItemCls}-label`]: {
flexGrow: 0,
},
@ -494,53 +495,110 @@ const makeVerticalLayout = (token: FormToken): CSSObject => {
};
const genVerticalStyle: GenerateStyle<FormToken> = (token) => {
const { componentCls, formItemCls, rootPrefixCls } = token;
const { componentCls, formItemCls, antCls } = token;
return {
[`${componentCls}-vertical`]: {
[formItemCls]: {
'&-row': {
[`${formItemCls}:not(${formItemCls}-horizontal)`]: {
[`${formItemCls}-row`]: {
flexDirection: 'column',
},
'&-label > label': {
[`${formItemCls}-label > label`]: {
height: 'auto',
},
[`${componentCls}-item-control`]: {
[`${formItemCls}-control`]: {
width: '100%',
},
[`${formItemCls}-label,
${antCls}-col-24${formItemCls}-label,
${antCls}-col-xl-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
},
},
[`${componentCls}-vertical ${formItemCls}-label,
.${rootPrefixCls}-col-24${formItemCls}-label,
.${rootPrefixCls}-col-xl-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
[`@media (max-width: ${unit(token.screenXSMax)})`]: [
makeVerticalLayout(token),
{
[componentCls]: {
[`.${rootPrefixCls}-col-xs-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
[`${formItemCls}:not(${formItemCls}-horizontal)`]: {
[`${antCls}-col-xs-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
},
},
},
],
[`@media (max-width: ${unit(token.screenSMMax)})`]: {
[componentCls]: {
[`.${rootPrefixCls}-col-sm-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
[`${formItemCls}:not(${formItemCls}-horizontal)`]: {
[`${antCls}-col-sm-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
},
},
},
[`@media (max-width: ${unit(token.screenMDMax)})`]: {
[componentCls]: {
[`.${rootPrefixCls}-col-md-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
[`${formItemCls}:not(${formItemCls}-horizontal)`]: {
[`${antCls}-col-md-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
},
},
},
[`@media (max-width: ${unit(token.screenLGMax)})`]: {
[componentCls]: {
[`.${rootPrefixCls}-col-lg-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
[`${formItemCls}:not(${formItemCls}-horizontal)`]: {
[`${antCls}-col-lg-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
},
},
},
};
};
const genItemVerticalStyle: GenerateStyle<FormToken> = (token) => {
const { formItemCls, antCls } = token;
return {
[`${formItemCls}-vertical`]: {
[`${formItemCls}-row`]: {
flexDirection: 'column',
},
[`${formItemCls}-label > label`]: {
height: 'auto',
},
[`${formItemCls}-control`]: {
width: '100%',
},
},
[`${formItemCls}-vertical ${formItemCls}-label,
${antCls}-col-24${formItemCls}-label,
${antCls}-col-xl-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
[`@media (max-width: ${unit(token.screenXSMax)})`]: [
makeVerticalLayout(token),
{
[formItemCls]: {
[`${antCls}-col-xs-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
},
},
],
[`@media (max-width: ${unit(token.screenSMMax)})`]: {
[formItemCls]: {
[`${antCls}-col-sm-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
},
},
[`@media (max-width: ${unit(token.screenMDMax)})`]: {
[formItemCls]: {
[`${antCls}-col-md-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
},
},
[`@media (max-width: ${unit(token.screenLGMax)})`]: {
[formItemCls]: {
[`${antCls}-col-lg-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
},
},
};
@ -581,9 +639,11 @@ export default genStyleHooks(
genFormStyle(formToken),
genFormItemStyle(formToken),
genFormValidateMotionStyle(formToken),
genHorizontalStyle(formToken),
genHorizontalStyle(formToken, formToken.componentCls),
genHorizontalStyle(formToken, formToken.formItemCls),
genInlineStyle(formToken),
genVerticalStyle(formToken),
genItemVerticalStyle(formToken),
genCollapseMotion(formToken),
zoomIn,
];

View File

@ -688,6 +688,52 @@ exports[`renders components/image/demo/preview-group-visible.tsx extend context
exports[`renders components/image/demo/preview-group-visible.tsx extend context correctly 2`] = `[]`;
exports[`renders components/image/demo/preview-imgInfo.tsx extend context correctly 1`] = `
<div
class="ant-image"
style="width: 200px; height: 200px;"
>
<img
alt="test"
class="ant-image-img"
height="200px"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
style="height: 200px;"
width="200px"
/>
<div
class="ant-image-mask"
>
<div
class="ant-image-mask-info"
>
<span
aria-label="eye"
class="anticon anticon-eye"
role="img"
>
<svg
aria-hidden="true"
data-icon="eye"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
/>
</svg>
</span>
Preview
</div>
</div>
</div>
`;
exports[`renders components/image/demo/preview-imgInfo.tsx extend context correctly 2`] = `[]`;
exports[`renders components/image/demo/preview-mask.tsx extend context correctly 1`] = `
<div
class="ant-image"

View File

@ -669,6 +669,50 @@ exports[`renders components/image/demo/preview-group-visible.tsx correctly 1`] =
</div>
`;
exports[`renders components/image/demo/preview-imgInfo.tsx correctly 1`] = `
<div
class="ant-image"
style="width:200px;height:200px"
>
<img
alt="test"
class="ant-image-img"
height="200px"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
style="height:200px"
width="200px"
/>
<div
class="ant-image-mask"
>
<div
class="ant-image-mask-info"
>
<span
aria-label="eye"
class="anticon anticon-eye"
role="img"
>
<svg
aria-hidden="true"
data-icon="eye"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
/>
</svg>
</span>
Preview
</div>
</div>
</div>
`;
exports[`renders components/image/demo/preview-mask.tsx correctly 1`] = `
<div
class="ant-image"

View File

@ -0,0 +1,7 @@
## zh-CN
在渲染函数中获取图片的信息。
## en-US
Gets image info in the render function.

View File

@ -0,0 +1,19 @@
import React from 'react';
import { Image } from 'antd';
const src = 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png';
const App: React.FC = () => (
<Image
src={src}
width="200px"
height="200px"
alt="test"
preview={{
imageRender: (_, { image }) => <div>{JSON.stringify(image)}</div>,
toolbarRender: (_, { image }) => <div>{JSON.stringify(image)}</div>,
}}
/>
);
export default App;

View File

@ -1,6 +1,7 @@
import React from 'react';
import {
DownloadOutlined,
UndoOutlined,
RotateLeftOutlined,
RotateRightOutlined,
SwapOutlined,
@ -38,7 +39,15 @@ const App: React.FC = () => {
_,
{
transform: { scale },
actions: { onFlipY, onFlipX, onRotateLeft, onRotateRight, onZoomOut, onZoomIn },
actions: {
onFlipY,
onFlipX,
onRotateLeft,
onRotateRight,
onZoomOut,
onZoomIn,
onReset,
},
},
) => (
<Space size={12} className="toolbar-wrapper">
@ -49,6 +58,7 @@ const App: React.FC = () => {
<RotateRightOutlined onClick={onRotateRight} />
<ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} />
<ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} />
<UndoOutlined onClick={onReset} />
</Space>
),
}}

View File

@ -29,6 +29,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*LVQ3R5JjjJEAAA
<code src="./demo/nested.tsx">nested</code>
<code src="./demo/preview-group-top-progress.tsx" debug>Top progress customization when previewing multiple images</code>
<code src="./demo/component-token.tsx" debug>Custom component token</code>
<code src="./demo/preview-imgInfo.tsx" debug>Gets image info in the render function</code>
## API
@ -96,8 +97,8 @@ Other attributes [&lt;img>](https://developer.mozilla.org/en-US/docs/Web/HTML/El
| closeIcon | Custom close icon | React.ReactNode | - | 5.7.0 |
| forceRender | Force render preview dialog | boolean | - | - |
| countRender | Custom preview count content | (current: number, total: number) => React.ReactNode | - | 4.20.0 |
| toolbarRender | Custom toolbar render | (originalNode: React.ReactElement, info: [ToolbarRenderInfoType](#toolbarrenderinfotype)) => React.ReactNode | - | 5.7.0 |
| imageRender | Custom preview content | (originalNode: React.ReactElement, info: { transform: [TransformType](#transformtype), current: number }) => React.ReactNode | - | 5.7.0 |
| toolbarRender | Custom toolbar render | (originalNode: React.ReactElement, info: [ToolbarRenderInfoType](#toolbarrenderinfotype)) => React.ReactNode | - | 5.7.0, `info.image`: 5.18.0 |
| imageRender | Custom preview content | (originalNode: React.ReactElement, info: { transform: [TransformType](#transformtype), image: [ImgInfo](#imginfo), current: number }) => React.ReactNode | - | 5.7.0, image: 5.18.0 |
| onTransform | Callback when the transform of image changed | { transform: [TransformType](#transformtype), action: [TransformAction](#transformaction) } | - | 5.7.0 |
| onChange | Callback when switch preview image | (current: number, prevCurrent: number) => void | - | 5.3.0 |
| onVisibleChange | Callback when `visible` changed | (visible: boolean, prevVisible: boolean, current: number) => void | - | current Property 5.3.0 |
@ -155,10 +156,23 @@ type TransformAction =
onRotateRight: () => void;
onZoomOut: () => void;
onZoomIn: () => void;
onReset: () => void; // support after 5.17.3
onClose: () => void;
};
transform: TransformType,
current: number;
total: number;
image: ImgInfo
}
```
### ImgInfo
```typescript
{
url: string;
alt: string;
width: string | number;
height: string | number;
}
```

View File

@ -30,6 +30,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*LVQ3R5JjjJEAAA
<code src="./demo/nested.tsx">嵌套</code>
<code src="./demo/preview-group-top-progress.tsx" debug>多图预览时顶部进度自定义</code>
<code src="./demo/component-token.tsx" debug>自定义组件 Token</code>
<code src="./demo/preview-imgInfo.tsx" debug>在渲染函数中获取图片信息</code>
## API
@ -97,8 +98,8 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*LVQ3R5JjjJEAAA
| closeIcon | 自定义关闭 Icon | React.ReactNode | - | 5.7.0 |
| forceRender | 强制渲染预览图 | boolean | - | - |
| countRender | 自定义预览计数内容 | (current: number, total: number) => React.ReactNode | - | 4.20.0 |
| toolbarRender | 自定义工具栏 | (originalNode: React.ReactElement, info: [ToolbarRenderInfoType](#toolbarrenderinfotype)) => React.ReactNode | - | 5.7.0 |
| imageRender | 自定义预览内容 | (originalNode: React.ReactElement, info: { transform: [TransformType](#transformtype), current: number }) => React.ReactNode | - | 5.7.0 |
| toolbarRender | 自定义工具栏 | (originalNode: React.ReactElement, info: [ToolbarRenderInfoType](#toolbarrenderinfotype)) => React.ReactNode | - | 5.7.0, `info.image`: 5.18.0 |
| imageRender | 自定义预览内容 | (originalNode: React.ReactElement, info: { transform: [TransformType](#transformtype), image: [ImgInfo](#imginfo), current: number }) => React.ReactNode | - | 5.7.0, image: 5.18.0 |
| onTransform | 预览图 transform 变化的回调 | { transform: [TransformType](#transformtype), action: [TransformAction](#transformaction) } | - | 5.7.0 |
| onChange | 切换预览图的回调 | (current: number, prevCurrent: number) => void | - | 5.3.0 |
| onVisibleChange | 当 `visible` 发生改变时的回调 | (visible: boolean, prevVisible: boolean, current: number) => void | - | current 参数 5.3.0 |
@ -134,7 +135,8 @@ type TransformAction =
| 'wheel'
| 'doubleClick'
| 'move'
| 'dragRebound';
| 'dragRebound'
| 'reset';
```
### ToolbarRenderInfoType
@ -156,10 +158,24 @@ type TransformAction =
onRotateRight: () => void;
onZoomOut: () => void;
onZoomIn: () => void;
onReset: () => void; // 5.17.3 之后支持
onClose: () => void;
};
transform: TransformType,
current: number;
total: number;
image: ImgInfo
}
```
### ImgInfo
```typescript
{
url: string;
alt: string;
width: string | number;
height: string | number;
}
```

View File

@ -287,7 +287,7 @@ export const genInputGroupStyle = (token: InputToken): CSSObject => {
},
'& > *': {
display: 'inline-block',
display: 'inline-flex',
float: 'none',
verticalAlign: 'top', // https://github.com/ant-design/ant-design-pro/issues/139
borderRadius: 0,

View File

@ -9,9 +9,17 @@ import { ListContext } from './context';
export interface ListItemProps extends HTMLAttributes<HTMLDivElement> {
className?: string;
classNames?: {
actions?: string;
extra?: string;
};
children?: ReactNode;
prefixCls?: string;
style?: CSSProperties;
styles?: {
actions?: CSSProperties;
extra?: CSSProperties;
};
extra?: ReactNode;
actions?: ReactNode[];
colStyle?: CSSProperties;
@ -27,6 +35,9 @@ export interface ListItemMetaProps {
title?: ReactNode;
}
type ListItemClassNamesModule = keyof Exclude<ListItemProps['classNames'], undefined>;
type ListItemStylesModule = keyof Exclude<ListItemProps['styles'], undefined>;
export const Meta: FC<ListItemMetaProps> = ({
prefixCls: customizePrefixCls,
className,
@ -61,12 +72,22 @@ const InternalItem = React.forwardRef<HTMLDivElement, ListItemProps>((props, ref
children,
actions,
extra,
styles,
className,
classNames: customizeClassNames,
colStyle,
...others
} = props;
const { grid, itemLayout } = useContext(ListContext);
const { getPrefixCls } = useContext(ConfigContext);
const { getPrefixCls, list } = useContext(ConfigContext);
const moduleClass = (moduleName: ListItemClassNamesModule) =>
classNames(list?.item?.classNames?.[moduleName], customizeClassNames?.[moduleName]);
const moduleStyle = (moduleName: ListItemStylesModule): React.CSSProperties => ({
...list?.item?.styles?.[moduleName],
...styles?.[moduleName],
});
const isItemContainsTextNodeAndNotSingular = () => {
let result;
@ -87,7 +108,11 @@ const InternalItem = React.forwardRef<HTMLDivElement, ListItemProps>((props, ref
const prefixCls = getPrefixCls('list', customizePrefixCls);
const actionsContent = actions && actions.length > 0 && (
<ul className={`${prefixCls}-item-action`} key="actions">
<ul
className={classNames(`${prefixCls}-item-action`, moduleClass('actions'))}
key="actions"
style={moduleStyle('actions')}
>
{actions.map((action: ReactNode, i: number) => (
// eslint-disable-next-line react/no-array-index-key
<li key={`${prefixCls}-item-action-${i}`}>
@ -116,7 +141,11 @@ const InternalItem = React.forwardRef<HTMLDivElement, ListItemProps>((props, ref
{children}
{actionsContent}
</div>,
<div className={`${prefixCls}-item-extra`} key="extra">
<div
className={classNames(`${prefixCls}-item-extra`, moduleClass('extra'))}
key="extra"
style={moduleStyle('extra')}
>
{extra}
</div>,
]

View File

@ -246,4 +246,62 @@ describe('List Item Layout', () => {
const title = container.querySelector('.ant-list-item-meta-title');
expect(title && getComputedStyle(title).margin).toEqual('0px 0px 4px 0px');
});
it('List.Item support styles and classNames', () => {
const dataSource = [{ id: 1, title: `ant design` }];
const getItem = (item: any, provider?: boolean) => {
const styles = provider ? { extra: { color: 'red' }, actions: { color: 'blue' } } : undefined;
return (
<List.Item
extra="test-extra"
actions={['test-actions']}
styles={styles}
classNames={{ extra: 'test-extra', actions: 'test-actions' }}
>
{item.title}
</List.Item>
);
};
// ConfigProvider
const { container, rerender } = render(
<ConfigProvider
list={{
item: {
styles: { extra: { color: 'pink' }, actions: { color: 'green' } },
classNames: { extra: 'test-provider-extra', actions: 'test-provider-actions' },
},
}}
>
<List itemLayout="vertical" dataSource={dataSource} renderItem={(item) => getItem(item)} />,
</ConfigProvider>,
);
expect(container.querySelector('.ant-list-item-extra')!).toHaveStyle('color: pink');
expect(container.querySelector('.ant-list-item-action')!).toHaveStyle('color: green');
expect(container.querySelector('.ant-list-item-extra')!).toHaveClass(
'test-provider-extra test-extra',
);
expect(container.querySelector('.ant-list-item-action')!).toHaveClass(
'test-provider-actions test-actions',
);
// item styles is high priority
rerender(
<ConfigProvider
list={{
item: { styles: { extra: { color: 'pink' }, actions: { color: 'green' } } },
}}
>
<List
itemLayout="vertical"
dataSource={dataSource}
renderItem={(item) => getItem(item, true)}
/>
,
</ConfigProvider>,
);
expect(container.querySelector('.ant-list-item-extra')!).toHaveStyle('color: red');
expect(container.querySelector('.ant-list-item-action')!).toHaveStyle('color: blue');
});
});

View File

@ -1799,20 +1799,24 @@ exports[`renders components/list/demo/loadmore.tsx extend context correctly 1`]
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
</div>

View File

@ -1794,20 +1794,24 @@ exports[`renders components/list/demo/loadmore.tsx correctly 1`] = `
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
</div>

View File

@ -78,7 +78,9 @@ More about pagination, please check [`Pagination`](/components/pagination/).
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| actions | The actions content of list item. If `itemLayout` is `vertical`, shows the content on bottom, otherwise shows content on the far right | Array&lt;ReactNode> | - | |
| classNames | Semantic structure className | `Record<actions \| extra , string>` | - | 5.18.0 |
| extra | The extra content of list item. If `itemLayout` is `vertical`, shows the content on right, otherwise shows content on the far right | ReactNode | - | |
| styles | Semantic DOM style | `Record<actions \| extra , CSSProperties>` | - | 5.18.0 |
### List.Item.Meta

View File

@ -81,7 +81,9 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*tBzwQ7raKX8AAA
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| actions | 列表操作组,根据 `itemLayout` 的不同,位置在卡片底部或者最右侧 | Array&lt;ReactNode> | - | |
| classNames | 语义化结构 className | `Record<actions \| extra , string>` | - | 5.18.0 |
| extra | 额外内容,通常用在 `itemLayout``vertical` 的情况下,展示右侧内容; `horizontal` 展示在列表元素最右侧 | ReactNode | - | |
| styles | 语义化结构 style | `Record<actions \| extra , CSSProperties>` | - | 5.18.0 |
### List.Item.Meta

View File

@ -12,6 +12,7 @@ import zIndexContext from '../_util/zindexContext';
import { ConfigContext } from '../config-provider';
import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
import { NoFormStyle } from '../form/context';
import Skeleton from '../skeleton';
import { NoCompactStyle } from '../space/Compact';
import { usePanelRef } from '../watermark/context';
import type { ModalProps, MousePosition } from './interface';
@ -86,6 +87,8 @@ const Modal: React.FC<ModalProps> = (props) => {
footer,
classNames: modalClassNames,
styles: modalStyles,
children,
loading,
...restProps
} = props;
@ -100,9 +103,10 @@ const Modal: React.FC<ModalProps> = (props) => {
[`${prefixCls}-wrap-rtl`]: direction === 'rtl',
});
const dialogFooter = footer !== null && (
<Footer {...props} onOk={handleOk} onCancel={handleCancel} />
);
const dialogFooter =
footer !== null && !loading ? (
<Footer {...props} onOk={handleOk} onCancel={handleCancel} />
) : null;
const [mergedClosable, mergedCloseIcon] = useClosable(
pickClosable(props),
@ -149,12 +153,20 @@ const Modal: React.FC<ModalProps> = (props) => {
...modalClassNames,
wrapper: classNames(wrapClassNameExtended, modalClassNames?.wrapper),
}}
styles={{
...modalContext?.styles,
...modalStyles,
}}
styles={{ ...modalContext?.styles, ...modalStyles }}
panelRef={panelRef}
/>
>
{loading ? (
<Skeleton
active
title={false}
paragraph={{ rows: 4 }}
className={`${prefixCls}-body-skeleton`}
/>
) : (
children
)}
</Dialog>
</zIndexContext.Provider>
</NoFormStyle>
</NoCompactStyle>,

View File

@ -630,6 +630,19 @@ exports[`renders components/modal/demo/hooks.tsx extend context correctly 1`] =
exports[`renders components/modal/demo/hooks.tsx extend context correctly 2`] = `[]`;
exports[`renders components/modal/demo/loading.tsx extend context correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open Modal
</span>
</button>
`;
exports[`renders components/modal/demo/loading.tsx extend context correctly 2`] = `[]`;
exports[`renders components/modal/demo/locale.tsx extend context correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"

View File

@ -606,6 +606,17 @@ exports[`renders components/modal/demo/hooks.tsx correctly 1`] = `
</div>
`;
exports[`renders components/modal/demo/loading.tsx correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open Modal
</span>
</button>
`;
exports[`renders components/modal/demo/locale.tsx correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"

View File

@ -0,0 +1,7 @@
## zh-CN
设置对话框加载状态。
## en-US
Set the loading status of Modal.

View File

@ -0,0 +1,42 @@
import React from 'react';
import { Button, Modal } from 'antd';
const App: React.FC = () => {
const [open, setOpen] = React.useState<boolean>(false);
const [loading, setLoading] = React.useState<boolean>(true);
const showLoading = () => {
setOpen(true);
setLoading(true);
// Simple loading mock. You should add cleanup logic in real world.
setTimeout(() => {
setLoading(false);
}, 2000);
};
return (
<>
<Button type="primary" onClick={showLoading}>
Open Modal
</Button>
<Modal
title={<p>Loading Modal</p>}
footer={
<Button type="primary" onClick={showLoading}>
Reload
</Button>
}
loading={loading}
open={open}
onCancel={() => setOpen(false)}
>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Modal>
</>
);
};
export default App;

View File

@ -21,6 +21,7 @@ Additionally, if you need to show a simple confirmation dialog, you can use [`Ap
<code src="./demo/basic.tsx">Basic</code>
<code src="./demo/async.tsx">Asynchronously close</code>
<code src="./demo/footer.tsx">Customized Footer</code>
<code src="./demo/loading.tsx" version="5.18.0">Loading</code>
<code src="./demo/footer-render.tsx">Customized Footer render function</code>
<code src="./demo/hooks.tsx">Use hooks to get context</code>
<code src="./demo/locale.tsx">Internationalization</code>
@ -68,6 +69,7 @@ Common props ref[Common props](/docs/react/common-props)
| okText | Text of the OK button | ReactNode | `OK` | |
| okType | Button `type` of the OK button | string | `primary` | |
| style | Style of floating layer, typically used at least for adjusting the position | CSSProperties | - | |
| loading | Show the skeleton | boolean | | 5.18.0 |
| title | The modal dialog's title | ReactNode | - | |
| open | Whether the modal dialog is visible or not | boolean | false | |
| width | Width of the modal dialog | string \| number | 520 | |

View File

@ -22,6 +22,7 @@ demo:
<code src="./demo/basic.tsx">基本</code>
<code src="./demo/async.tsx">异步关闭</code>
<code src="./demo/footer.tsx">自定义页脚</code>
<code src="./demo/loading.tsx" version="5.18.0">加载中</code>
<code src="./demo/footer-render.tsx">自定义页脚渲染函数</code>
<code src="./demo/hooks.tsx">使用 hooks 获得上下文</code>
<code src="./demo/locale.tsx">国际化</code>
@ -69,6 +70,7 @@ demo:
| okText | 确认按钮文字 | ReactNode | `确定` | |
| okType | 确认按钮类型 | string | `primary` | |
| style | 可用于设置浮层的样式,调整浮层位置等 | CSSProperties | - | |
| loading | 显示骨架屏 | boolean | | 5.18.0 |
| title | 标题 | ReactNode | - | |
| open | 对话框是否可见 | boolean | - | |
| width | 宽度 | string \| number | 520 | |

View File

@ -1,4 +1,4 @@
import type { FC } from 'react';
import type React from 'react';
import type { DialogProps } from 'rc-dialog';
import type { ClosableType } from '../_util/hooks/useClosable';
@ -8,7 +8,10 @@ import type { DirectionType } from '../config-provider';
interface ModalCommonProps extends Omit<DialogProps, 'footer'> {
footer?:
| React.ReactNode
| ((originNode: React.ReactNode, extra: { OkBtn: FC; CancelBtn: FC }) => React.ReactNode);
| ((
originNode: React.ReactNode,
extra: { OkBtn: React.FC; CancelBtn: React.FC },
) => React.ReactNode);
}
export interface ModalProps extends ModalCommonProps {
@ -70,6 +73,10 @@ export interface ModalProps extends ModalCommonProps {
// Legacy
/** @deprecated Please use `open` instead. */
visible?: boolean;
/**
* @since 5.18.0
*/
loading?: boolean;
}
type getContainerFunc = () => HTMLElement;

View File

@ -270,6 +270,14 @@ const genModalStyle: GenerateStyle<ModalToken> = (token) => {
lineHeight: token.lineHeight,
wordWrap: 'break-word',
padding: token.bodyPadding,
[`${componentCls}-body-skeleton`]: {
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
margin: `${unit(token.margin)} auto`,
},
},
[`${componentCls}-footer`]: {

View File

@ -537,6 +537,39 @@ exports[`renders components/notification/demo/render-panel.tsx extend context co
exports[`renders components/notification/demo/render-panel.tsx extend context correctly 2`] = `[]`;
exports[`renders components/notification/demo/show-with-progress.tsx extend context correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Pause on hover
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Don't pause on hover
</span>
</button>
</div>
</div>
`;
exports[`renders components/notification/demo/show-with-progress.tsx extend context correctly 2`] = `[]`;
exports[`renders components/notification/demo/stack.tsx extend context correctly 1`] = `
<div>
<div

View File

@ -523,6 +523,37 @@ exports[`renders components/notification/demo/render-panel.tsx correctly 1`] = `
</div>
`;
exports[`renders components/notification/demo/show-with-progress.tsx correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Pause on hover
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Don't pause on hover
</span>
</button>
</div>
</div>
`;
exports[`renders components/notification/demo/stack.tsx correctly 1`] = `
<div>
<div

View File

@ -0,0 +1,7 @@
## zh-CN
显示自动关闭通知框的进度条。
## en-US
Show progress bar for auto-closing notification.

View File

@ -0,0 +1,32 @@
import React from 'react';
import { Button, Space, notification } from 'antd';
const App: React.FC = () => {
const [api, contextHolder] = notification.useNotification();
const openNotification = (pauseOnHover: boolean) => () => {
api.open({
message: 'Notification Title',
description:
'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
showProgress: true,
pauseOnHover,
});
};
return (
<>
{contextHolder}
<Space>
<Button type="primary" onClick={openNotification(true)}>
Pause on hover
</Button>
<Button type="primary" onClick={openNotification(false)}>
Don&apos;t pause on hover
</Button>
</Space>
</>
);
};
export default App;

View File

@ -30,6 +30,7 @@ To display a notification message at any of the four corners of the viewport. Ty
<code src="./demo/custom-style.tsx">Customized style</code>
<code src="./demo/update.tsx">Update Message Content</code>
<code src="./demo/stack.tsx" version="5.10.0">Stack</code>
<code src="./demo/show-with-progress.tsx" version="5.18.0">Show with progress</code>
<code src="./demo/basic.tsx">Static Method (deprecated)</code>
<code src="./demo/render-panel.tsx" debug>_InternalPanelDoNotUseOrYouWillBeFired</code>
@ -53,6 +54,8 @@ The properties of config are as follows:
| closeIcon | Custom close icon | ReactNode | true | 5.7.0: close button will be hidden when setting to null or false |
| description | The content of notification box (required) | ReactNode | - | - |
| duration | Time in seconds before Notification is closed. When set to 0 or null, it will never be closed automatically | number | 4.5 | - |
| showProgress | Show progress bar for auto-closing notification | boolean | | 5.18.0 |
| pauseOnHover | keep the timer running or not on hover | boolean | true | 5.18.0 |
| icon | Customized icon | ReactNode | - | - |
| key | The unique identifier of the Notification | string | - | - |
| message | The title of notification box (required) | ReactNode | - | - |

View File

@ -31,6 +31,7 @@ demo:
<code src="./demo/custom-style.tsx">自定义样式</code>
<code src="./demo/update.tsx">更新消息内容</code>
<code src="./demo/stack.tsx" version="5.10.0">堆叠</code>
<code src="./demo/show-with-progress.tsx" version="5.18.0">显示进度条</code>
<code src="./demo/basic.tsx">静态方法(不推荐)</code>
<code src="./demo/render-panel.tsx" debug>_InternalPanelDoNotUseOrYouWillBeFired</code>
@ -105,6 +106,8 @@ notification.config({
| bottom | 消息从底部弹出时,距离底部的位置,单位像素 | number | 24 | |
| closeIcon | 自定义关闭图标 | ReactNode | true | 5.7.0:设置为 null 或 false 时隐藏关闭按钮 |
| duration | 默认自动关闭延时,单位秒 | number | 4.5 | |
| showProgress | 显示自动关闭通知框的进度条 | boolean | | 5.18.0 |
| pauseOnHover | 悬停时是否暂停计时器 | boolean | true | 5.18.0 |
| getContainer | 配置渲染节点的输出位置,但依旧为全屏展示 | () => HTMLNode | () => document.body | |
| placement | 弹出位置,可选 `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` | string | `topRight` | |
| rtl | 是否开启 RTL 模式 | boolean | false | |

View File

@ -25,6 +25,8 @@ export interface ArgsProps {
key?: React.Key;
onClose?: () => void;
duration?: number | null;
showProgress?: boolean;
pauseOnHover?: boolean;
icon?: React.ReactNode;
placement?: NotificationPlacement;
style?: React.CSSProperties;
@ -52,6 +54,8 @@ export interface GlobalConfigProps {
top?: number;
bottom?: number;
duration?: number;
showProgress?: boolean;
pauseOnHover?: boolean;
prefixCls?: string;
getContainer?: () => HTMLElement | ShadowRoot;
placement?: NotificationPlacement;
@ -72,4 +76,6 @@ export interface NotificationConfig {
rtl?: boolean;
stack?: boolean | { threshold?: number };
duration?: number;
showProgress?: boolean;
pauseOnHover?: boolean;
}

View File

@ -34,6 +34,8 @@ export interface NotificationToken extends FullToken<'Notification'> {
notificationMarginBottom: number;
notificationMarginEdge: number;
notificationStackLayer: number;
notificationProgressBg: string;
notificationProgressHeight: number;
}
export const genNoticeStyle = (token: NotificationToken): CSSObject => {
@ -52,6 +54,8 @@ export const genNoticeStyle = (token: NotificationToken): CSSObject => {
notificationBg,
notificationPadding,
notificationMarginEdge,
notificationProgressBg,
notificationProgressHeight,
fontSize,
lineHeight,
width,
@ -154,6 +158,39 @@ export const genNoticeStyle = (token: NotificationToken): CSSObject => {
...genFocusStyle(token),
},
[`${noticeCls}-progress`]: {
position: 'absolute',
display: 'block',
appearance: 'none',
WebkitAppearance: 'none',
inlineSize: `calc(100% - ${unit(borderRadiusLG)} * 2)`,
left: {
_skip_check_: true,
value: borderRadiusLG,
},
right: {
_skip_check_: true,
value: borderRadiusLG,
},
bottom: 0,
blockSize: notificationProgressHeight,
border: 0,
'&, &::-webkit-progress-bar': {
borderRadius: borderRadiusLG,
backgroundColor: `rgba(0, 0, 0, 0.04)`,
},
'&::-moz-progress-bar': {
background: notificationProgressBg,
},
'&::-webkit-progress-value': {
borderRadius: borderRadiusLG,
background: notificationProgressBg,
},
},
[`${noticeCls}-btn`]: {
float: 'right',
marginTop: token.marginSM,
@ -279,6 +316,8 @@ export const prepareNotificationToken: (
notificationMarginEdge: token.marginLG,
animationMaxHeight: 150,
notificationStackLayer: 3,
notificationProgressHeight: 2,
notificationProgressBg: `linear-gradient(90deg, ${token.colorPrimaryBorderHover}, ${token.colorPrimary})`,
});
return notificationToken;

View File

@ -89,6 +89,7 @@ const Holder = React.forwardRef<HolderRef, HolderProps>((props, ref) => {
closable: true,
closeIcon: getCloseIcon(prefixCls),
duration: duration ?? DEFAULT_DURATION,
pauseOnHover: true,
getContainer: () => staticGetContainer?.() || getPopupContainer?.() || document.body,
maxCount,
onAllRemoved,

View File

@ -1,17 +1,22 @@
import * as React from 'react';
import { presetPrimaryColors } from '@ant-design/colors';
import classNames from 'classnames';
import { devUseWarning } from '../_util/warning';
import type { DirectionType } from '../config-provider';
import type { ProgressGradient, ProgressProps, StringGradients } from './progress';
import type {
ProgressGradient,
ProgressProps,
StringGradients,
PercentPositionType,
} from './progress';
import { LineStrokeColorVar, Percent } from './style';
import { getSize, getSuccessPercent, validProgress } from './utils';
interface LineProps extends ProgressProps {
prefixCls: string;
direction?: DirectionType;
children: React.ReactNode;
strokeColor?: string | ProgressGradient;
percentPosition: PercentPositionType;
}
/**
@ -82,9 +87,12 @@ const Line: React.FC<LineProps> = (props) => {
strokeLinecap = 'round',
children,
trailColor = null,
percentPosition,
success,
} = props;
const { align: infoAlign, type: infoPosition } = percentPosition;
const backgroundProps =
strokeColor && typeof strokeColor !== 'string'
? handleGradient(strokeColor, directionConfig)
@ -126,21 +134,36 @@ const Line: React.FC<LineProps> = (props) => {
const outerStyle: React.CSSProperties = {
width: width < 0 ? '100%' : width,
height,
};
return (
<>
<div className={`${prefixCls}-outer`} style={outerStyle}>
<div className={`${prefixCls}-inner`} style={trailStyle}>
<div className={`${prefixCls}-bg`} style={percentStyle} />
{successPercent !== undefined ? (
<div className={`${prefixCls}-success-bg`} style={successPercentStyle} />
) : null}
</div>
const lineInner = (
<div className={`${prefixCls}-inner`} style={trailStyle}>
<div
className={classNames(`${prefixCls}-bg`, `${prefixCls}-bg-${infoPosition}`)}
style={percentStyle}
>
{infoPosition === 'inner' && children}
</div>
{successPercent !== undefined && (
<div className={`${prefixCls}-success-bg`} style={successPercentStyle} />
)}
</div>
);
const isOuterStart = infoPosition === 'outer' && infoAlign === 'start';
const isOuterEnd = infoPosition === 'outer' && infoAlign === 'end';
return infoPosition === 'outer' && infoAlign === 'center' ? (
<div className={`${prefixCls}-layout-bottom`}>
{lineInner}
{children}
</>
</div>
) : (
<div className={`${prefixCls}-outer`} style={outerStyle}>
{isOuterStart && children}
{lineInner}
{isOuterEnd && children}
</div>
);
};

File diff suppressed because it is too large Load Diff

View File

@ -246,18 +246,18 @@ exports[`Progress render dashboard zero gapDegree 1`] = `
exports[`Progress render format 1`] = `
<div
aria-valuenow="10"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-show-info ant-progress-default"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-line-align-end ant-progress-line-position-outer ant-progress-show-info ant-progress-default"
role="progressbar"
>
<div
class="ant-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="ant-progress-inner"
>
<div
class="ant-progress-bg"
class="ant-progress-bg ant-progress-bg-outer"
style="width: 50%; height: 8px; --progress-percent: 0.5;"
/>
<div
@ -265,59 +265,88 @@ exports[`Progress render format 1`] = `
style="width: 10%; height: 8px;"
/>
</div>
<span
class="ant-progress-text ant-progress-text-end ant-progress-text-outer"
title="50 10"
>
50 10
</span>
</div>
<span
class="ant-progress-text"
title="50 10"
</div>
`;
exports[`Progress render inner info position 1`] = `
<div
aria-valuenow="100"
class="ant-progress ant-progress-status-success ant-progress-line ant-progress-line-align-center ant-progress-line-position-inner ant-progress-show-info"
role="progressbar"
>
<div
class="ant-progress-outer"
style="width: 400px;"
>
50 10
</span>
<div
class="ant-progress-inner"
>
<div
class="ant-progress-bg ant-progress-bg-inner"
style="width: 100%; height: 20px; --progress-percent: 1;"
>
<span
class="ant-progress-text ant-progress-text-center ant-progress-text-inner"
title="100%"
>
100%
</span>
</div>
</div>
</div>
</div>
`;
exports[`Progress render negative progress 1`] = `
<div
aria-valuenow="-20"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-show-info ant-progress-default"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-line-align-end ant-progress-line-position-outer ant-progress-show-info ant-progress-default"
role="progressbar"
>
<div
class="ant-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="ant-progress-inner"
>
<div
class="ant-progress-bg"
class="ant-progress-bg ant-progress-bg-outer"
style="width: 0%; height: 8px; --progress-percent: 0;"
/>
</div>
<span
class="ant-progress-text ant-progress-text-end ant-progress-text-outer"
title="0%"
>
0%
</span>
</div>
<span
class="ant-progress-text"
title="0%"
>
0%
</span>
</div>
`;
exports[`Progress render negative successPercent 1`] = `
<div
aria-valuenow="-20"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-show-info ant-progress-default"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-line-align-end ant-progress-line-position-outer ant-progress-show-info ant-progress-default"
role="progressbar"
>
<div
class="ant-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="ant-progress-inner"
>
<div
class="ant-progress-bg"
class="ant-progress-bg ant-progress-bg-outer"
style="width: 50%; height: 8px; --progress-percent: 0.5;"
/>
<div
@ -325,131 +354,131 @@ exports[`Progress render negative successPercent 1`] = `
style="width: 0%; height: 8px;"
/>
</div>
<span
class="ant-progress-text ant-progress-text-end ant-progress-text-outer"
title="50%"
>
50%
</span>
</div>
<span
class="ant-progress-text"
title="50%"
>
50%
</span>
</div>
`;
exports[`Progress render normal progress 1`] = `
<div
aria-valuenow="0"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-show-info ant-progress-default"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-line-align-end ant-progress-line-position-outer ant-progress-show-info ant-progress-default"
role="progressbar"
>
<div
class="ant-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="ant-progress-inner"
>
<div
class="ant-progress-bg"
class="ant-progress-bg ant-progress-bg-outer"
style="width: 0%; height: 8px; --progress-percent: 0;"
/>
</div>
<span
class="ant-progress-text ant-progress-text-end ant-progress-text-outer"
title="0%"
>
0%
</span>
</div>
<span
class="ant-progress-text"
title="0%"
>
0%
</span>
</div>
`;
exports[`Progress render out-of-range progress 1`] = `
<div
aria-valuenow="120"
class="ant-progress ant-progress-status-success ant-progress-line ant-progress-show-info ant-progress-default"
class="ant-progress ant-progress-status-success ant-progress-line ant-progress-line-align-end ant-progress-line-position-outer ant-progress-show-info ant-progress-default"
role="progressbar"
>
<div
class="ant-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="ant-progress-inner"
>
<div
class="ant-progress-bg"
class="ant-progress-bg ant-progress-bg-outer"
style="width: 100%; height: 8px; --progress-percent: 1;"
/>
</div>
</div>
<span
class="ant-progress-text"
>
<span
aria-label="check-circle"
class="anticon anticon-check-circle"
role="img"
class="ant-progress-text ant-progress-text-end ant-progress-text-outer"
>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="check-circle"
class="anticon anticon-check-circle"
role="img"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
</span>
</span>
</span>
</div>
</div>
`;
exports[`Progress render out-of-range progress with info 1`] = `
<div
aria-valuenow="120"
class="ant-progress ant-progress-status-success ant-progress-line ant-progress-show-info ant-progress-default"
class="ant-progress ant-progress-status-success ant-progress-line ant-progress-line-align-end ant-progress-line-position-outer ant-progress-show-info ant-progress-default"
role="progressbar"
>
<div
class="ant-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="ant-progress-inner"
>
<div
class="ant-progress-bg"
class="ant-progress-bg ant-progress-bg-outer"
style="width: 100%; height: 8px; --progress-percent: 1;"
/>
</div>
</div>
<span
class="ant-progress-text"
>
<span
aria-label="check-circle"
class="anticon anticon-check-circle"
role="img"
class="ant-progress-text ant-progress-text-end ant-progress-text-outer"
>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="check-circle"
class="anticon anticon-check-circle"
role="img"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
</span>
</span>
</span>
</div>
</div>
`;
@ -511,56 +540,56 @@ exports[`Progress render strokeColor 1`] = `
exports[`Progress render strokeColor 2`] = `
<div
aria-valuenow="50"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-show-info ant-progress-default"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-line-align-end ant-progress-line-position-outer ant-progress-show-info ant-progress-default"
role="progressbar"
>
<div
class="ant-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="ant-progress-inner"
>
<div
class="ant-progress-bg"
class="ant-progress-bg ant-progress-bg-outer"
style="width: 50%; height: 8px; --progress-line-stroke-color: linear-gradient(to right, #108ee9, #87d068); --progress-percent: 0.5;"
/>
</div>
<span
class="ant-progress-text ant-progress-text-end ant-progress-text-outer"
title="50%"
>
50%
</span>
</div>
<span
class="ant-progress-text"
title="50%"
>
50%
</span>
</div>
`;
exports[`Progress render strokeColor 3`] = `
<div
aria-valuenow="50"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-show-info ant-progress-default"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-line-align-end ant-progress-line-position-outer ant-progress-show-info ant-progress-default"
role="progressbar"
>
<div
class="ant-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="ant-progress-inner"
>
<div
class="ant-progress-bg"
class="ant-progress-bg ant-progress-bg-outer"
style="width: 50%; height: 8px; --progress-line-stroke-color: linear-gradient(to right, #108ee9 0%, #87d068 100%); --progress-percent: 0.5;"
/>
</div>
<span
class="ant-progress-text ant-progress-text-end ant-progress-text-outer"
title="50%"
>
50%
</span>
</div>
<span
class="ant-progress-text"
title="50%"
>
50%
</span>
</div>
`;
@ -624,18 +653,18 @@ exports[`Progress render strokeWidth of progress 1`] = `
exports[`Progress render successColor progress 1`] = `
<div
aria-valuenow="30"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-show-info ant-progress-default"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-line-align-end ant-progress-line-position-outer ant-progress-show-info ant-progress-default"
role="progressbar"
>
<div
class="ant-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="ant-progress-inner"
>
<div
class="ant-progress-bg"
class="ant-progress-bg ant-progress-bg-outer"
style="width: 60%; height: 8px; --progress-percent: 0.6;"
/>
<div
@ -643,13 +672,13 @@ exports[`Progress render successColor progress 1`] = `
style="width: 30%; height: 8px; background-color: rgb(255, 255, 255);"
/>
</div>
<span
class="ant-progress-text ant-progress-text-end ant-progress-text-outer"
title="60%"
>
60%
</span>
</div>
<span
class="ant-progress-text"
title="60%"
>
60%
</span>
</div>
`;
@ -766,57 +795,57 @@ exports[`Progress render successColor progress type="dashboard" 1`] = `
exports[`Progress render trailColor progress 1`] = `
<div
aria-valuenow="0"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-show-info ant-progress-default"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-line-align-end ant-progress-line-position-outer ant-progress-show-info ant-progress-default"
role="progressbar"
>
<div
class="ant-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="ant-progress-inner"
style="background-color: rgb(255, 255, 255);"
>
<div
class="ant-progress-bg"
class="ant-progress-bg ant-progress-bg-outer"
style="width: 0%; height: 8px; --progress-percent: 0;"
/>
</div>
<span
class="ant-progress-text ant-progress-text-end ant-progress-text-outer"
title="0%"
>
0%
</span>
</div>
<span
class="ant-progress-text"
title="0%"
>
0%
</span>
</div>
`;
exports[`Progress rtl render component should be rendered correctly in RTL direction 1`] = `
<div
aria-valuenow="0"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-show-info ant-progress-default ant-progress-rtl"
class="ant-progress ant-progress-status-normal ant-progress-line ant-progress-line-align-end ant-progress-line-position-outer ant-progress-show-info ant-progress-default ant-progress-rtl"
role="progressbar"
>
<div
class="ant-progress-outer"
style="width: 100%; height: 8px;"
style="width: 100%;"
>
<div
class="ant-progress-inner"
>
<div
class="ant-progress-bg"
class="ant-progress-bg ant-progress-bg-outer"
style="width: 0%; height: 8px; --progress-percent: 0;"
/>
</div>
<span
class="ant-progress-text ant-progress-text-end ant-progress-text-outer"
title="0%"
>
0%
</span>
</div>
<span
class="ant-progress-text"
title="0%"
>
0%
</span>
</div>
`;

View File

@ -279,6 +279,13 @@ describe('Progress', () => {
'Warning: [antd: Progress] Type "circle" and "dashboard" do not accept array as `size`, please use number or preset size instead.',
);
});
it('should warnning if pass object into `size` in type dashboard', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
render(<Progress size={{ width: 60, height: 20 }} type="dashboard" />);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Progress] Type "circle" and "dashboard" do not accept object as `size`, please use number or preset size instead.',
);
});
it('should update the percentage based on the value of percent', () => {
const Content: React.FC = () => {
@ -333,9 +340,6 @@ describe('Progress', () => {
);
const { container, rerender } = render(<App size={30} />);
expect(container.querySelector('.ant-progress-line .ant-progress-outer')).toHaveStyle({
width: '30px',
});
expect(container.querySelector('.ant-progress-steps .ant-progress-steps-item')).toHaveStyle({
width: '30px',
height: '30px',
@ -353,6 +357,8 @@ describe('Progress', () => {
expect(container.querySelector('.ant-progress-line .ant-progress-outer')).toHaveStyle({
width: '60px',
});
expect(container.querySelector('.ant-progress-line .ant-progress-bg')).toHaveStyle({
height: '20px',
});
expect(container.querySelector('.ant-progress-steps .ant-progress-steps-item')).toHaveStyle({
@ -367,6 +373,19 @@ describe('Progress', () => {
width: '60px',
height: '60px',
});
rerender(<App size={{ width: 60, height: 20 }} />);
expect(container.querySelector('.ant-progress-line .ant-progress-outer')).toHaveStyle({
width: '60px',
});
expect(container.querySelector('.ant-progress-line .ant-progress-bg')).toHaveStyle({
height: '20px',
});
expect(container.querySelector('.ant-progress-steps .ant-progress-steps-item')).toHaveStyle({
width: '60px',
height: '20px',
});
});
it('no strict warning', () => {
@ -415,4 +434,40 @@ describe('Progress', () => {
const { container } = render(<Progress percent={70} steps={5} />);
expect(container.firstChild).toMatchSnapshot();
});
it('should show inner info position', () => {
const { container: wrapper, rerender } = render(
<Progress
percent={0}
percentPosition={{ align: 'center', type: 'inner' }}
size={[200, 20]}
/>,
);
expect(
wrapper.querySelectorAll('.ant-progress-line-align-center.ant-progress-line-position-inner'),
).toHaveLength(1);
rerender(
<Progress
percent={100}
percentPosition={{ align: 'center', type: 'inner' }}
size={[400, 20]}
/>,
);
expect(wrapper.querySelectorAll('.ant-progress-text-inner')).toHaveLength(1);
rerender(<Progress percent={100} percentPosition={{ align: 'center', type: 'outer' }} />);
expect(wrapper.querySelectorAll('.ant-progress-layout-bottom')).toHaveLength(1);
});
it('render inner info position', () => {
const { container } = render(
<Progress
percent={100}
percentPosition={{ align: 'center', type: 'inner' }}
size={[400, 20]}
/>,
);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -0,0 +1,7 @@
## zh-CN
改变进度数值位置,可使用 `percentPosition` 调整,使进度条数值在进度条内部、外部或底部。
## en-US
Change the position of the progress value, you can use `percentPosition` to adjust it so that the progress bar value is inside, outside or at the bottom of the progress bar.

View File

@ -0,0 +1,33 @@
import React from 'react';
import { Flex, Progress } from 'antd';
const App: React.FC = () => (
<Flex gap="small" vertical>
<Progress
percent={0}
percentPosition={{ align: 'center', type: 'inner' }}
size={[200, 20]}
strokeColor="#E6F4FF"
/>
<Progress percent={10} percentPosition={{ align: 'center', type: 'inner' }} size={[300, 20]} />
<Progress
percent={50}
percentPosition={{ align: 'start', type: 'inner' }}
size={[300, 20]}
strokeColor="#B7EB8F"
/>
<Progress
percent={60}
percentPosition={{ align: 'end', type: 'inner' }}
size={[300, 20]}
strokeColor="#001342"
/>
<Progress percent={100} percentPosition={{ align: 'center', type: 'inner' }} size={[400, 20]} />
<Progress percent={60} percentPosition={{ align: 'start', type: 'outer' }} />
<Progress percent={100} percentPosition={{ align: 'start', type: 'outer' }} />
<Progress percent={60} percentPosition={{ align: 'center', type: 'outer' }} size="small" />
<Progress percent={100} percentPosition={{ align: 'center', type: 'outer' }} />
</Flex>
);
export default App;

View File

@ -33,6 +33,7 @@ If it will take a long time to complete an operation, you can use `Progress` to
<code src="./demo/steps.tsx">Progress bar with steps</code>
<code src="./demo/circle-steps.tsx" version="5.16.0">Circular progress bar whit steps</code>
<code src="./demo/size.tsx">Progress size</code>
<code src="./demo/info-position.tsx" version="5.18.0">Change progress value position</code>
## API
@ -51,7 +52,7 @@ Properties that shared by all types.
| success | Configs of successfully progress bar | { percent: number, strokeColor: string } | - | - |
| trailColor | The color of unfilled part | string | - | - |
| type | To set the type, options: `line` `circle` `dashboard` | string | `line` |
| size | Progress size | number \| \[number \| string, number] \| "small" \| "default" | "default" | v5.3.0 |
| size | Progress size | number \| \[number \| string, number] \| { width: number, height: number } \| "small" \| "default" | "default" | 5.3.0, Object: 5.18.0 |
### `type="line"`
@ -59,6 +60,7 @@ Properties that shared by all types.
| --- | --- | --- | --- | --- |
| steps | The total step count | number | - | - |
| strokeColor | The color of progress bar, render `linear-gradient` when passing an object, could accept `string[]` when has `steps`. | string \| string[] \| { from: string; to: string; direction: string } | - | 4.21.0: `string[]` |
| percentPosition | Progress value position, passed in object, `align` indicates the horizontal position of the value, `type` indicates whether the value is inside or outside the progress bar | { align: string; type: string } | { align: \"end\", type: \"outer\" } | 5.18.0 |
### `type="circle"`

View File

@ -34,6 +34,7 @@ demo:
<code src="./demo/steps.tsx">步骤进度条</code>
<code src="./demo/circle-steps.tsx" version="5.16.0">步骤进度圈</code>
<code src="./demo/size.tsx">尺寸</code>
<code src="./demo/info-position.tsx" version="5.18.0">改变进度数值位置</code>
## API
@ -52,7 +53,7 @@ demo:
| success | 成功进度条相关配置 | { percent: number, strokeColor: string } | - | - |
| trailColor | 未完成的分段的颜色 | string | - | - |
| type | 类型,可选 `line` `circle` `dashboard` | string | `line` | - |
| size | 进度条的尺寸 | number \| \[number \| string, number] \| "small" \| "default" | "default" | v5.3.0 |
| size | 进度条的尺寸 | number \| \[number \| string, number] \| { width: number, height: number } \| "small" \| "default" | "default" | 5.3.0, Object: 5.18.0 |
### `type="line"`
@ -60,6 +61,7 @@ demo:
| --- | --- | --- | --- | --- |
| steps | 进度条总共步数 | number | - | - |
| strokeColor | 进度条的色彩,传入 object 时为渐变。当有 `steps` 时支持传入一个数组。 | string \| string[] \| { from: string; to: string; direction: string } | - | 4.21.0: `string[]` |
| percentPosition | 进度数值位置,传入对象,`align` 表示数值的水平位置,`type` 表示数值在进度条内部还是外部 | { align: string; type: string } | { align: \"end\", type: \"outer\" } | 5.18.0 |
### `type="circle"`

View File

@ -14,15 +14,19 @@ import Line from './Line';
import Steps from './Steps';
import useStyle from './style';
import { getSize, getSuccessPercent, validProgress } from './utils';
import { TinyColor } from '@ctrl/tinycolor';
export const ProgressTypes = ['line', 'circle', 'dashboard'] as const;
export type ProgressType = (typeof ProgressTypes)[number];
const ProgressStatuses = ['normal', 'exception', 'active', 'success'] as const;
export type ProgressSize = 'default' | 'small';
export type StringGradients = Record<string, string>;
type FromToGradients = { from: string; to: string };
export type ProgressGradient = { direction?: string } & (StringGradients | FromToGradients);
export interface PercentPositionType {
align?: 'start' | 'center' | 'end';
type?: 'inner' | 'outer';
}
export interface SuccessProps {
percent?: number;
@ -52,10 +56,11 @@ export interface ProgressProps extends ProgressAriaProps {
style?: React.CSSProperties;
gapDegree?: number;
gapPosition?: 'top' | 'bottom' | 'left' | 'right';
size?: number | [number | string, number] | ProgressSize;
size?: number | [number | string, number] | ProgressSize | { width?: number; height?: number };
steps?: number | { count: number; gap: number };
/** @deprecated Use `success` instead */
successPercent?: number;
percentPosition?: PercentPositionType;
children?: React.ReactNode;
}
@ -73,9 +78,25 @@ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>((props, ref) =>
status,
format,
style,
percentPosition = {},
...restProps
} = props;
const { align: infoAlign = 'end', type: infoPosition = 'outer' } = percentPosition;
const strokeColorNotArray = Array.isArray(strokeColor) ? strokeColor[0] : strokeColor;
const strokeColorNotGradient =
typeof strokeColor === 'string' || Array.isArray(strokeColor) ? strokeColor : undefined;
const strokeColorIsBright = React.useMemo(() => {
if (strokeColorNotArray) {
const color =
typeof strokeColorNotArray === 'string'
? strokeColorNotArray
: Object.values(strokeColorNotArray)[0];
return new TinyColor(color).isLight();
}
return false;
}, [strokeColor]);
const percentNumber = React.useMemo<number>(() => {
const successPercent = getSuccessPercent(props);
return parseInt(
@ -99,6 +120,8 @@ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>((props, ref) =>
const prefixCls = getPrefixCls('progress', customizePrefixCls);
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
const isLineType = type === 'line';
const isPureLineType = isLineType && !steps;
const progressInfo = React.useMemo<React.ReactNode>(() => {
if (!showInfo) {
return null;
@ -106,8 +129,12 @@ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>((props, ref) =>
const successPercent = getSuccessPercent(props);
let text: React.ReactNode;
const textFormatter = format || ((number) => `${number}%`);
const isLineType = type === 'line';
if (format || (progressStatus !== 'exception' && progressStatus !== 'success')) {
const isBrightInnerColor = isLineType && strokeColorIsBright && infoPosition === 'inner';
if (
infoPosition === 'inner' ||
format ||
(progressStatus !== 'exception' && progressStatus !== 'success')
) {
text = textFormatter(validProgress(percent), validProgress(successPercent));
} else if (progressStatus === 'exception') {
text = isLineType ? <CloseCircleFilled /> : <CloseOutlined />;
@ -116,7 +143,14 @@ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>((props, ref) =>
}
return (
<span className={`${prefixCls}-text`} title={typeof text === 'string' ? text : undefined}>
<span
className={classNames(`${prefixCls}-text`, {
[`${prefixCls}-text-bright`]: isBrightInnerColor,
[`${prefixCls}-text-${infoAlign}`]: isPureLineType,
[`${prefixCls}-text-${infoPosition}`]: isPureLineType,
})}
title={typeof text === 'string' ? text : undefined}
>
{text}
</span>
);
@ -128,12 +162,20 @@ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>((props, ref) =>
warning.deprecated(!('successPercent' in props), 'successPercent', 'success.percent');
warning.deprecated(!('width' in props), 'width', 'size');
if ((type === 'circle' || type === 'dashboard') && Array.isArray(size)) {
warning(
false,
'usage',
'Type "circle" and "dashboard" do not accept array as `size`, please use number or preset size instead.',
);
if (type === 'circle' || type === 'dashboard') {
if (Array.isArray(size)) {
warning(
false,
'usage',
'Type "circle" and "dashboard" do not accept array as `size`, please use number or preset size instead.',
);
} else if (typeof size === 'object') {
warning(
false,
'usage',
'Type "circle" and "dashboard" do not accept object as `size`, please use number or preset size instead.',
);
}
}
if (props.success && 'progress' in props.success) {
@ -141,9 +183,6 @@ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>((props, ref) =>
}
}
const strokeColorNotArray = Array.isArray(strokeColor) ? strokeColor[0] : strokeColor;
const strokeColorNotGradient =
typeof strokeColor === 'string' || Array.isArray(strokeColor) ? strokeColor : undefined;
let progress: React.ReactNode;
// Render progress shape
if (type === 'line') {
@ -162,6 +201,10 @@ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>((props, ref) =>
strokeColor={strokeColorNotArray}
prefixCls={prefixCls}
direction={direction}
percentPosition={{
align: infoAlign,
type: infoPosition,
}}
>
{progressInfo}
</Line>
@ -185,7 +228,9 @@ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>((props, ref) =>
{
[`${prefixCls}-${(type === 'dashboard' && 'circle') || type}`]: type !== 'line',
[`${prefixCls}-inline-circle`]: type === 'circle' && getSize(size, 'circle')[0] <= 20,
[`${prefixCls}-line`]: !steps && type === 'line',
[`${prefixCls}-line`]: isPureLineType,
[`${prefixCls}-line-align-${infoAlign}`]: isPureLineType,
[`${prefixCls}-line-position-${infoPosition}`]: isPureLineType,
[`${prefixCls}-steps`]: steps,
[`${prefixCls}-show-info`]: showInfo,
[`${prefixCls}-${size}`]: typeof size === 'string',

View File

@ -1,5 +1,5 @@
import type { CSSObject } from '@ant-design/cssinjs';
import { Keyframes, unit } from '@ant-design/cssinjs';
import { Keyframes } from '@ant-design/cssinjs';
import { resetComponent } from '../../style';
import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal';
@ -85,21 +85,16 @@ const genBaseStyle: GenerateStyle<ProgressToken> = (token) => {
},
[`${progressCls}-outer`]: {
display: 'inline-block',
display: 'inline-flex',
alignItems: 'center',
width: '100%',
},
[`&${progressCls}-show-info`]: {
[`${progressCls}-outer`]: {
marginInlineEnd: `calc(-2em - ${unit(token.marginXS)})`,
paddingInlineEnd: `calc(2em + ${unit(token.paddingXS)})`,
},
},
[`${progressCls}-inner`]: {
position: 'relative',
display: 'inline-block',
width: '100%',
flex: 1,
overflow: 'hidden',
verticalAlign: 'middle',
backgroundColor: token.remainingColor,
@ -119,9 +114,20 @@ const genBaseStyle: GenerateStyle<ProgressToken> = (token) => {
transition: `all ${token.motionDurationSlow} ${token.motionEaseInOutCirc}`,
},
[`${progressCls}-layout-bottom`]: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
[`${progressCls}-text`]: {
width: 'max-content',
marginInlineStart: 0,
marginTop: token.marginXXS,
},
},
[`${progressCls}-bg`]: {
overflow: 'hidden',
'&::after': {
content: '""',
background: {
@ -132,6 +138,18 @@ const genBaseStyle: GenerateStyle<ProgressToken> = (token) => {
width: `calc(1 / var(${Percent}) * 100%)`,
display: 'block',
},
[`&${progressCls}-bg-inner`]: {
minWidth: 'max-content',
'&::after': {
content: 'none',
},
[`${progressCls}-text-inner`]: {
color: token.colorWhite,
[`&${progressCls}-text-bright`]: {
color: 'rgba(0, 0, 0, 0.45)',
},
},
},
},
[`${progressCls}-success-bg`]: {
@ -143,10 +161,10 @@ const genBaseStyle: GenerateStyle<ProgressToken> = (token) => {
[`${progressCls}-text`]: {
display: 'inline-block',
width: '2em',
marginInlineStart: token.marginXS,
color: token.colorText,
lineHeight: 1,
width: '2em',
whiteSpace: 'nowrap',
textAlign: 'start',
verticalAlign: 'middle',
@ -154,6 +172,30 @@ const genBaseStyle: GenerateStyle<ProgressToken> = (token) => {
[iconPrefixCls]: {
fontSize: token.fontSize,
},
[`&${progressCls}-text-outer`]: {
width: 'max-content',
},
[`&${progressCls}-text-outer${progressCls}-text-start`]: {
width: 'max-content',
marginInlineStart: 0,
marginInlineEnd: token.marginXS,
},
},
[`${progressCls}-text-inner`]: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%',
marginInlineStart: 0,
padding: `0 ${token.paddingXXS}`,
[`&${progressCls}-text-start`]: {
justifyContent: 'start',
},
[`&${progressCls}-text-end`]: {
justifyContent: 'end',
},
},
[`&${progressCls}-status-active`]: {

View File

@ -57,8 +57,12 @@ export const getSize = (
} else if (typeof size === 'number') {
[width, height] = [size, size];
} else {
[width = 14, height = 8] = size as [number, number];
[width = 14, height = 8] = (Array.isArray(size) ? size : [size.width, size.height]) as [
number,
number,
];
}
width *= steps;
} else if (type === 'line') {
const strokeWidth = extra?.strokeWidth;
@ -67,14 +71,17 @@ export const getSize = (
} else if (typeof size === 'number') {
[width, height] = [size, size];
} else {
[width = -1, height = 8] = size as [number, number];
[width = -1, height = 8] = (Array.isArray(size) ? size : [size.width, size.height]) as [
number,
number,
];
}
} else if (type === 'circle' || type === 'dashboard') {
if (typeof size === 'string' || typeof size === 'undefined') {
[width, height] = size === 'small' ? [60, 60] : [120, 120];
} else if (typeof size === 'number') {
[width, height] = [size, size];
} else {
} else if (Array.isArray(size)) {
width = (size[0] ?? size[1] ?? 120) as number;
height = (size[0] ?? size[1] ?? 120) as number;
}

View File

@ -347,20 +347,24 @@ exports[`renders components/qr-code/demo/status.tsx extend context correctly 1`]
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
</div>

View File

@ -299,20 +299,24 @@ exports[`renders components/qr-code/demo/status.tsx correctly 1`] = `
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
</div>

View File

@ -40,6 +40,7 @@ Common props ref[Common props](/docs/react/common-props)
| count | Star count | number | 5 | |
| defaultValue | The default value | number | 0 | |
| disabled | If read only, unable to interact | boolean | false | |
| keyboard | Support keyboard operation | boolean | true | 5.18.0 |
| style | The custom style object of rate | CSSProperties | - | |
| tooltips | Customize tooltip by each character | string\[] | - | |
| value | The current value | number | - | |

View File

@ -41,6 +41,7 @@ demo:
| count | star 总数 | number | 5 | |
| defaultValue | 默认值 | number | 0 | |
| disabled | 只读,无法进行交互 | boolean | false | |
| keyboard | 支持使用键盘操作 | boolean | true | 5.18.0 |
| style | 自定义样式对象 | CSSProperties | - | |
| tooltips | 自定义每项的提示信息 | string\[] | - | |
| value | 当前数,受控值 | number | - | |

View File

@ -0,0 +1,30 @@
import * as React from 'react';
import classNames from 'classnames';
import Progress from './Progress';
export interface IndicatorProps {
prefixCls: string;
percent?: number;
}
export default function Looper(props: IndicatorProps) {
const { prefixCls, percent = 0 } = props;
const dotClassName = `${prefixCls}-dot`;
const holderClassName = `${dotClassName}-holder`;
const hideClassName = `${holderClassName}-hidden`;
// ===================== Render =====================
return (
<>
<span className={classNames(holderClassName, percent > 0 && hideClassName)}>
<span className={classNames(dotClassName, `${prefixCls}-dot-spin`)}>
{[1, 2, 3, 4].map((i) => (
<i className={`${prefixCls}-dot-item`} key={i} />
))}
</span>
</span>
<Progress prefixCls={prefixCls} percent={percent} />
</>
);
}

View File

@ -0,0 +1,73 @@
import * as React from 'react';
import classNames from 'classnames';
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
export interface ProgressProps {
prefixCls: string;
percent: number;
}
export default function Progress({ percent, prefixCls }: ProgressProps) {
const dotClassName = `${prefixCls}-dot`;
const holderClassName = `${dotClassName}-holder`;
const hideClassName = `${holderClassName}-hidden`;
const [render, setRender] = React.useState(false);
// ==================== Visible =====================
useLayoutEffect(() => {
if (percent !== 0) {
setRender(true);
}
}, [percent !== 0]);
// ==================== Progress ====================
const safePtg = Math.max(Math.min(percent, 100), 0);
const viewSize = 100;
const borderWidth = viewSize / 5;
const radius = viewSize / 2 - borderWidth / 2;
const circumference = radius * 2 * Math.PI;
const renderCircle = (circleClassName?: string, style?: React.CSSProperties) => (
<circle
className={classNames(circleClassName, `${dotClassName}-circle`)}
r={radius}
cx="50"
cy="50"
strokeWidth={borderWidth}
style={style}
/>
);
// ===================== Render =====================
if (!render) {
return null;
}
return (
<span
className={classNames(
holderClassName,
`${dotClassName}-progress`,
safePtg <= 0 && hideClassName,
)}
>
<svg
viewBox={`0 0 ${viewSize} ${viewSize}`}
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={safePtg}
>
{renderCircle(`${dotClassName}-circle-bg`)}
{renderCircle('', {
strokeDasharray: `${(circumference * safePtg) / 100} ${
(circumference * (100 - safePtg)) / 100
}`,
strokeDashoffset: `${circumference / 4}`,
})}
</svg>
</span>
);
}

View File

@ -0,0 +1,24 @@
import * as React from 'react';
import classNames from 'classnames';
import { cloneElement } from '../../_util/reactNode';
import Looper from './Looper';
export interface IndicatorProps {
prefixCls: string;
indicator?: React.ReactNode;
percent?: number;
}
export default function Indicator(props: IndicatorProps) {
const { prefixCls, indicator, percent } = props;
const dotClassName = `${prefixCls}-dot`;
if (indicator && React.isValidElement(indicator)) {
return cloneElement(indicator, {
className: classNames(indicator.props.className, dotClassName),
});
}
return <Looper prefixCls={prefixCls} percent={percent} />;
}

View File

@ -7,20 +7,24 @@ exports[`renders components/spin/demo/basic.tsx extend context correctly 1`] = `
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
`;
@ -125,67 +129,44 @@ Array [
type="button"
>
<span>
Show fullscreen for 3s
Show fullscreen
</span>
</button>,
<div
aria-busy="false"
aria-live="polite"
class="ant-spin ant-spin-fullscreen"
class="ant-spin-fullscreen"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
<div
aria-busy="false"
aria-live="polite"
class="ant-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
<span
class="ant-spin-dot-holder"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
</div>,
]
`;
exports[`renders components/spin/demo/fullscreen.tsx extend context correctly 2`] = `[]`;
exports[`renders components/spin/demo/inside.tsx extend context correctly 1`] = `
<div
class="example"
>
<div
aria-busy="true"
aria-live="polite"
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</div>
</div>
`;
exports[`renders components/spin/demo/inside.tsx extend context correctly 2`] = `[]`;
exports[`renders components/spin/demo/nested.tsx extend context correctly 1`] = `
Array [
<div
@ -246,6 +227,214 @@ Array [
exports[`renders components/spin/demo/nested.tsx extend context correctly 2`] = `[]`;
exports[`renders components/spin/demo/percent.tsx extend context correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
>
<div
class="ant-switch-handle"
/>
<span
class="ant-switch-inner"
>
<span
class="ant-switch-inner-checked"
>
Auto
</span>
<span
class="ant-switch-inner-unchecked"
>
Auto
</span>
</span>
</button>
</div>
<div
class="ant-space-item"
>
<div
aria-busy="true"
aria-live="polite"
class="ant-spin ant-spin-sm ant-spin-spinning"
>
<span
class="ant-spin-dot-holder"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
<span
class="ant-spin-dot-holder ant-spin-dot-progress ant-spin-dot-holder-hidden"
>
<svg
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="0"
role="progressbar"
viewBox="0 0 100 100"
>
<circle
class="ant-spin-dot-circle-bg ant-spin-dot-circle"
cx="50"
cy="50"
r="40"
stroke-width="20"
/>
<circle
class="ant-spin-dot-circle"
cx="50"
cy="50"
r="40"
stroke-width="20"
style="stroke-dasharray: 0 251.32741228718342; stroke-dashoffset: 62.83185307179586;"
/>
</svg>
</span>
</div>
</div>
<div
class="ant-space-item"
>
<div
aria-busy="true"
aria-live="polite"
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot-holder"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
<span
class="ant-spin-dot-holder ant-spin-dot-progress ant-spin-dot-holder-hidden"
>
<svg
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="0"
role="progressbar"
viewBox="0 0 100 100"
>
<circle
class="ant-spin-dot-circle-bg ant-spin-dot-circle"
cx="50"
cy="50"
r="40"
stroke-width="20"
/>
<circle
class="ant-spin-dot-circle"
cx="50"
cy="50"
r="40"
stroke-width="20"
style="stroke-dasharray: 0 251.32741228718342; stroke-dashoffset: 62.83185307179586;"
/>
</svg>
</span>
</div>
</div>
<div
class="ant-space-item"
>
<div
aria-busy="true"
aria-live="polite"
class="ant-spin ant-spin-lg ant-spin-spinning"
>
<span
class="ant-spin-dot-holder"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
<span
class="ant-spin-dot-holder ant-spin-dot-progress ant-spin-dot-holder-hidden"
>
<svg
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="0"
role="progressbar"
viewBox="0 0 100 100"
>
<circle
class="ant-spin-dot-circle-bg ant-spin-dot-circle"
cx="50"
cy="50"
r="40"
stroke-width="20"
/>
<circle
class="ant-spin-dot-circle"
cx="50"
cy="50"
r="40"
stroke-width="20"
style="stroke-dasharray: 0 251.32741228718342; stroke-dashoffset: 62.83185307179586;"
/>
</svg>
</span>
</div>
</div>
</div>
`;
exports[`renders components/spin/demo/percent.tsx extend context correctly 2`] = `[]`;
exports[`renders components/spin/demo/size.tsx extend context correctly 1`] = `
<div
class="ant-flex ant-flex-align-center ant-flex-gap-middle"
@ -256,20 +445,24 @@ exports[`renders components/spin/demo/size.tsx extend context correctly 1`] = `
class="ant-spin ant-spin-sm ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
<div
@ -278,20 +471,24 @@ exports[`renders components/spin/demo/size.tsx extend context correctly 1`] = `
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
<div
@ -300,20 +497,24 @@ exports[`renders components/spin/demo/size.tsx extend context correctly 1`] = `
class="ant-spin ant-spin-lg ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
</div>
@ -338,20 +539,24 @@ exports[`renders components/spin/demo/tip.tsx extend context correctly 1`] = `
class="ant-spin ant-spin-sm ant-spin-spinning ant-spin-show-text"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
<div
class="ant-spin-text"
@ -378,20 +583,24 @@ exports[`renders components/spin/demo/tip.tsx extend context correctly 1`] = `
class="ant-spin ant-spin-spinning ant-spin-show-text"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
<div
class="ant-spin-text"
@ -418,20 +627,24 @@ exports[`renders components/spin/demo/tip.tsx extend context correctly 1`] = `
class="ant-spin ant-spin-lg ant-spin-spinning ant-spin-show-text"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
<div
class="ant-spin-text"
@ -459,20 +672,24 @@ exports[`renders components/spin/demo/tip.tsx extend context correctly 1`] = `
class="ant-spin ant-spin-spinning ant-spin-show-text"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
<div
class="ant-spin-text"

View File

@ -7,20 +7,24 @@ exports[`renders components/spin/demo/basic.tsx correctly 1`] = `
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
`;
@ -119,63 +123,42 @@ Array [
type="button"
>
<span>
Show fullscreen for 3s
Show fullscreen
</span>
</button>,
<div
aria-busy="false"
aria-live="polite"
class="ant-spin ant-spin-fullscreen"
class="ant-spin-fullscreen"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
<div
aria-busy="false"
aria-live="polite"
class="ant-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
<span
class="ant-spin-dot-holder"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
</div>,
]
`;
exports[`renders components/spin/demo/inside.tsx correctly 1`] = `
<div
class="example"
>
<div
aria-busy="true"
aria-live="polite"
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</div>
</div>
`;
exports[`renders components/spin/demo/nested.tsx correctly 1`] = `
Array [
<div
@ -234,6 +217,131 @@ Array [
]
`;
exports[`renders components/spin/demo/percent.tsx correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
>
<div
class="ant-switch-handle"
/>
<span
class="ant-switch-inner"
>
<span
class="ant-switch-inner-checked"
>
Auto
</span>
<span
class="ant-switch-inner-unchecked"
>
Auto
</span>
</span>
</button>
</div>
<div
class="ant-space-item"
>
<div
aria-busy="true"
aria-live="polite"
class="ant-spin ant-spin-sm ant-spin-spinning"
>
<span
class="ant-spin-dot-holder"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
</div>
<div
class="ant-space-item"
>
<div
aria-busy="true"
aria-live="polite"
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot-holder"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
</div>
<div
class="ant-space-item"
>
<div
aria-busy="true"
aria-live="polite"
class="ant-spin ant-spin-lg ant-spin-spinning"
>
<span
class="ant-spin-dot-holder"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
</div>
</div>
`;
exports[`renders components/spin/demo/size.tsx correctly 1`] = `
<div
class="ant-flex ant-flex-align-center ant-flex-gap-middle"
@ -244,20 +352,24 @@ exports[`renders components/spin/demo/size.tsx correctly 1`] = `
class="ant-spin ant-spin-sm ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
<div
@ -266,20 +378,24 @@ exports[`renders components/spin/demo/size.tsx correctly 1`] = `
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
<div
@ -288,20 +404,24 @@ exports[`renders components/spin/demo/size.tsx correctly 1`] = `
class="ant-spin ant-spin-lg ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
</div>
@ -324,20 +444,24 @@ exports[`renders components/spin/demo/tip.tsx correctly 1`] = `
class="ant-spin ant-spin-sm ant-spin-spinning ant-spin-show-text"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
<div
class="ant-spin-text"
@ -364,20 +488,24 @@ exports[`renders components/spin/demo/tip.tsx correctly 1`] = `
class="ant-spin ant-spin-spinning ant-spin-show-text"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
<div
class="ant-spin-text"
@ -404,20 +532,24 @@ exports[`renders components/spin/demo/tip.tsx correctly 1`] = `
class="ant-spin ant-spin-lg ant-spin-spinning ant-spin-show-text"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
<div
class="ant-spin-text"
@ -445,20 +577,24 @@ exports[`renders components/spin/demo/tip.tsx correctly 1`] = `
class="ant-spin ant-spin-spinning ant-spin-show-text"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
<div
class="ant-spin-text"

View File

@ -5,7 +5,28 @@ exports[`Spin if indicator set null should not be render default indicator 1`] =
aria-busy="true"
aria-live="polite"
class="ant-spin ant-spin-spinning"
/>
>
<span
class="ant-spin-dot-holder"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
`;
exports[`Spin rtl render component should be rendered correctly in RTL direction 1`] = `
@ -15,20 +36,24 @@ exports[`Spin rtl render component should be rendered correctly in RTL direction
class="ant-spin ant-spin-spinning ant-spin-rtl"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
class="ant-spin-dot-holder"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</span>
</div>
`;

View File

@ -1,12 +1,20 @@
import React from 'react';
import { render } from '@testing-library/react';
import Spin from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { waitFakeTimer } from '../../../tests/utils';
import { act, render, waitFakeTimer } from '../../../tests/utils';
describe('Spin', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllTimers();
jest.useRealTimers();
});
mountTest(Spin);
rtlTest(Spin);
@ -89,7 +97,21 @@ describe('Spin', () => {
it('right style when fullscreen', () => {
const { container } = render(<Spin fullscreen spinning />);
const element = container.querySelector<HTMLDivElement>('.ant-spin.ant-spin-fullscreen');
const element = container.querySelector<HTMLDivElement>('.ant-spin-fullscreen');
expect(element).not.toHaveStyle({ pointerEvents: 'none' });
});
it('percent support auto', () => {
const { container } = render(<Spin percent="auto" />);
act(() => {
jest.advanceTimersByTime(100000);
});
const nowPTG = Number(
container.querySelector('[role="progressbar"]')?.getAttribute('aria-valuenow'),
);
expect(nowPTG).toBeGreaterThanOrEqual(1);
});
});

View File

@ -2,19 +2,29 @@ import React from 'react';
import { Button, Spin } from 'antd';
const App: React.FC = () => {
const [spinning, setSpinning] = React.useState<boolean>(false);
const [spinning, setSpinning] = React.useState(false);
const [percent, setPercent] = React.useState(0);
const showLoader = () => {
setSpinning(true);
setTimeout(() => {
setSpinning(false);
}, 3000);
let ptg = -10;
const interval = setInterval(() => {
ptg += 5;
setPercent(ptg);
if (ptg > 120) {
clearInterval(interval);
setSpinning(false);
setPercent(0);
}
}, 100);
};
return (
<>
<Button onClick={showLoader}>Show fullscreen for 3s</Button>
<Spin spinning={spinning} fullscreen />
<Button onClick={showLoader}>Show fullscreen</Button>
<Spin spinning={spinning} percent={percent} fullscreen />
</>
);
};

View File

@ -1,24 +0,0 @@
## zh-CN
放入一个容器中。
## en-US
Spin in a container.
```css
.example {
margin: 20px 0;
margin-bottom: 20px;
padding: 30px 50px;
text-align: center;
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
```
<style>
.example {
background: rgba(255,255,255,0.08);
}
</style>

View File

@ -1,10 +0,0 @@
import React from 'react';
import { Spin } from 'antd';
const App: React.FC = () => (
<div className="example">
<Spin />
</div>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
展示进度,当设置 `percent="auto"` 时会预估一个永远不会停止的进度条。
## en-US
Show the progress. When `percent="auto"` is set, an indeterminate progress will be displayed.

View File

@ -0,0 +1,40 @@
import React from 'react';
import { Space, Spin, Switch } from 'antd';
const App: React.FC = () => {
const [auto, setAuto] = React.useState(false);
const [percent, setPercent] = React.useState(-50);
React.useEffect(() => {
const timeout = setTimeout(() => {
setPercent((v) => {
const nextPercent = v + 5;
return nextPercent > 150 ? -50 : nextPercent;
});
}, 100);
return () => {
clearTimeout(timeout);
};
}, [percent]);
const mergedPercent = auto ? 'auto' : percent;
return (
<Space>
<Switch
checkedChildren="Auto"
unCheckedChildren="Auto"
checked={auto}
onChange={() => {
setAuto(!auto);
setPercent(-50);
}}
/>
<Spin percent={mergedPercent} size="small" />
<Spin percent={mergedPercent} />
<Spin percent={mergedPercent} size="large" />
</Space>
);
};
export default App;

Some files were not shown because too many files have changed in this diff Show More