chore: auto merge branches (#36910)

chore: Next merge feature
This commit is contained in:
github-actions[bot] 2022-08-05 08:24:44 +00:00 committed by GitHub
commit c9ea74299a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 2428 additions and 1625 deletions

View File

@ -0,0 +1,23 @@
name: Discussions
on:
discussion:
types: [created]
permissions:
contents: read
jobs:
discussion-create:
permissions:
contents: read # for visiky/dingtalk-release-notify to get latest release
runs-on: ubuntu-latest
steps:
- name: send to dingtalk
uses: visiky/dingtalk-release-notify@main
with:
DING_TALK_TOKEN: ${{ secrets.DINGDING_BOT_TOKEN }}
notify_title: '🔥 @${{ github.event.discussion.user.login }} 创建了讨论:${{ github.event.discussion.title }}'
notify_body: '### 🔥 @${{ github.event.discussion.user.login }} 创建了讨论:[${{ github.event.discussion.title }}](${{ github.event.discussion.html_url }}) <hr /> ![](https://gw.alipayobjects.com/zos/antfincdn/5Cl2G7JjF/jieping2022-03-20%252520xiawu11.06.04.png)'
notify_footer: '> 💬 欢迎前往 GitHub 进行讨论,社区可能需要你的帮助。'
at_all: false # whether to ding everybody

View File

@ -89,4 +89,18 @@ describe('AutoComplete', () => {
); );
expect(container.querySelector('input').classList.contains('custom')).toBeTruthy(); expect(container.querySelector('input').classList.contains('custom')).toBeTruthy();
}); });
it('should show warning when use dropdownClassName', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
render(
<AutoComplete dropdownClassName="myCustomClassName">
<AutoComplete.Option value="111">111</AutoComplete.Option>
<AutoComplete.Option value="222">222</AutoComplete.Option>
</AutoComplete>,
);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: AutoComplete] `dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
);
errorSpy.mockRestore();
});
}); });

View File

@ -66,7 +66,7 @@ const options = [
const App: React.FC = () => ( const App: React.FC = () => (
<AutoComplete <AutoComplete
dropdownClassName="certain-category-search-dropdown" popupClassName="certain-category-search-dropdown"
dropdownMatchSelectWidth={500} dropdownMatchSelectWidth={500}
style={{ width: 250 }} style={{ width: 250 }}
options={options} options={options}

View File

@ -31,7 +31,7 @@ The differences with Select are:
| defaultOpen | Initial open state of dropdown | boolean | - | | | defaultOpen | Initial open state of dropdown | boolean | - | |
| defaultValue | Initial selected option | string | - | | | defaultValue | Initial selected option | string | - | |
| disabled | Whether disabled select | boolean | false | | | disabled | Whether disabled select | boolean | false | |
| dropdownClassName | The className of dropdown menu | string | - | | | popupClassName | The className of dropdown menu | string | - | 4.23.0 |
| dropdownMatchSelectWidth | Determine whether the dropdown menu and the select input are the same width. Default set `min-width` same as input. Will ignore when value less than select width. `false` will disable virtual scroll | boolean \| number | true | | | dropdownMatchSelectWidth | Determine whether the dropdown menu and the select input are the same width. Default set `min-width` same as input. Will ignore when value less than select width. `false` will disable virtual scroll | boolean \| number | true | |
| filterOption | If true, filter options by input, if function, filter options against it. The function will receive two arguments, `inputValue` and `option`, if the function returns true, the option will be included in the filtered set; Otherwise, it will be excluded | boolean \| function(inputValue, option) | true | | | filterOption | If true, filter options by input, if function, filter options against it. The function will receive two arguments, `inputValue` and `option`, if the function returns true, the option will be included in the filtered set; Otherwise, it will be excluded | boolean \| function(inputValue, option) | true | |
| notFoundContent | Specify content to show when no result matches | string | `Not Found` | | | notFoundContent | Specify content to show when no result matches | string | `Not Found` | |

View File

@ -41,6 +41,12 @@ export interface AutoCompleteProps<
> { > {
dataSource?: DataSourceItemType[]; dataSource?: DataSourceItemType[];
status?: InputStatus; status?: InputStatus;
/**
* @deprecated `dropdownClassName` is deprecated which will be removed in next major version.
* Please use `popupClassName` instead.
*/
dropdownClassName?: string;
popupClassName?: string;
} }
function isSelectOptionOrSelectOptGroup(child: any): Boolean { function isSelectOptionOrSelectOptGroup(child: any): Boolean {
@ -51,7 +57,14 @@ const AutoComplete: React.ForwardRefRenderFunction<RefSelectProps, AutoCompleteP
props, props,
ref, ref,
) => { ) => {
const { prefixCls: customizePrefixCls, className, children, dataSource } = props; const {
prefixCls: customizePrefixCls,
className,
popupClassName,
dropdownClassName,
children,
dataSource,
} = props;
const childNodes: React.ReactElement[] = toArray(children); const childNodes: React.ReactElement[] = toArray(children);
// ============================= Input ============================= // ============================= Input =============================
@ -112,6 +125,12 @@ const AutoComplete: React.ForwardRefRenderFunction<RefSelectProps, AutoCompleteP
'`dataSource` is deprecated, please use `options` instead.', '`dataSource` is deprecated, please use `options` instead.',
); );
warning(
!dropdownClassName,
'AutoComplete',
'`dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
);
warning( warning(
!customizeInput || !('size' in props), !customizeInput || !('size' in props),
'AutoComplete', 'AutoComplete',
@ -128,6 +147,7 @@ const AutoComplete: React.ForwardRefRenderFunction<RefSelectProps, AutoCompleteP
ref={ref} ref={ref}
{...omit(props, ['dataSource'])} {...omit(props, ['dataSource'])}
prefixCls={prefixCls} prefixCls={prefixCls}
dropdownClassName={popupClassName || dropdownClassName}
className={classNames(`${prefixCls}-auto-complete`, className)} className={classNames(`${prefixCls}-auto-complete`, className)}
mode={Select.SECRET_COMBOBOX_MODE_DO_NOT_USE as any} mode={Select.SECRET_COMBOBOX_MODE_DO_NOT_USE as any}
{...{ {...{

View File

@ -32,7 +32,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/qtJm4yt45/AutoComplete.svg
| defaultOpen | 是否默认展开下拉菜单 | boolean | - | | | defaultOpen | 是否默认展开下拉菜单 | boolean | - | |
| defaultValue | 指定默认选中的条目 | string | - | | | defaultValue | 指定默认选中的条目 | string | - | |
| disabled | 是否禁用 | boolean | false | | | disabled | 是否禁用 | boolean | false | |
| dropdownClassName | 下拉菜单的 className 属性 | string | - | | | popupClassName | 下拉菜单的 className 属性 | string | - | 4.23.0 |
| dropdownMatchSelectWidth | 下拉菜单和选择器同宽。默认将设置 `min-width`当值小于选择框宽度时会被忽略。false 时会关闭虚拟滚动 | boolean \| number | true | | | dropdownMatchSelectWidth | 下拉菜单和选择器同宽。默认将设置 `min-width`当值小于选择框宽度时会被忽略。false 时会关闭虚拟滚动 | boolean \| number | true | |
| filterOption | 是否根据输入项进行筛选。当其为一个函数时,会接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 true反之则返回 false | boolean \| function(inputValue, option) | true | | | filterOption | 是否根据输入项进行筛选。当其为一个函数时,会接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 true反之则返回 false | boolean \| function(inputValue, option) | true | |
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。[示例](https://codesandbox.io/s/4j168r7jw0) | function(triggerNode) | () => document.body | | | getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。[示例](https://codesandbox.io/s/4j168r7jw0) | function(triggerNode) | () => document.body | |

View File

@ -540,7 +540,7 @@ describe('Cascader', () => {
it('popupClassName', () => { it('popupClassName', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const { container } = render( const { container } = render(
<Cascader open popupPlacement="bottomLeft" popupClassName="mock-cls" />, <Cascader open popupPlacement="bottomLeft" dropdownClassName="mock-cls" />,
); );
expect(container.querySelector('.mock-cls')).toBeTruthy(); expect(container.querySelector('.mock-cls')).toBeTruthy();
@ -549,7 +549,7 @@ describe('Cascader', () => {
expect(global.triggerProps.popupPlacement).toEqual('bottomLeft'); expect(global.triggerProps.popupPlacement).toEqual('bottomLeft');
expect(errorSpy).toHaveBeenCalledWith( expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Cascader] `popupClassName` is deprecated. Please use `dropdownClassName` instead.', 'Warning: [antd: Cascader] `dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
); );
errorSpy.mockRestore(); errorSpy.mockRestore();

View File

@ -30,7 +30,7 @@ Cascade selection box.
| defaultValue | Initial selected value | string\[] \| number\[] | \[] | | | defaultValue | Initial selected value | string\[] \| number\[] | \[] | |
| disabled | Whether disabled select | boolean | false | | | disabled | Whether disabled select | boolean | false | |
| displayRender | The render function of displaying selected options | (label, selectedOptions) => ReactNode | label => label.join(`/`) | `multiple`: 4.18.0 | | displayRender | The render function of displaying selected options | (label, selectedOptions) => ReactNode | label => label.join(`/`) | `multiple`: 4.18.0 |
| dropdownClassName | The additional className of popup overlay | string | - | 4.17.0 | | popupClassName | The additional className of popup overlay | string | - | 4.23.0 |
| dropdownRender | Customize dropdown content | (menus: ReactNode) => ReactNode | - | 4.4.0 | | dropdownRender | Customize dropdown content | (menus: ReactNode) => ReactNode | - | 4.4.0 |
| expandIcon | Customize the current item expand icon | ReactNode | - | 4.4.0 | | expandIcon | Customize the current item expand icon | ReactNode | - | 4.4.0 |
| expandTrigger | expand current item when click or hover, one of `click` `hover` | string | `click` | | | expandTrigger | expand current item when click or hover, one of `click` `hover` | string | `click` | |

View File

@ -112,6 +112,11 @@ export type CascaderProps<DataNodeType> = UnionCascaderProps & {
suffixIcon?: React.ReactNode; suffixIcon?: React.ReactNode;
options?: DataNodeType[]; options?: DataNodeType[];
status?: InputStatus; status?: InputStatus;
/**
* @deprecated `dropdownClassName` is deprecated which will be removed in next major
* version.Please use `popupClassName` instead.
*/
dropdownClassName?: string;
}; };
export interface CascaderRef { export interface CascaderRef {
@ -168,9 +173,9 @@ const Cascader = React.forwardRef((props: CascaderProps<any>, ref: React.Ref<Cas
// =================== Warning ===================== // =================== Warning =====================
warning( warning(
popupClassName === undefined, !dropdownClassName,
'Cascader', 'Cascader',
'`popupClassName` is deprecated. Please use `dropdownClassName` instead.', '`dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
); );
warning( warning(
@ -192,7 +197,7 @@ const Cascader = React.forwardRef((props: CascaderProps<any>, ref: React.Ref<Cas
// =================== Dropdown ==================== // =================== Dropdown ====================
const mergedDropdownClassName = classNames( const mergedDropdownClassName = classNames(
dropdownClassName || popupClassName, popupClassName || dropdownClassName,
`${cascaderPrefixCls}-dropdown`, `${cascaderPrefixCls}-dropdown`,
{ {
[`${cascaderPrefixCls}-dropdown-rtl`]: mergedDirection === 'rtl', [`${cascaderPrefixCls}-dropdown-rtl`]: mergedDirection === 'rtl',

View File

@ -31,7 +31,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg
| defaultValue | 默认的选中项 | string\[] \| number\[] | \[] | | | defaultValue | 默认的选中项 | string\[] \| number\[] | \[] | |
| disabled | 禁用 | boolean | false | | | disabled | 禁用 | boolean | false | |
| displayRender | 选择后展示的渲染函数 | (label, selectedOptions) => ReactNode | label => label.join(`/`) | `multiple`: 4.18.0 | | displayRender | 选择后展示的渲染函数 | (label, selectedOptions) => ReactNode | label => label.join(`/`) | `multiple`: 4.18.0 |
| dropdownClassName | 自定义浮层类名 | string | - | 4.17.0 | | popupClassName | 自定义浮层类名 | string | - | 4.23.0 |
| dropdownRender | 自定义下拉框内容 | (menus: ReactNode) => ReactNode | - | 4.4.0 | | dropdownRender | 自定义下拉框内容 | (menus: ReactNode) => ReactNode | - | 4.4.0 |
| expandIcon | 自定义次级菜单展开图标 | ReactNode | - | 4.4.0 | | expandIcon | 自定义次级菜单展开图标 | ReactNode | - | 4.4.0 |
| expandTrigger | 次级菜单的展开方式,可选 'click' 和 'hover' | string | `click` | | | expandTrigger | 次级菜单的展开方式,可选 'click' 和 'hover' | string | `click` | |

View File

@ -188,6 +188,20 @@ describe('DatePicker', () => {
).toBe(60); ).toBe(60);
}); });
it('DatePicker should show warning when use dropdownClassName', () => {
mount(<DatePicker dropdownClassName="myCustomClassName" />);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: DatePicker] `dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
);
});
it('RangePicker should show warning when use dropdownClassName', () => {
mount(<DatePicker.RangePicker dropdownClassName="myCustomClassName" />);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: RangePicker] `dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
);
});
it('DatePicker.RangePicker with defaultPickerValue and showTime', () => { it('DatePicker.RangePicker with defaultPickerValue and showTime', () => {
const startDate = dayjs('1982-02-12'); const startDate = dayjs('1982-02-12');
const endDate = dayjs('1982-02-22'); const endDate = dayjs('1982-02-22');

View File

@ -18,6 +18,7 @@ import { getMergedStatus, getStatusClassNames } from '../../_util/statusUtils';
import enUS from '../locale/en_US'; import enUS from '../locale/en_US';
import { getRangePlaceholder, transPlacement2DropdownAlign } from '../util'; import { getRangePlaceholder, transPlacement2DropdownAlign } from '../util';
import type { CommonPickerMethods, PickerComponentClass } from './interface'; import type { CommonPickerMethods, PickerComponentClass } from './interface';
import warning from '../../_util/warning';
import useStyle from '../style'; import useStyle from '../style';
@ -28,7 +29,14 @@ export default function generateRangePicker<DateType>(
const RangePicker = forwardRef< const RangePicker = forwardRef<
InternalRangePickerProps | CommonPickerMethods, InternalRangePickerProps | CommonPickerMethods,
RangePickerProps<DateType> RangePickerProps<DateType> & {
/**
* @deprecated `dropdownClassName` is deprecated which will be removed in next major
* version.Please use `popupClassName` instead.
*/
dropdownClassName: string;
popupClassName?: string;
}
>((props, ref) => { >((props, ref) => {
const { const {
prefixCls: customizePrefixCls, prefixCls: customizePrefixCls,
@ -39,8 +47,9 @@ export default function generateRangePicker<DateType>(
disabled: customDisabled, disabled: customDisabled,
bordered = true, bordered = true,
placeholder, placeholder,
status: customStatus, popupClassName,
dropdownClassName, dropdownClassName,
status: customStatus,
...restProps ...restProps
} = props; } = props;
@ -59,6 +68,12 @@ export default function generateRangePicker<DateType>(
...(picker === 'time' ? getTimeProps({ format, ...props, picker }) : {}), ...(picker === 'time' ? getTimeProps({ format, ...props, picker }) : {}),
}; };
warning(
!dropdownClassName,
'RangePicker',
'`dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
);
// ===================== Size ===================== // ===================== Size =====================
const size = React.useContext(SizeContext); const size = React.useContext(SizeContext);
const mergedSize = customizeSize || size; const mergedSize = customizeSize || size;
@ -128,7 +143,7 @@ export default function generateRangePicker<DateType>(
generateConfig={generateConfig} generateConfig={generateConfig}
components={Components} components={Components}
direction={direction} direction={direction}
dropdownClassName={classNames(hashId, dropdownClassName)} dropdownClassName={classNames(hashId, popupClassName || dropdownClassName)}
/> />
); );
}} }}

View File

@ -27,8 +27,13 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
type DatePickerProps = PickerProps<DateType> & { type DatePickerProps = PickerProps<DateType> & {
status?: InputStatus; status?: InputStatus;
hashId?: string; hashId?: string;
/**
* @deprecated `dropdownClassName` is deprecated which will be removed in next major
* version.Please use `popupClassName` instead.
*/
dropdownClassName?: string;
popupClassName?: string;
}; };
function getPicker<InnerPickerProps extends DatePickerProps>( function getPicker<InnerPickerProps extends DatePickerProps>(
picker?: PickerMode, picker?: PickerMode,
displayName?: string, displayName?: string,
@ -43,18 +48,13 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
bordered = true, bordered = true,
placement, placement,
placeholder, placeholder,
popupClassName,
dropdownClassName,
disabled: customDisabled, disabled: customDisabled,
status: customStatus, status: customStatus,
dropdownClassName,
...restProps ...restProps
} = props; } = props;
warning(
picker !== 'quarter',
displayName!,
`DatePicker.${displayName} is legacy usage. Please use DatePicker[picker='${picker}'] directly.`,
);
const { getPrefixCls, direction, getPopupContainer } = useContext(ConfigContext); const { getPrefixCls, direction, getPopupContainer } = useContext(ConfigContext);
const prefixCls = getPrefixCls('picker', customizePrefixCls); const prefixCls = getPrefixCls('picker', customizePrefixCls);
const innerRef = React.useRef<RCPicker<DateType>>(null); const innerRef = React.useRef<RCPicker<DateType>>(null);
@ -86,6 +86,18 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
}; };
const rootPrefixCls = getPrefixCls(); const rootPrefixCls = getPrefixCls();
// =================== Warning =====================
warning(
picker !== 'quarter',
displayName!,
`DatePicker.${displayName} is legacy usage. Please use DatePicker[picker='${picker}'] directly.`,
);
warning(
!dropdownClassName,
'DatePicker',
'`dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
);
// ===================== Size ===================== // ===================== Size =====================
const size = React.useContext(SizeContext); const size = React.useContext(SizeContext);
const mergedSize = customizeSize || size; const mergedSize = customizeSize || size;
@ -146,7 +158,7 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
components={Components} components={Components}
direction={direction} direction={direction}
disabled={mergedDisabled} disabled={mergedDisabled}
dropdownClassName={classNames(hashId, dropdownClassName)} dropdownClassName={classNames(hashId, popupClassName || dropdownClassName)}
/> />
); );
}} }}

View File

@ -59,7 +59,7 @@ The following APIs are shared by DatePicker, RangePicker.
| dateRender | Custom rendering function for date cells | function(currentDate: dayjs, today: dayjs) => React.ReactNode | - | | | dateRender | Custom rendering function for date cells | function(currentDate: dayjs, today: dayjs) => React.ReactNode | - | |
| disabled | Determine whether the DatePicker is disabled | boolean | false | | | disabled | Determine whether the DatePicker is disabled | boolean | false | |
| disabledDate | Specify the date that cannot be selected | (currentDate: dayjs) => boolean | - | | | disabledDate | Specify the date that cannot be selected | (currentDate: dayjs) => boolean | - | |
| dropdownClassName | To customize the className of the popup calendar | string | - | | | popupClassName | To customize the className of the popup calendar | string | - | 4.23.0 |
| getPopupContainer | To set the container of the floating layer, while the default is to create a `div` element in `body` | function(trigger) | - | | | getPopupContainer | To set the container of the floating layer, while the default is to create a `div` element in `body` | function(trigger) | - | |
| inputReadOnly | Set the `readonly` attribute of the input tag (avoids virtual keyboard on touch devices) | boolean | false | | | inputReadOnly | Set the `readonly` attribute of the input tag (avoids virtual keyboard on touch devices) | boolean | false | |
| locale | Localization configuration | object | [default](https://github.com/ant-design/ant-design/blob/master/components/date-picker/locale/example.json) | | | locale | Localization configuration | object | [default](https://github.com/ant-design/ant-design/blob/master/components/date-picker/locale/example.json) | |

View File

@ -60,7 +60,7 @@ import locale from 'antd/es/locale/zh_CN';
| dateRender | 自定义日期单元格的内容 | function(currentDate: dayjs, today: dayjs) => React.ReactNode | - | | | dateRender | 自定义日期单元格的内容 | function(currentDate: dayjs, today: dayjs) => React.ReactNode | - | |
| disabled | 禁用 | boolean | false | | | disabled | 禁用 | boolean | false | |
| disabledDate | 不可选择的日期 | (currentDate: dayjs) => boolean | - | | | disabledDate | 不可选择的日期 | (currentDate: dayjs) => boolean | - | |
| dropdownClassName | 额外的弹出日历 className | string | - | | | popupClassName | 额外的弹出日历 className | string | - | 4.23.0 |
| getPopupContainer | 定义浮层的容器,默认为 body 上新建 div | function(trigger) | - | | | getPopupContainer | 定义浮层的容器,默认为 body 上新建 div | function(trigger) | - | |
| inputReadOnly | 设置输入框为只读(避免在移动设备上打开虚拟键盘) | boolean | false | | | inputReadOnly | 设置输入框为只读(避免在移动设备上打开虚拟键盘) | boolean | false | |
| locale | 国际化配置 | object | [默认配置](https://github.com/ant-design/ant-design/blob/master/components/date-picker/locale/example.json) | | | locale | 国际化配置 | object | [默认配置](https://github.com/ant-design/ant-design/blob/master/components/date-picker/locale/example.json) | |

View File

@ -69,7 +69,7 @@ const genDrawerStyle: GenerateStyle<DrawerToken> = (token: DrawerToken) => {
}, },
// Placement // Placement
[`&-left ${wrapperCls}`]: { [`&-left > ${wrapperCls}`]: {
top: 0, top: 0,
bottom: 0, bottom: 0,
left: { left: {
@ -78,7 +78,7 @@ const genDrawerStyle: GenerateStyle<DrawerToken> = (token: DrawerToken) => {
}, },
boxShadow: token.boxShadowDrawerRight, boxShadow: token.boxShadowDrawerRight,
}, },
[`&-right ${wrapperCls}`]: { [`&-right > ${wrapperCls}`]: {
top: 0, top: 0,
right: { right: {
_skip_check_: true, _skip_check_: true,
@ -87,12 +87,12 @@ const genDrawerStyle: GenerateStyle<DrawerToken> = (token: DrawerToken) => {
bottom: 0, bottom: 0,
boxShadow: token.boxShadowDrawerLeft, boxShadow: token.boxShadowDrawerLeft,
}, },
[`&-top ${wrapperCls}`]: { [`&-top > ${wrapperCls}`]: {
top: 0, top: 0,
insetInline: 0, insetInline: 0,
boxShadow: token.boxShadowDrawerDown, boxShadow: token.boxShadowDrawerDown,
}, },
[`&-bottom ${wrapperCls}`]: { [`&-bottom > ${wrapperCls}`]: {
bottom: 0, bottom: 0,
insetInline: 0, insetInline: 0,
boxShadow: token.boxShadowDrawerUp, boxShadow: token.boxShadowDrawerUp,

View File

@ -4246,7 +4246,7 @@ exports[`renders ./components/dropdown/demo/dropdown-button.md extend context co
</div> </div>
<div <div
class="ant-space-item" class="ant-space-item"
style="padding-bottom:8px" style="margin-right:8px;padding-bottom:8px"
> >
<button <button
class="ant-btn ant-btn-default ant-dropdown-trigger" class="ant-btn ant-btn-default ant-dropdown-trigger"
@ -4528,6 +4528,288 @@ exports[`renders ./components/dropdown/demo/dropdown-button.md extend context co
</div> </div>
</div> </div>
</div> </div>
<div
class="ant-space-item"
style="padding-bottom:8px"
>
<div
class="ant-btn-group ant-dropdown-button"
>
<button
class="ant-btn ant-btn-default ant-btn-dangerous"
type="button"
>
<span>
Danger
</span>
</button>
<button
class="ant-btn ant-btn-default ant-btn-icon-only ant-btn-dangerous ant-dropdown-trigger"
type="button"
>
<span
aria-label="ellipsis"
class="anticon anticon-ellipsis"
role="img"
>
<svg
aria-hidden="true"
data-icon="ellipsis"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"
/>
</svg>
</span>
</button>
<div>
<div
class="ant-dropdown"
style="opacity:0"
>
<ul
class="ant-dropdown-menu ant-dropdown-menu-root ant-dropdown-menu-vertical"
data-menu-list="true"
role="menu"
tabindex="0"
>
<li
class="ant-dropdown-menu-item"
role="menuitem"
tabindex="-1"
>
<span
aria-label="user"
class="anticon anticon-user ant-dropdown-menu-item-icon"
role="img"
>
<svg
aria-hidden="true"
data-icon="user"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M858.5 763.6a374 374 0 00-80.6-119.5 375.63 375.63 0 00-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 00-80.6 119.5A371.7 371.7 0 00136 901.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 008-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z"
/>
</svg>
</span>
<span
class="ant-dropdown-menu-title-content"
>
1st menu item
</span>
</li>
<div>
<div
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
style="opacity:0"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
/>
</div>
</div>
</div>
<li
class="ant-dropdown-menu-item"
role="menuitem"
tabindex="-1"
>
<span
aria-label="user"
class="anticon anticon-user ant-dropdown-menu-item-icon"
role="img"
>
<svg
aria-hidden="true"
data-icon="user"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M858.5 763.6a374 374 0 00-80.6-119.5 375.63 375.63 0 00-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 00-80.6 119.5A371.7 371.7 0 00136 901.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 008-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z"
/>
</svg>
</span>
<span
class="ant-dropdown-menu-title-content"
>
2nd menu item
</span>
</li>
<div>
<div
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
style="opacity:0"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
/>
</div>
</div>
</div>
<li
class="ant-dropdown-menu-item"
role="menuitem"
tabindex="-1"
>
<span
aria-label="user"
class="anticon anticon-user ant-dropdown-menu-item-icon"
role="img"
>
<svg
aria-hidden="true"
data-icon="user"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M858.5 763.6a374 374 0 00-80.6-119.5 375.63 375.63 0 00-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 00-80.6 119.5A371.7 371.7 0 00136 901.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 008-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z"
/>
</svg>
</span>
<span
class="ant-dropdown-menu-title-content"
>
3rd menu item
</span>
</li>
<div>
<div
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
style="opacity:0"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
/>
</div>
</div>
</div>
</ul>
<div
aria-hidden="true"
style="display:none"
>
<div>
<div
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
style="opacity:0"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
/>
</div>
</div>
</div>
<div>
<div
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
style="opacity:0"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
/>
</div>
</div>
</div>
<div>
<div
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
style="opacity:0"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
`; `;

View File

@ -334,7 +334,7 @@ exports[`renders ./components/dropdown/demo/dropdown-button.md correctly 1`] = `
</div> </div>
<div <div
class="ant-space-item" class="ant-space-item"
style="padding-bottom:8px" style="margin-right:8px;padding-bottom:8px"
> >
<button <button
class="ant-btn ant-btn-default ant-dropdown-trigger" class="ant-btn ant-btn-default ant-dropdown-trigger"
@ -375,6 +375,47 @@ exports[`renders ./components/dropdown/demo/dropdown-button.md correctly 1`] = `
</div> </div>
</button> </button>
</div> </div>
<div
class="ant-space-item"
style="padding-bottom:8px"
>
<div
class="ant-btn-group ant-dropdown-button"
>
<button
class="ant-btn ant-btn-default ant-btn-dangerous"
type="button"
>
<span>
Danger
</span>
</button>
<button
class="ant-btn ant-btn-default ant-btn-icon-only ant-btn-dangerous ant-dropdown-trigger"
type="button"
>
<span
aria-label="ellipsis"
class="anticon anticon-ellipsis"
role="img"
>
<svg
aria-hidden="true"
data-icon="ellipsis"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"
/>
</svg>
</span>
</button>
</div>
</div>
</div> </div>
`; `;

View File

@ -82,6 +82,9 @@ const App: React.FC = () => (
</Space> </Space>
</Button> </Button>
</Dropdown> </Dropdown>
<Dropdown.Button danger onClick={handleButtonClick} overlay={menu}>
Danger
</Dropdown.Button>
</Space> </Space>
); );

View File

@ -17,6 +17,7 @@ export type DropdownButtonType = 'default' | 'primary' | 'ghost' | 'dashed' | 'l
export interface DropdownButtonProps extends ButtonGroupProps, DropdownProps { export interface DropdownButtonProps extends ButtonGroupProps, DropdownProps {
type?: DropdownButtonType; type?: DropdownButtonType;
htmlType?: ButtonHTMLType; htmlType?: ButtonHTMLType;
danger?: boolean;
disabled?: boolean; disabled?: boolean;
loading?: ButtonProps['loading']; loading?: ButtonProps['loading'];
onClick?: React.MouseEventHandler<HTMLButtonElement>; onClick?: React.MouseEventHandler<HTMLButtonElement>;
@ -41,6 +42,7 @@ const DropdownButton: DropdownButtonInterface = props => {
const { const {
prefixCls: customizePrefixCls, prefixCls: customizePrefixCls,
type = 'default', type = 'default',
danger,
disabled, disabled,
loading, loading,
onClick, onClick,
@ -97,6 +99,7 @@ const DropdownButton: DropdownButtonInterface = props => {
const leftButton = ( const leftButton = (
<Button <Button
type={type} type={type}
danger={danger}
disabled={disabled} disabled={disabled}
loading={loading} loading={loading}
onClick={onClick} onClick={onClick}
@ -108,7 +111,7 @@ const DropdownButton: DropdownButtonInterface = props => {
</Button> </Button>
); );
const rightButton = <Button type={type} icon={icon} />; const rightButton = <Button type={type} danger={danger} icon={icon} />;
const [leftButtonToRender, rightButtonToRender] = buttonsRender!([leftButton, rightButton]); const [leftButtonToRender, rightButtonToRender] = buttonsRender!([leftButton, rightButton]);

View File

@ -42,6 +42,7 @@ You should use [Menu](/components/menu/) as `overlay`. The menu items and divide
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| buttonsRender | Custom buttons inside Dropdown.Button | (buttons: ReactNode\[]) => ReactNode\[] | - | | | buttonsRender | Custom buttons inside Dropdown.Button | (buttons: ReactNode\[]) => ReactNode\[] | - | |
| loading | Set the loading status of button | boolean \| { delay: number } | false | | | loading | Set the loading status of button | boolean \| { delay: number } | false | |
| danger | Set the danger status of button | boolean | - | 4.23.0 |
| disabled | Whether the dropdown menu is disabled | boolean | - | | | disabled | Whether the dropdown menu is disabled | boolean | - | |
| icon | Icon (appears on the right) | ReactNode | - | | | icon | Icon (appears on the right) | ReactNode | - | |
| overlay | The dropdown menu | [Menu](/components/menu) | - | | | overlay | The dropdown menu | [Menu](/components/menu) | - | |

View File

@ -46,6 +46,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/eedWN59yJ/Dropdown.svg
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| buttonsRender | 自定义左右两个按钮 | (buttons: ReactNode\[]) => ReactNode\[] | - | | | buttonsRender | 自定义左右两个按钮 | (buttons: ReactNode\[]) => ReactNode\[] | - | |
| loading | 设置按钮载入状态 | boolean \| { delay: number } | false | | | loading | 设置按钮载入状态 | boolean \| { delay: number } | false | |
| danger | 设置危险按钮 | boolean | - | 4.23.0 |
| disabled | 菜单是否禁用 | boolean | - | | | disabled | 菜单是否禁用 | boolean | - | |
| icon | 右侧的 icon | ReactNode | - | | | icon | 右侧的 icon | ReactNode | - | |
| overlay | 菜单 | [Menu](/components/menu/) | - | | | overlay | 菜单 | [Menu](/components/menu/) | - | |

View File

@ -121,7 +121,7 @@ export interface InputProps
disabled?: boolean; disabled?: boolean;
status?: InputStatus; status?: InputStatus;
bordered?: boolean; bordered?: boolean;
[key: `data-${string}`]: string; [key: `data-${string}`]: string | undefined;
} }
const Input = forwardRef<InputRef, InputProps>((props, ref) => { const Input = forwardRef<InputRef, InputProps>((props, ref) => {

View File

@ -18,7 +18,7 @@ import { fixControlledValue, resolveOnChange, triggerFocus } from './Input';
import useStyle from './style'; import useStyle from './style';
interface ShowCountProps { interface ShowCountProps {
formatter: (args: { count: number; maxLength?: number }) => string; formatter: (args: { value: string; count: number; maxLength?: number }) => string;
} }
function fixEmojiLength(value: string, maxLength: number) { function fixEmojiLength(value: string, maxLength: number) {
@ -238,7 +238,7 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
let dataCount = ''; let dataCount = '';
if (typeof showCount === 'object') { if (typeof showCount === 'object') {
dataCount = showCount.formatter({ count: valueLength, maxLength }); dataCount = showCount.formatter({ value: val, count: valueLength, maxLength });
} else { } else {
dataCount = `${valueLength}${hasMaxLength ? ` / ${maxLength}` : ''}`; dataCount = `${valueLength}${hasMaxLength ? ` / ${maxLength}` : ''}`;
} }

View File

@ -272,12 +272,14 @@ describe('should support showCount', () => {
const { container } = render( const { container } = render(
<Input <Input
maxLength={5} maxLength={5}
showCount={{ formatter: ({ count, maxLength }) => `${count}, ${maxLength}` }} showCount={{
formatter: ({ value, count, maxLength }) => `${value}, ${count}, ${maxLength}`,
}}
value="12345" value="12345"
/>, />,
); );
expect(container.querySelector('input')?.getAttribute('value')).toBe('12345'); expect(container.querySelector('input')?.getAttribute('value')).toBe('12345');
expect(container.querySelector('.ant-input-show-count-suffix')?.innerHTML).toBe('5, 5'); expect(container.querySelector('.ant-input-show-count-suffix')?.innerHTML).toBe('12345, 5, 5');
}); });
}); });

View File

@ -304,13 +304,15 @@ describe('TextArea', () => {
const { container } = render( const { container } = render(
<TextArea <TextArea
maxLength={5} maxLength={5}
showCount={{ formatter: ({ count, maxLength }) => `${count}, ${maxLength}` }} showCount={{
formatter: ({ value, count, maxLength }) => `${value}, ${count}, ${maxLength}`,
}}
value="12345" value="12345"
/>, />,
); );
expect(container.querySelector('textarea').value).toBe('12345'); expect(container.querySelector('textarea').value).toBe('12345');
expect(container.querySelector('.ant-input-textarea').getAttribute('data-count')).toBe( expect(container.querySelector('.ant-input-textarea').getAttribute('data-count')).toBe(
'5, 5', '12345, 5, 5',
); );
}); });
}); });

View File

@ -26,7 +26,7 @@ A basic widget for getting the user input is a text field. Keyboard and mouse ca
| disabled | Whether the input is disabled | boolean | false | | | disabled | Whether the input is disabled | boolean | false | |
| id | The ID for input | string | - | | | id | The ID for input | string | - | |
| maxLength | The max length | number | - | | | maxLength | The max length | number | - | |
| showCount | Whether show text count | boolean \| { formatter: ({ count: number, maxLength?: number }) => ReactNode } | false | 4.18.0 | | showCount | Whether show text count | boolean \| { formatter: (info: { value: string, count: number, maxLength?: number }) => ReactNode } | false | 4.18.0 info.value: 4.23.0 |
| status | Set validation status | 'error' \| 'warning' | - | 4.19.0 | | status | Set validation status | 'error' \| 'warning' | - | 4.19.0 |
| prefix | The prefix icon for the Input | ReactNode | - | | | prefix | The prefix icon for the Input | ReactNode | - | |
| size | The size of the input box. Note: in the context of a form, the `middle` size is used | `large` \| `middle` \| `small` | - | | | size | The size of the input box. Note: in the context of a form, the `middle` size is used | `large` \| `middle` \| `small` | - | |
@ -49,7 +49,7 @@ The rest of the props of Input are exactly the same as the original [input](http
| bordered | Whether has border style | boolean | true | 4.5.0 | | bordered | Whether has border style | boolean | true | 4.5.0 |
| defaultValue | The initial input content | string | - | | | defaultValue | The initial input content | string | - | |
| maxLength | The max length | number | - | 4.7.0 | | maxLength | The max length | number | - | 4.7.0 |
| showCount | Whether show text count | boolean \| { formatter: ({ count: number, maxLength?: number }) => string } | false | 4.7.0 (formatter: 4.10.0) | | showCount | Whether show text count | boolean \| { formatter: (info: { value: string, count: number, maxLength?: number }) => string } | false | 4.7.0 formatter: 4.10.0 info.value: 4.23.0 |
| value | The input content value | string | - | | | value | The input content value | string | - | |
| onPressEnter | The callback function that is triggered when Enter key is pressed | function(e) | - | | | onPressEnter | The callback function that is triggered when Enter key is pressed | function(e) | - | |
| onResize | The callback function that is triggered when resize | function({ width, height }) | - | | | onResize | The callback function that is triggered when resize | function({ width, height }) | - | |

View File

@ -27,7 +27,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/xS9YEJhfe/Input.svg
| disabled | 是否禁用状态,默认为 false | boolean | false | | | disabled | 是否禁用状态,默认为 false | boolean | false | |
| id | 输入框的 id | string | - | | | id | 输入框的 id | string | - | |
| maxLength | 最大长度 | number | - | | | maxLength | 最大长度 | number | - | |
| showCount | 是否展示字数 | boolean \| { formatter: ({ count: number, maxLength?: number }) => ReactNode } | false | 4.18.0 | | showCount | 是否展示字数 | boolean \| { formatter: (info: { value: string, count: number, maxLength?: number }) => ReactNode } | false | 4.18.0 info.value: 4.23.0 |
| status | 设置校验状态 | 'error' \| 'warning' | - | 4.19.0 | | status | 设置校验状态 | 'error' \| 'warning' | - | 4.19.0 |
| prefix | 带有前缀图标的 input | ReactNode | - | | | prefix | 带有前缀图标的 input | ReactNode | - | |
| size | 控件大小。注:标准表单内的输入框大小限制为 `middle` | `large` \| `middle` \| `small` | - | | | size | 控件大小。注:标准表单内的输入框大小限制为 `middle` | `large` \| `middle` \| `small` | - | |
@ -50,7 +50,7 @@ Input 的其他属性和 React 自带的 [input](https://reactjs.org/docs/dom-el
| bordered | 是否有边框 | boolean | true | 4.5.0 | | bordered | 是否有边框 | boolean | true | 4.5.0 |
| defaultValue | 输入框默认内容 | string | - | | | defaultValue | 输入框默认内容 | string | - | |
| maxLength | 内容最大长度 | number | - | 4.7.0 | | maxLength | 内容最大长度 | number | - | 4.7.0 |
| showCount | 是否展示字数 | boolean \| { formatter: ({ count: number, maxLength?: number }) => string } | false | 4.7.0 (formatter: 4.10.0) | | showCount | 是否展示字数 | boolean \| { formatter: (info: { value: string, count: number, maxLength?: number }) => string } | false | 4.7.0 formatter: 4.10.0 info.value: 4.23.0 |
| value | 输入框内容 | string | - | | | value | 输入框内容 | string | - | |
| onPressEnter | 按下回车的回调 | function(e) | - | | | onPressEnter | 按下回车的回调 | function(e) | - | |
| onResize | resize 回调 | function({ width, height }) | - | | | onResize | resize 回调 | function({ width, height }) | - | |

View File

@ -1,121 +1,132 @@
import { mount } from 'enzyme';
import React from 'react'; import React from 'react';
import Progress from '..'; import Progress from '..';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest'; import rtlTest from '../../../tests/shared/rtlTest';
import { handleGradient, sortGradient } from '../Line'; import { handleGradient, sortGradient } from '../Line';
import ProgressSteps from '../Steps'; import ProgressSteps from '../Steps';
import { render } from '../../../tests/utils';
describe('Progress', () => { describe('Progress', () => {
mountTest(Progress); mountTest(Progress);
rtlTest(Progress); rtlTest(Progress);
it('successPercent should decide the progress status when it exists', () => { it('successPercent should decide the progress status when it exists', () => {
const wrapper = mount(<Progress percent={100} success={{ percent: 50 }} />); const { container: wrapper, rerender } = render(
expect(wrapper.find('.ant-progress-status-success')).toHaveLength(0); <Progress percent={100} success={{ percent: 50 }} />,
);
expect(wrapper.querySelectorAll('.ant-progress-status-success')).toHaveLength(0);
wrapper.setProps({ percent: 50, success: { percent: 100 } }); rerender(<Progress percent={50} success={{ percent: 100 }} />);
expect(wrapper.find('.ant-progress-status-success')).toHaveLength(1); expect(wrapper.querySelectorAll('.ant-progress-status-success')).toHaveLength(1);
wrapper.setProps({ percent: 100, success: { percent: 0 } }); rerender(<Progress percent={100} success={{ percent: 0 }} />);
expect(wrapper.find('.ant-progress-status-success')).toHaveLength(0); expect(wrapper.querySelectorAll('.ant-progress-status-success')).toHaveLength(0);
}); });
it('render out-of-range progress', () => { it('render out-of-range progress', () => {
const wrapper = mount(<Progress percent={120} />); const { container: wrapper } = render(<Progress percent={120} />);
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('render out-of-range progress with info', () => { it('render out-of-range progress with info', () => {
const wrapper = mount(<Progress percent={120} showInfo />); const { container: wrapper } = render(<Progress percent={120} showInfo />);
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('render negative progress', () => { it('render negative progress', () => {
const wrapper = mount(<Progress percent={-20} />); const { container: wrapper } = render(<Progress percent={-20} />);
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('render negative successPercent', () => { it('render negative successPercent', () => {
const wrapper = mount(<Progress percent={50} success={{ percent: -20 }} />); const { container: wrapper } = render(<Progress percent={50} success={{ percent: -20 }} />);
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('render format', () => { it('render format', () => {
const wrapper = mount( const { container: wrapper } = render(
<Progress <Progress
percent={50} percent={50}
success={{ percent: 10 }} success={{ percent: 10 }}
format={(percent, successPercent) => `${percent} ${successPercent}`} format={(percent, successPercent) => `${percent} ${successPercent}`}
/>, />,
); );
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('render strokeColor', () => { it('render strokeColor', () => {
const wrapper = mount(<Progress type="circle" percent={50} strokeColor="red" />); const { container: wrapper, rerender } = render(
expect(wrapper.render()).toMatchSnapshot(); <Progress type="circle" percent={50} strokeColor="red" />,
wrapper.setProps({ );
strokeColor: { expect(wrapper.firstChild).toMatchSnapshot();
from: '#108ee9', rerender(
to: '#87d068', <Progress
}, strokeColor={{
type: 'line', from: '#108ee9',
}); to: '#87d068',
expect(wrapper.render()).toMatchSnapshot(); }}
wrapper.setProps({ percent={50}
strokeColor: { type="line"
'0%': '#108ee9', />,
'100%': '#87d068', );
}, expect(wrapper.firstChild).toMatchSnapshot();
}); rerender(
expect(wrapper.render()).toMatchSnapshot(); <Progress
strokeColor={{
'0%': '#108ee9',
'100%': '#87d068',
}}
percent={50}
type="line"
/>,
);
expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('render normal progress', () => { it('render normal progress', () => {
const wrapper = mount(<Progress status="normal" />); const { container: wrapper } = render(<Progress status="normal" />);
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('render trailColor progress', () => { it('render trailColor progress', () => {
const wrapper = mount(<Progress status="normal" trailColor="#ffffff" />); const { container: wrapper } = render(<Progress status="normal" trailColor="#ffffff" />);
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('render successColor progress', () => { it('render successColor progress', () => {
const wrapper = mount( const { container: wrapper } = render(
<Progress percent={60} success={{ percent: 30, strokeColor: '#ffffff' }} />, <Progress percent={60} success={{ percent: 30, strokeColor: '#ffffff' }} />,
); );
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('render successColor progress type="circle"', () => { it('render successColor progress type="circle"', () => {
const wrapper = mount( const { container: wrapper } = render(
<Progress percent={60} type="circle" success={{ percent: 30, strokeColor: '#ffffff' }} />, <Progress percent={60} type="circle" success={{ percent: 30, strokeColor: '#ffffff' }} />,
); );
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('render successColor progress type="dashboard"', () => { it('render successColor progress type="dashboard"', () => {
const wrapper = mount( const { container: wrapper } = render(
<Progress percent={60} type="dashboard" success={{ percent: 30, strokeColor: '#ffffff' }} />, <Progress percent={60} type="dashboard" success={{ percent: 30, strokeColor: '#ffffff' }} />,
); );
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('render dashboard zero gapDegree', () => { it('render dashboard zero gapDegree', () => {
const wrapper = mount(<Progress type="dashboard" gapDegree={0} />); const { container: wrapper } = render(<Progress type="dashboard" gapDegree={0} />);
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('render dashboard 295 gapDegree', () => { it('render dashboard 295 gapDegree', () => {
const wrapper = mount(<Progress type="dashboard" gapDegree={295} />); const { container: wrapper } = render(<Progress type="dashboard" gapDegree={295} />);
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('render dashboard 296 gapDegree', () => { it('render dashboard 296 gapDegree', () => {
const wrapper = mount(<Progress type="dashboard" gapDegree={296} />); const { container: wrapper } = render(<Progress type="dashboard" gapDegree={296} />);
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('get correct line-gradient', () => { it('get correct line-gradient', () => {
@ -138,74 +149,74 @@ describe('Progress', () => {
}); });
it('should show success status when percent is 100', () => { it('should show success status when percent is 100', () => {
const wrapper = mount(<Progress percent={100} />); const { container: wrapper } = render(<Progress percent={100} />);
expect(wrapper.find('.ant-progress-status-success')).toHaveLength(1); expect(wrapper.querySelectorAll('.ant-progress-status-success')).toHaveLength(1);
}); });
// https://github.com/ant-design/ant-design/issues/15950 // https://github.com/ant-design/ant-design/issues/15950
it('should show success status when percent is 100 and status is undefined', () => { it('should show success status when percent is 100 and status is undefined', () => {
const wrapper = mount(<Progress percent={100} status={undefined} />); const { container: wrapper } = render(<Progress percent={100} status={undefined} />);
expect(wrapper.find('.ant-progress-status-success')).toHaveLength(1); expect(wrapper.querySelectorAll('.ant-progress-status-success')).toHaveLength(1);
}); });
// https://github.com/ant-design/ant-design/pull/15951#discussion_r273062969 // https://github.com/ant-design/ant-design/pull/15951#discussion_r273062969
it('should show success status when status is invalid', () => { it('should show success status when status is invalid', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const wrapper = mount(<Progress percent={100} status="invalid" />); const { container: wrapper } = render(<Progress percent={100} status="invalid" />);
expect(wrapper.find('.ant-progress-status-success')).toHaveLength(1); expect(wrapper.querySelectorAll('.ant-progress-status-success')).toHaveLength(1);
errorSpy.mockRestore(); errorSpy.mockRestore();
}); });
it('should support steps', () => { it('should support steps', () => {
const wrapper = mount(<Progress steps={3} />); const { container: wrapper } = render(<Progress steps={3} />);
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('steps should be changable', () => { it('steps should be changable', () => {
const wrapper = mount(<Progress steps={5} percent={60} />); const { container: wrapper, rerender } = render(<Progress steps={5} percent={60} />);
expect(wrapper.find('.ant-progress-steps-item-active').length).toBe(3); expect(wrapper.querySelectorAll('.ant-progress-steps-item-active').length).toBe(3);
wrapper.setProps({ percent: 40 }); rerender(<Progress steps={5} percent={40} />);
expect(wrapper.find('.ant-progress-steps-item-active').length).toBe(2); expect(wrapper.querySelectorAll('.ant-progress-steps-item-active').length).toBe(2);
}); });
it('steps should be changable when has strokeColor', () => { it('steps should be changable when has strokeColor', () => {
const wrapper = mount(<Progress steps={5} percent={60} strokeColor="#1890ff" />); const { container: wrapper, rerender } = render(
expect(wrapper.find('.ant-progress-steps-item').at(0).getDOMNode().style.backgroundColor).toBe( <Progress steps={5} percent={60} strokeColor="#1890ff" />,
);
expect(wrapper.querySelectorAll('.ant-progress-steps-item')[0].style.backgroundColor).toBe(
'rgb(24, 144, 255)', 'rgb(24, 144, 255)',
); );
wrapper.setProps({ percent: 40 }); rerender(<Progress steps={5} percent={40} strokeColor="#1890ff" />);
expect(wrapper.find('.ant-progress-steps-item').at(2).getDOMNode().style.backgroundColor).toBe( expect(wrapper.querySelectorAll('.ant-progress-steps-item')[2].style.backgroundColor).toBe('');
'', expect(wrapper.querySelectorAll('.ant-progress-steps-item')[1].style.backgroundColor).toBe(
);
expect(wrapper.find('.ant-progress-steps-item').at(1).getDOMNode().style.backgroundColor).toBe(
'rgb(24, 144, 255)', 'rgb(24, 144, 255)',
); );
}); });
it('steps should support trailColor', () => { it('steps should support trailColor', () => {
const wrapper = mount(<Progress steps={5} percent={20} trailColor="#1890ee" />); const { container: wrapper } = render(<Progress steps={5} percent={20} trailColor="#1890ee" />);
expect(wrapper.find('.ant-progress-steps-item').at(1).getDOMNode().style.backgroundColor).toBe( expect(wrapper.querySelectorAll('.ant-progress-steps-item')[1].style.backgroundColor).toBe(
'rgb(24, 144, 238)', 'rgb(24, 144, 238)',
); );
}); });
it('should display correct step', () => { it('should display correct step', () => {
const wrapper = mount(<Progress steps={9} percent={22.22} />); const { container: wrapper, rerender } = render(<Progress steps={9} percent={22.22} />);
expect(wrapper.find('.ant-progress-steps-item-active').length).toBe(2); expect(wrapper.querySelectorAll('.ant-progress-steps-item-active').length).toBe(2);
wrapper.setProps({ percent: 33.33 }); rerender(<Progress steps={9} percent={33.33} />);
expect(wrapper.find('.ant-progress-steps-item-active').length).toBe(3); expect(wrapper.querySelectorAll('.ant-progress-steps-item-active').length).toBe(3);
wrapper.setProps({ percent: 44.44 }); rerender(<Progress steps={9} percent={44.44} />);
expect(wrapper.find('.ant-progress-steps-item-active').length).toBe(4); expect(wrapper.querySelectorAll('.ant-progress-steps-item-active').length).toBe(4);
}); });
it('steps should have default percent 0', () => { it('steps should have default percent 0', () => {
const wrapper = mount(<ProgressSteps />); const { container: wrapper } = render(<ProgressSteps />);
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.firstChild).toMatchSnapshot();
}); });
it('should warnning if use `progress` in success', () => { it('should warnning if use `progress` in success', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
mount(<Progress percent={60} success={{ progress: 30 }} />); render(<Progress percent={60} success={{ progress: 30 }} />);
expect(errorSpy).toHaveBeenCalledWith( expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Progress] `success.progress` is deprecated. Please use `success.percent` instead.', 'Warning: [antd: Progress] `success.progress` is deprecated. Please use `success.percent` instead.',
); );
@ -213,7 +224,7 @@ describe('Progress', () => {
it('should warnning if use `progress` in success in type Circle', () => { it('should warnning if use `progress` in success in type Circle', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
mount(<Progress percent={60} success={{ progress: 30 }} type="circle" />); render(<Progress percent={60} success={{ progress: 30 }} type="circle" />);
expect(errorSpy).toHaveBeenCalledWith( expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Progress] `success.progress` is deprecated. Please use `success.percent` instead.', 'Warning: [antd: Progress] `success.progress` is deprecated. Please use `success.percent` instead.',
); );
@ -223,8 +234,10 @@ describe('Progress', () => {
describe('github issues', () => { describe('github issues', () => {
it('"Rendered more hooks than during the previous render"', () => { it('"Rendered more hooks than during the previous render"', () => {
expect(() => { expect(() => {
const wrapper = mount(<Progress percent={60} success={{ percent: 0 }} type="circle" />); const { rerender } = render(
wrapper.setProps({ success: { percent: 10 } }); <Progress percent={60} success={{ percent: 0 }} type="circle" />,
);
rerender(<Progress percent={60} success={{ percent: 10 }} type="circle" />);
}).not.toThrow(); }).not.toThrow();
}); });
}); });

View File

@ -91,6 +91,16 @@ describe('Select', () => {
expect(container.querySelectorAll('.anticon-down').length).toBe(0); expect(container.querySelectorAll('.anticon-down').length).toBe(0);
expect(container.querySelectorAll('.anticon-search').length).toBe(1); expect(container.querySelectorAll('.anticon-search').length).toBe(1);
}); });
it('should show warning when use dropdownClassName', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
render(<Select dropdownClassName="myCustomClassName" />);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Select] `dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
);
errorSpy.mockRestore();
});
// //
describe('Select Custom Icons', () => { describe('Select Custom Icons', () => {
it('should support customized icons', () => { it('should support customized icons', () => {

View File

@ -33,7 +33,7 @@ Select component to select value from options.
| defaultOpen | Initial open state of dropdown | boolean | - | | | defaultOpen | Initial open state of dropdown | boolean | - | |
| defaultValue | Initial selected option | string \| string\[]<br />number \| number\[]<br />LabeledValue \| LabeledValue\[] | - | | | defaultValue | Initial selected option | string \| string\[]<br />number \| number\[]<br />LabeledValue \| LabeledValue\[] | - | |
| disabled | Whether disabled select | boolean | false | | | disabled | Whether disabled select | boolean | false | |
| dropdownClassName | The className of dropdown menu | string | - | | | popupClassName | The className of dropdown menu | string | - | 4.23.0 |
| dropdownMatchSelectWidth | Determine whether the dropdown menu and the select input are the same width. Default set `min-width` same as input. Will ignore when value less than select width. `false` will disable virtual scroll | boolean \| number | true | | | dropdownMatchSelectWidth | Determine whether the dropdown menu and the select input are the same width. Default set `min-width` same as input. Will ignore when value less than select width. `false` will disable virtual scroll | boolean \| number | true | |
| dropdownRender | Customize dropdown content | (originNode: ReactNode) => ReactNode | - | | | dropdownRender | Customize dropdown content | (originNode: ReactNode) => ReactNode | - | |
| dropdownStyle | The style of dropdown menu | CSSProperties | - | | | dropdownStyle | The style of dropdown menu | CSSProperties | - | |

View File

@ -19,6 +19,7 @@ import { getTransitionDirection, getTransitionName } from '../_util/motion';
import type { InputStatus } from '../_util/statusUtils'; import type { InputStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils'; import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import getIcons from './utils/iconUtil'; import getIcons from './utils/iconUtil';
import warning from '../_util/warning';
import useStyle from './style'; import useStyle from './style';
import genPurePanel from '../_util/PurePanel'; import genPurePanel from '../_util/PurePanel';
@ -56,6 +57,12 @@ export interface SelectProps<
placement?: SelectCommonPlacement; placement?: SelectCommonPlacement;
mode?: 'multiple' | 'tags'; mode?: 'multiple' | 'tags';
status?: InputStatus; status?: InputStatus;
/**
* @deprecated `dropdownClassName` is deprecated which will be removed in next major
* version.Please use `popupClassName` instead.
*/
dropdownClassName?: string;
popupClassName?: string;
} }
const SECRET_COMBOBOX_MODE_DO_NOT_USE = 'SECRET_COMBOBOX_MODE_DO_NOT_USE'; const SECRET_COMBOBOX_MODE_DO_NOT_USE = 'SECRET_COMBOBOX_MODE_DO_NOT_USE';
@ -67,6 +74,7 @@ const InternalSelect = <OptionType extends BaseOptionType | DefaultOptionType =
className, className,
getPopupContainer, getPopupContainer,
dropdownClassName, dropdownClassName,
popupClassName,
listHeight = 256, listHeight = 256,
placement, placement,
listItemHeight = 24, listItemHeight = 24,
@ -112,6 +120,13 @@ const InternalSelect = <OptionType extends BaseOptionType | DefaultOptionType =
const mergedShowArrow = const mergedShowArrow =
showArrow !== undefined ? showArrow : props.loading || !(isMultiple || mode === 'combobox'); showArrow !== undefined ? showArrow : props.loading || !(isMultiple || mode === 'combobox');
// =================== Warning =====================
warning(
!dropdownClassName,
'Select',
'`dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
);
// ===================== Form Status ===================== // ===================== Form Status =====================
const { const {
status: contextStatus, status: contextStatus,
@ -144,7 +159,7 @@ const InternalSelect = <OptionType extends BaseOptionType | DefaultOptionType =
const selectProps = omit(props as typeof props & { itemIcon: any }, ['suffixIcon', 'itemIcon']); const selectProps = omit(props as typeof props & { itemIcon: any }, ['suffixIcon', 'itemIcon']);
const rcSelectRtlDropdownClassName = classNames( const rcSelectRtlDropdownClassName = classNames(
dropdownClassName, popupClassName || dropdownClassName,
{ {
[`${prefixCls}-dropdown-${direction}`]: direction === 'rtl', [`${prefixCls}-dropdown-${direction}`]: direction === 'rtl',
}, },

View File

@ -34,7 +34,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg
| defaultOpen | 是否默认展开下拉菜单 | boolean | - | | | defaultOpen | 是否默认展开下拉菜单 | boolean | - | |
| defaultValue | 指定默认选中的条目 | string \| string\[]<br />number \| number\[]<br />LabeledValue \| LabeledValue\[] | - | | | defaultValue | 指定默认选中的条目 | string \| string\[]<br />number \| number\[]<br />LabeledValue \| LabeledValue\[] | - | |
| disabled | 是否禁用 | boolean | false | | | disabled | 是否禁用 | boolean | false | |
| dropdownClassName | 下拉菜单的 className 属性 | string | - | | | popupClassName | 下拉菜单的 className 属性 | string | - | 4.23.0 |
| dropdownMatchSelectWidth | 下拉菜单和选择器同宽。默认将设置 `min-width`当值小于选择框宽度时会被忽略。false 时会关闭虚拟滚动 | boolean \| number | true | | | dropdownMatchSelectWidth | 下拉菜单和选择器同宽。默认将设置 `min-width`当值小于选择框宽度时会被忽略。false 时会关闭虚拟滚动 | boolean \| number | true | |
| dropdownRender | 自定义下拉框内容 | (originNode: ReactNode) => ReactNode | - | | | dropdownRender | 自定义下拉框内容 | (originNode: ReactNode) => ReactNode | - | |
| dropdownStyle | 下拉菜单的 style 属性 | CSSProperties | - | | | dropdownStyle | 下拉菜单的 style 属性 | CSSProperties | - | |

View File

@ -1,180 +0,0 @@
import { mount } from 'enzyme';
import React from 'react';
import Skeleton from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
describe('Skeleton', () => {
const genSkeleton = props =>
mount(
<Skeleton loading {...props}>
Bamboo
</Skeleton>,
);
const genSkeletonButton = props => mount(<Skeleton.Button {...props} />);
const genSkeletonAvatar = props => mount(<Skeleton.Avatar {...props} />);
const genSkeletonInput = props => mount(<Skeleton.Input {...props} />);
const genSkeletonImage = props => mount(<Skeleton.Image {...props} />);
const genSkeletonNode = props => mount(<Skeleton.Node {...props} />);
mountTest(Skeleton);
rtlTest(Skeleton);
it('should without avatar and paragraph', () => {
const wrapperSmall = genSkeleton({ avatar: false, paragraph: false });
expect(wrapperSmall.render()).toMatchSnapshot();
});
it('should square avatar', () => {
const wrapperSmall = genSkeleton({ avatar: true, paragraph: false });
expect(wrapperSmall.render()).toMatchSnapshot();
});
it('should round title and paragraph', () => {
const wrapperSmall = genSkeleton({ round: true, title: true, paragraph: true });
expect(wrapperSmall.render()).toMatchSnapshot();
});
it('should display without children and falsy loading props', () => {
const wrapper = mount(<Skeleton loading={false} />);
expect(wrapper.render()).toMatchSnapshot();
});
it('should display with empty children and falsy loading props', () => {
const wrapper = mount(<Skeleton loading={false}>{0}</Skeleton>);
expect(wrapper.text()).toBe('0');
});
it('should display children', () => {
const wrapper = mount(<Skeleton loading={false}>{[1, 2, 3]}</Skeleton>);
expect(wrapper.text()).toBe('123');
});
describe('avatar', () => {
it('size', () => {
const wrapperSmall = genSkeleton({ avatar: { size: 'small' } });
expect(wrapperSmall.render()).toMatchSnapshot();
const wrapperDefault = genSkeleton({ avatar: { size: 'default' } });
expect(wrapperDefault.render()).toMatchSnapshot();
const wrapperLarge = genSkeleton({ avatar: { size: 'large' } });
expect(wrapperLarge.render()).toMatchSnapshot();
const wrapperNumber = genSkeleton({ avatar: { size: 20 } });
expect(wrapperNumber.render()).toMatchSnapshot();
});
it('shape', () => {
const wrapperCircle = genSkeleton({ avatar: { shape: 'circle' } });
expect(wrapperCircle.render()).toMatchSnapshot();
const wrapperSquare = genSkeleton({ avatar: { shape: 'square' } });
expect(wrapperSquare.render()).toMatchSnapshot();
});
});
describe('title', () => {
it('width', () => {
const wrapper = genSkeleton({ title: { width: '93%' } });
expect(wrapper.render()).toMatchSnapshot();
});
});
describe('paragraph', () => {
it('rows', () => {
const wrapper = genSkeleton({ paragraph: { rows: 5 } });
expect(wrapper.render()).toMatchSnapshot();
});
it('width', () => {
const wrapperPure = genSkeleton({ paragraph: { width: '93%' } });
expect(wrapperPure.render()).toMatchSnapshot();
const wrapperList = genSkeleton({ paragraph: { width: ['28%', '93%'] } });
expect(wrapperList.render()).toMatchSnapshot();
});
});
describe('button element', () => {
it('active', () => {
const wrapper = genSkeletonButton({ active: true });
expect(wrapper.render()).toMatchSnapshot();
});
it('block', () => {
const wrapper = genSkeletonButton({ block: true });
expect(wrapper.render()).toMatchSnapshot();
});
it('size', () => {
const wrapperDefault = genSkeletonButton({ size: 'default' });
expect(wrapperDefault.render()).toMatchSnapshot();
const wrapperLarge = genSkeletonButton({ size: 'large' });
expect(wrapperLarge.render()).toMatchSnapshot();
const wrapperSmall = genSkeletonButton({ size: 'small' });
expect(wrapperSmall.render()).toMatchSnapshot();
});
it('shape', () => {
const wrapperDefault = genSkeletonButton({ shape: 'default' });
expect(wrapperDefault.render()).toMatchSnapshot();
const wrapperRound = genSkeletonButton({ shape: 'round' });
expect(wrapperRound.render()).toMatchSnapshot();
const wrapperCircle = genSkeletonButton({ shape: 'circle' });
expect(wrapperCircle.render()).toMatchSnapshot();
});
});
describe('avatar element', () => {
it('active', () => {
const wrapper = genSkeletonAvatar({ active: true });
expect(wrapper.render()).toMatchSnapshot();
});
it('size', () => {
const wrapperSmall = genSkeletonAvatar({ size: 'small' });
expect(wrapperSmall.render()).toMatchSnapshot();
const wrapperDefault = genSkeletonAvatar({ size: 'default' });
expect(wrapperDefault.render()).toMatchSnapshot();
const wrapperLarge = genSkeletonAvatar({ size: 'large' });
expect(wrapperLarge.render()).toMatchSnapshot();
const wrapperNumber = genSkeletonAvatar({ size: 20 });
expect(wrapperNumber.render()).toMatchSnapshot();
});
it('shape', () => {
const wrapperCircle = genSkeletonAvatar({ shape: 'circle' });
expect(wrapperCircle.render()).toMatchSnapshot();
const wrapperSquare = genSkeletonAvatar({ shape: 'square' });
expect(wrapperSquare.render()).toMatchSnapshot();
});
});
describe('input element', () => {
it('active', () => {
const wrapper = genSkeletonInput({ active: true });
expect(wrapper.render()).toMatchSnapshot();
});
it('size', () => {
const wrapperSmall = genSkeletonInput({ size: 'small' });
expect(wrapperSmall.render()).toMatchSnapshot();
const wrapperDefault = genSkeletonInput({ size: 'default' });
expect(wrapperDefault.render()).toMatchSnapshot();
const wrapperLarge = genSkeletonInput({ size: 'large' });
expect(wrapperLarge.render()).toMatchSnapshot();
});
});
describe('image element', () => {
it('should render normal', () => {
const wrapper = genSkeletonImage();
expect(wrapper.render()).toMatchSnapshot();
});
});
describe('custom node element', () => {
it('should render normal', () => {
const wrapper = genSkeletonNode();
expect(wrapper.render()).toMatchSnapshot();
const wrapperNode = genSkeletonNode({ children: <span>Custom Content Node</span> });
expect(wrapperNode.render()).toMatchSnapshot();
});
});
it('should support style', () => {
const wrapper = genSkeleton({ style: { background: 'blue' } });
expect(wrapper.render()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,188 @@
import React from 'react';
import Skeleton from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { render } from '../../../tests/utils';
import type { SkeletonProps } from '../Skeleton';
import type { AvatarProps } from '../Avatar';
import type { SkeletonButtonProps } from '../Button';
import type { SkeletonImageProps } from '../Image';
import type { SkeletonInputProps } from '../Input';
import type { SkeletonNodeProps } from '../Node';
describe('Skeleton', () => {
const genSkeleton = (props: SkeletonProps) =>
render(
<Skeleton loading {...props}>
Bamboo
</Skeleton>,
);
const genSkeletonButton = (props: SkeletonButtonProps) => render(<Skeleton.Button {...props} />);
const genSkeletonAvatar = (props: AvatarProps) => render(<Skeleton.Avatar {...props} />);
const genSkeletonInput = (props: SkeletonInputProps) => render(<Skeleton.Input {...props} />);
const genSkeletonImage = (props: SkeletonImageProps) => render(<Skeleton.Image {...props} />);
const genSkeletonNode = (props: SkeletonNodeProps) => render(<Skeleton.Node {...props} />);
mountTest(Skeleton);
rtlTest(Skeleton);
it('should without avatar and paragraph', () => {
const { asFragment } = genSkeleton({ avatar: false, paragraph: false });
expect(asFragment().firstChild).toMatchSnapshot();
});
it('should square avatar', () => {
const { asFragment } = genSkeleton({ avatar: true, paragraph: false });
expect(asFragment().firstChild).toMatchSnapshot();
});
it('should round title and paragraph', () => {
const { asFragment } = genSkeleton({ round: true, title: true, paragraph: true });
expect(asFragment().firstChild).toMatchSnapshot();
});
it('should display without children and falsy loading props', () => {
const { asFragment } = render(<Skeleton loading={false} />);
expect(asFragment().firstChild).toMatchSnapshot();
});
it('should display with empty children and falsy loading props', () => {
const { container } = render(<Skeleton loading={false}>{0}</Skeleton>);
expect(container.textContent).toBe('0');
});
it('should display children', () => {
const { container } = render(<Skeleton loading={false}>{[1, 2, 3]}</Skeleton>);
expect(container.textContent).toBe('123');
});
describe('avatar', () => {
it('size', () => {
const { asFragment } = genSkeleton({ avatar: { size: 'small' } });
expect(asFragment().firstChild).toMatchSnapshot();
const { asFragment: wrapperDefault } = genSkeleton({ avatar: { size: 'default' } });
expect(wrapperDefault().firstChild).toMatchSnapshot();
const { asFragment: wrapperLarge } = genSkeleton({ avatar: { size: 'large' } });
expect(wrapperLarge().firstChild).toMatchSnapshot();
const { asFragment: wrapperNumber } = genSkeleton({ avatar: { size: 20 } });
expect(wrapperNumber().firstChild).toMatchSnapshot();
});
it('shape', () => {
const { asFragment: wrapperCircle } = genSkeleton({ avatar: { shape: 'circle' } });
expect(wrapperCircle().firstChild).toMatchSnapshot();
const { asFragment: wrapperSquare } = genSkeleton({ avatar: { shape: 'square' } });
expect(wrapperSquare().firstChild).toMatchSnapshot();
});
});
describe('title', () => {
it('width', () => {
const { asFragment } = genSkeleton({ title: { width: '93%' } });
expect(asFragment().firstChild).toMatchSnapshot();
});
});
describe('paragraph', () => {
it('rows', () => {
const { asFragment } = genSkeleton({ paragraph: { rows: 5 } });
expect(asFragment().firstChild).toMatchSnapshot();
});
it('width', () => {
const { asFragment: wrapperPure } = genSkeleton({ paragraph: { width: '93%' } });
expect(wrapperPure().firstChild).toMatchSnapshot();
const { asFragment: wrapperList } = genSkeleton({ paragraph: { width: ['28%', '93%'] } });
expect(wrapperList().firstChild).toMatchSnapshot();
});
});
describe('button element', () => {
it('active', () => {
const { asFragment } = genSkeletonButton({ active: true });
expect(asFragment().firstChild).toMatchSnapshot();
});
it('block', () => {
const { asFragment } = genSkeletonButton({ block: true });
expect(asFragment().firstChild).toMatchSnapshot();
});
it('size', () => {
const { asFragment: wrapperDefault } = genSkeletonButton({ size: 'default' });
expect(wrapperDefault().firstChild).toMatchSnapshot();
const { asFragment: wrapperLarge } = genSkeletonButton({ size: 'large' });
expect(wrapperLarge().firstChild).toMatchSnapshot();
const { asFragment } = genSkeletonButton({ size: 'small' });
expect(asFragment().firstChild).toMatchSnapshot();
});
it('shape', () => {
const { asFragment: wrapperDefault } = genSkeletonButton({ shape: 'default' });
expect(wrapperDefault().firstChild).toMatchSnapshot();
const { asFragment: wrapperRound } = genSkeletonButton({ shape: 'round' });
expect(wrapperRound().firstChild).toMatchSnapshot();
const { asFragment: wrapperCircle } = genSkeletonButton({ shape: 'circle' });
expect(wrapperCircle().firstChild).toMatchSnapshot();
});
});
describe('avatar element', () => {
it('active', () => {
const { asFragment } = genSkeletonAvatar({ active: true });
expect(asFragment().firstChild).toMatchSnapshot();
});
it('size', () => {
const { asFragment } = genSkeletonAvatar({ size: 'small' });
expect(asFragment().firstChild).toMatchSnapshot();
const { asFragment: wrapperDefault } = genSkeletonAvatar({ size: 'default' });
expect(wrapperDefault().firstChild).toMatchSnapshot();
const { asFragment: wrapperLarge } = genSkeletonAvatar({ size: 'large' });
expect(wrapperLarge().firstChild).toMatchSnapshot();
const { asFragment: wrapperNumber } = genSkeletonAvatar({ size: 20 });
expect(wrapperNumber().firstChild).toMatchSnapshot();
});
it('shape', () => {
const { asFragment: wrapperCircle } = genSkeletonAvatar({ shape: 'circle' });
expect(wrapperCircle().firstChild).toMatchSnapshot();
const { asFragment: wrapperSquare } = genSkeletonAvatar({ shape: 'square' });
expect(wrapperSquare().firstChild).toMatchSnapshot();
});
});
describe('input element', () => {
it('active', () => {
const { asFragment } = genSkeletonInput({ active: true });
expect(asFragment().firstChild).toMatchSnapshot();
});
it('size', () => {
const { asFragment } = genSkeletonInput({ size: 'small' });
expect(asFragment().firstChild).toMatchSnapshot();
const { asFragment: wrapperDefault } = genSkeletonInput({ size: 'default' });
expect(wrapperDefault().firstChild).toMatchSnapshot();
const { asFragment: wrapperLarge } = genSkeletonInput({ size: 'large' });
expect(wrapperLarge().firstChild).toMatchSnapshot();
});
});
describe('image element', () => {
it('should render normal', () => {
const { asFragment } = genSkeletonImage({});
expect(asFragment().firstChild).toMatchSnapshot();
});
});
describe('custom node element', () => {
it('should render normal', () => {
const { asFragment } = genSkeletonNode({});
expect(asFragment().firstChild).toMatchSnapshot();
const { asFragment: asFragmentNode } = genSkeletonNode({
children: <span>Custom Content Node</span>,
});
expect(asFragmentNode().firstChild).toMatchSnapshot();
});
});
it('should support style', () => {
const { asFragment } = genSkeleton({ style: { background: 'blue' } });
expect(asFragment().firstChild).toMatchSnapshot();
});
});

View File

@ -1,235 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Slider rtl render component should be rendered correctly in RTL direction 1`] = `
<div
class="ant-slider ant-slider-rtl ant-slider-horizontal"
>
<div
class="ant-slider-rail"
/>
<div
class="ant-slider-track"
style="right: 0%; width: 0%;"
/>
<div
class="ant-slider-step"
/>
<div
aria-disabled="false"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="0"
class="ant-slider-handle"
role="slider"
style="right: 0%; transform: translateX(50%);"
tabindex="0"
/>
</div>
`;
exports[`Slider should render in RTL direction 1`] = `
<div
class="ant-slider ant-slider-rtl ant-slider-horizontal"
>
<div
class="ant-slider-rail"
/>
<div
class="ant-slider-track"
style="right: 0%; width: 30%;"
/>
<div
class="ant-slider-step"
/>
<div
aria-disabled="false"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="30"
class="ant-slider-handle ant-tooltip-open"
role="slider"
style="right: 30%; transform: translateX(50%);"
tabindex="0"
/>
<div>
<div
class="ant-tooltip ant-slider-tooltip ant-tooltip-rtl ant-zoom-down-appear ant-zoom-down-appear-prepare ant-zoom-down"
style="opacity: 0;"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
>
30
</div>
</div>
</div>
</div>
</div>
`;
exports[`Slider should show correct placement tooltip when set tooltipPlacement 1`] = `
Array [
<div
aria-disabled="false"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="30"
class="ant-slider-handle ant-tooltip-open"
role="slider"
style="bottom: 30%; transform: translateY(50%);"
tabindex="0"
/>,
<div>
<div
class="ant-tooltip ant-slider-tooltip ant-zoom-down-appear ant-zoom-down-appear-prepare ant-zoom-down"
style="opacity: 0;"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
>
30
</div>
</div>
</div>
</div>,
]
`;
exports[`Slider should show correct placement tooltip when set tooltipPlacement 2`] = `
Array [
<div
aria-disabled="false"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="30"
class="ant-slider-handle"
role="slider"
style="bottom: 30%; transform: translateY(50%);"
tabindex="0"
/>,
<div>
<div
class="ant-tooltip ant-slider-tooltip ant-zoom-down-leave ant-zoom-down-leave-start ant-zoom-down"
style="pointer-events: none;"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
>
30
</div>
</div>
</div>
</div>,
]
`;
exports[`Slider should show tooltip when hovering slider handler 1`] = `
Array [
<div
aria-disabled="false"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="30"
class="ant-slider-handle ant-tooltip-open"
role="slider"
style="left: 30%; transform: translateX(-50%);"
tabindex="0"
/>,
<div>
<div
class="ant-tooltip ant-slider-tooltip ant-zoom-down-appear ant-zoom-down-appear-prepare ant-zoom-down"
style="opacity: 0;"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
>
30
</div>
</div>
</div>
</div>,
]
`;
exports[`Slider should show tooltip when hovering slider handler 2`] = `
Array [
<div
aria-disabled="false"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="30"
class="ant-slider-handle"
role="slider"
style="left: 30%; transform: translateX(-50%);"
tabindex="0"
/>,
<div>
<div
class="ant-tooltip ant-slider-tooltip ant-zoom-down-leave ant-zoom-down-leave-start ant-zoom-down"
style="pointer-events: none;"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
>
30
</div>
</div>
</div>
</div>,
]
`;

View File

@ -0,0 +1,181 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Slider rtl render component should be rendered correctly in RTL direction 1`] = `
<div
class="ant-slider ant-slider-rtl ant-slider-horizontal"
>
<div
class="ant-slider-rail"
/>
<div
class="ant-slider-track"
style="right: 0%; width: 0%;"
/>
<div
class="ant-slider-step"
/>
<div
aria-disabled="false"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="0"
class="ant-slider-handle"
role="slider"
style="right: 0%; transform: translateX(50%);"
tabindex="0"
/>
</div>
`;
exports[`Slider should render in RTL direction 1`] = `
<div>
<div
class="ant-slider ant-slider-rtl ant-slider-horizontal"
>
<div
class="ant-slider-rail"
/>
<div
class="ant-slider-track"
style="right: 0%; width: 30%;"
/>
<div
class="ant-slider-step"
/>
<div
aria-disabled="false"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="30"
class="ant-slider-handle ant-tooltip-open"
role="slider"
style="right: 30%; transform: translateX(50%);"
tabindex="0"
/>
<div>
<div
class="ant-tooltip ant-slider-tooltip ant-tooltip-rtl ant-zoom-down-appear ant-zoom-down-appear-prepare ant-zoom-down"
style="opacity: 0;"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
>
30
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Slider should show correct placement tooltip when set tooltipPlacement 1`] = `
<div
class="ant-tooltip ant-slider-tooltip ant-zoom-down-appear ant-zoom-down-appear-prepare ant-zoom-down"
style="opacity: 0;"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
>
30
</div>
</div>
</div>
`;
exports[`Slider should show correct placement tooltip when set tooltipPlacement 2`] = `
<div
class="ant-tooltip ant-slider-tooltip ant-zoom-down-leave ant-zoom-down-leave-start ant-zoom-down"
style="pointer-events: none;"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
>
30
</div>
</div>
</div>
`;
exports[`Slider should show tooltip when hovering slider handler 1`] = `
<div
class="ant-tooltip ant-slider-tooltip ant-zoom-down-appear ant-zoom-down-appear-prepare ant-zoom-down"
style="opacity: 0;"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
>
30
</div>
</div>
</div>
`;
exports[`Slider should show tooltip when hovering slider handler 2`] = `
<div
class="ant-tooltip ant-slider-tooltip ant-zoom-down-leave ant-zoom-down-leave-start ant-zoom-down"
style="pointer-events: none;"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
>
30
</div>
</div>
</div>
`;

View File

@ -1,116 +0,0 @@
import { mount } from 'enzyme';
import React from 'react';
import Slider from '..';
import focusTest from '../../../tests/shared/focusTest';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { sleep } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import SliderTooltip from '../SliderTooltip';
describe('Slider', () => {
mountTest(Slider);
rtlTest(Slider);
focusTest(Slider, { testLib: true });
it('should show tooltip when hovering slider handler', () => {
const wrapper = mount(<Slider defaultValue={30} />);
wrapper.find('.ant-slider-handle').at(0).simulate('mouseEnter');
expect(wrapper.find('Trigger').render()).toMatchSnapshot();
wrapper.find('.ant-slider-handle').at(0).simulate('mouseLeave');
expect(wrapper.find('Trigger').render()).toMatchSnapshot();
});
it('should show correct placement tooltip when set tooltipPlacement', () => {
const wrapper = mount(<Slider vertical defaultValue={30} tooltipPlacement="left" />);
wrapper.find('.ant-slider-handle').at(0).simulate('mouseEnter');
expect(wrapper.find('Trigger').render()).toMatchSnapshot();
wrapper.find('.ant-slider-handle').at(0).simulate('mouseLeave');
expect(wrapper.find('Trigger').render()).toMatchSnapshot();
});
it('when tooltipVisible is true, tooltip should show always, or should never show', () => {
let wrapper = mount(<Slider defaultValue={30} tooltipVisible />);
expect(wrapper.find('.ant-tooltip-content').at(0).hasClass('ant-tooltip-hidden')).toBe(false);
wrapper.find('.ant-slider-handle').at(0).simulate('mouseEnter');
expect(wrapper.find('.ant-tooltip-content').at(0).hasClass('ant-tooltip-hidden')).toBe(false);
wrapper.find('.ant-slider-handle').at(0).simulate('click');
expect(wrapper.find('.ant-tooltip-content').at(0).hasClass('ant-tooltip-hidden')).toBe(false);
wrapper = mount(<Slider defaultValue={30} tooltipVisible={false} />);
expect(wrapper.find('.ant-tooltip-content').length).toBe(0);
});
it('when step is null, thumb can only be slided to the specific mark', () => {
const intentionallyWrongValue = 40;
const marks = {
0: '0',
48: '48',
100: '100',
};
const wrapper = mount(
<Slider marks={marks} defaultValue={intentionallyWrongValue} step={null} tooltipVisible />,
);
expect(wrapper.find('.ant-slider-handle').get(0).props).toHaveProperty('aria-valuenow', 48);
});
it('when step is not null, thumb can be slided to the multiples of step', () => {
const marks = {
0: '0',
48: '48',
100: '100',
};
const wrapper = mount(<Slider marks={marks} defaultValue={49} step={1} tooltipVisible />);
expect(wrapper.find('.ant-slider-handle').get(0).props).toHaveProperty('aria-valuenow', 49);
});
it('when step is undefined, thumb can be slided to the multiples of step', () => {
const marks = {
0: '0',
48: '48',
100: '100',
};
const wrapper = mount(
<Slider marks={marks} defaultValue={49} step={undefined} tooltipVisible />,
);
expect(wrapper.find('.ant-slider-handle').get(0).props).toHaveProperty('aria-valuenow', 49);
});
it('should render in RTL direction', () => {
const wrapper = mount(
<ConfigProvider direction="rtl">
<Slider defaultValue={30} tooltipVisible />
</ConfigProvider>,
);
expect(wrapper.render()).toMatchSnapshot();
});
it('should keepAlign by calling forcePopupAlign', async () => {
let ref;
mount(
<SliderTooltip
title="30"
visible
ref={node => {
ref = node;
}}
/>,
);
ref.forcePopupAlign = jest.fn();
await sleep(20);
expect(ref.forcePopupAlign).toHaveBeenCalled();
});
it('tipFormatter should not crash with undefined value', () => {
[undefined, null].forEach(value => {
mount(<Slider value={value} tooltipVisible />);
});
});
it('step should not crash with undefined value', () => {
[undefined, null].forEach(value => {
mount(<Slider step={value} tooltipVisible />);
});
});
});

View File

@ -0,0 +1,131 @@
import React from 'react';
import Slider from '..';
import focusTest from '../../../tests/shared/focusTest';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { sleep, render, fireEvent } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import SliderTooltip from '../SliderTooltip';
describe('Slider', () => {
mountTest(Slider);
rtlTest(Slider);
focusTest(Slider, { testLib: true });
it('should show tooltip when hovering slider handler', () => {
const { container } = render(<Slider defaultValue={30} />);
fireEvent.mouseEnter(container.querySelector('.ant-slider-handle')!);
expect(document.querySelector('.ant-tooltip')).toMatchSnapshot();
fireEvent.mouseLeave(container.querySelector('.ant-slider-handle')!);
expect(document.querySelector('.ant-tooltip')).toMatchSnapshot();
});
it('should show correct placement tooltip when set tooltipPlacement', () => {
const { container } = render(<Slider vertical defaultValue={30} tooltipPlacement="left" />);
fireEvent.mouseEnter(container.querySelector('.ant-slider-handle')!);
expect(document.querySelector('.ant-tooltip')).toMatchSnapshot();
fireEvent.mouseLeave(container.querySelector('.ant-slider-handle')!);
expect(document.querySelector('.ant-tooltip')).toMatchSnapshot();
});
it('when tooltipVisible is true, tooltip should show always, or should never show', () => {
const { container: container1 } = render(<Slider defaultValue={30} tooltipVisible />);
expect(
container1.querySelector('.ant-tooltip-content')!.className.includes('ant-tooltip-hidden'),
).toBeFalsy();
fireEvent.mouseEnter(container1.querySelector('.ant-slider-handle')!);
expect(
container1.querySelector('.ant-tooltip-content')!.className.includes('ant-tooltip-hidden'),
).toBeFalsy();
fireEvent.click(container1.querySelector('.ant-slider-handle')!);
expect(
container1.querySelector('.ant-tooltip-content')!.className.includes('ant-tooltip-hidden'),
).toBeFalsy();
const { container: container2 } = render(<Slider defaultValue={30} tooltipVisible={false} />);
expect(container2.querySelector('.ant-tooltip-content')!).toBeNull();
});
it('when step is null, thumb can only be slided to the specific mark', () => {
const intentionallyWrongValue = 40;
const marks = {
0: '0',
48: '48',
100: '100',
};
const { container } = render(
<Slider marks={marks} defaultValue={intentionallyWrongValue} step={null} tooltipVisible />,
);
expect(container.querySelector('.ant-slider-handle')!.getAttribute('aria-valuenow')).toBe('48');
});
it('when step is not null, thumb can be slided to the multiples of step', () => {
const marks = {
0: '0',
48: '48',
100: '100',
};
const { container } = render(
<Slider marks={marks} defaultValue={49} step={1} tooltipVisible />,
);
expect(container.querySelector('.ant-slider-handle')!.getAttribute('aria-valuenow')).toBe('49');
});
it('when step is undefined, thumb can be slided to the multiples of step', () => {
const marks = {
0: '0',
48: '48',
100: '100',
};
const { container } = render(
<Slider marks={marks} defaultValue={49} step={undefined} tooltipVisible />,
);
expect(container.querySelector('.ant-slider-handle')!.getAttribute('aria-valuenow')).toBe('49');
});
it('should render in RTL direction', () => {
const { container } = render(
<ConfigProvider direction="rtl">
<Slider defaultValue={30} tooltipVisible />
</ConfigProvider>,
);
expect(container).toMatchSnapshot();
});
it('should keepAlign by calling forcePopupAlign', async () => {
let ref: any;
render(
<SliderTooltip
title="30"
visible
ref={node => {
ref = node;
}}
/>,
);
ref.forcePopupAlign = jest.fn();
await sleep(20);
expect(ref.forcePopupAlign).toHaveBeenCalled();
});
it('tipFormatter should not crash with undefined value', () => {
[undefined, null].forEach(value => {
render(<Slider value={value as any} tooltipVisible />);
});
});
it('step should not crash with undefined value', () => {
[undefined, null].forEach(value => {
render(<Slider step={value} tooltipVisible />);
});
});
});

View File

@ -63,7 +63,7 @@ interface ChangeEventInfo<RecordType> {
total?: number; total?: number;
}; };
filters: Record<string, FilterValue | null>; filters: Record<string, FilterValue | null>;
sorter: SorterResult<RecordType> | SorterResult<RecordType>[]; sorter: SorterResult<RecordType> | SorterResult<RecordType[]>;
filterStates: FilterState<RecordType>[]; filterStates: FilterState<RecordType>[];
sorterStates: SortState<RecordType>[]; sorterStates: SortState<RecordType>[];
@ -94,7 +94,7 @@ export interface TableProps<RecordType>
onChange?: ( onChange?: (
pagination: TablePaginationConfig, pagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>, filters: Record<string, FilterValue | null>,
sorter: SorterResult<RecordType> | SorterResult<RecordType>[], sorter: SorterResult<RecordType> | SorterResult<RecordType[]>,
extra: TableCurrentDataSource<RecordType>, extra: TableCurrentDataSource<RecordType>,
) => void; ) => void;
rowSelection?: TableRowSelection<RecordType>; rowSelection?: TableRowSelection<RecordType>;
@ -265,7 +265,7 @@ function InternalTable<RecordType extends object = any>(
// ============================ Sorter ============================= // ============================ Sorter =============================
const onSorterChange = ( const onSorterChange = (
sorter: SorterResult<RecordType> | SorterResult<RecordType>[], sorter: SorterResult<RecordType> | SorterResult<RecordType[]>,
sorterStates: SortState<RecordType>[], sorterStates: SortState<RecordType>[],
) => { ) => {
triggerOnChange( triggerOnChange(

View File

@ -36,6 +36,12 @@ describe('Table.typescript', () => {
const table = <Table<RecordType> dataSource={[{ key: 'Bamboo' }]} />; const table = <Table<RecordType> dataSource={[{ key: 'Bamboo' }]} />;
expect(table).toBeTruthy(); expect(table).toBeTruthy();
}); });
it('Sorter types', () => {
const table = <Table onChange={(_pagination, _filters, sorter) => sorter.field} />;
expect(table).toBeTruthy();
});
}); });
describe('Table.typescript types', () => { describe('Table.typescript types', () => {

View File

@ -247,7 +247,7 @@ function stateToInfo<RecordType>(sorterStates: SortState<RecordType>) {
function generateSorterInfo<RecordType>( function generateSorterInfo<RecordType>(
sorterStates: SortState<RecordType>[], sorterStates: SortState<RecordType>[],
): SorterResult<RecordType> | SorterResult<RecordType>[] { ): SorterResult<RecordType> | SorterResult<RecordType[]> {
const list = sorterStates.filter(({ sortOrder }) => sortOrder).map(stateToInfo); const list = sorterStates.filter(({ sortOrder }) => sortOrder).map(stateToInfo);
// =========== Legacy compatible support =========== // =========== Legacy compatible support ===========
@ -259,11 +259,7 @@ function generateSorterInfo<RecordType>(
}; };
} }
if (list.length <= 1) { return list[0] || {};
return list[0] || {};
}
return list;
} }
export function getSortData<RecordType>( export function getSortData<RecordType>(
@ -324,7 +320,7 @@ interface SorterConfig<RecordType> {
prefixCls: string; prefixCls: string;
mergedColumns: ColumnsType<RecordType>; mergedColumns: ColumnsType<RecordType>;
onSorterChange: ( onSorterChange: (
sorterResult: SorterResult<RecordType> | SorterResult<RecordType>[], sorterResult: SorterResult<RecordType> | SorterResult<RecordType[]>,
sortStates: SortState<RecordType>[], sortStates: SortState<RecordType>[],
) => void; ) => void;
sortDirections: SortOrder[]; sortDirections: SortOrder[];
@ -343,7 +339,7 @@ export default function useFilterSorter<RecordType>({
TransformColumns<RecordType>, TransformColumns<RecordType>,
SortState<RecordType>[], SortState<RecordType>[],
ColumnTitleProps<RecordType>, ColumnTitleProps<RecordType>,
() => SorterResult<RecordType> | SorterResult<RecordType>[], () => SorterResult<RecordType> | SorterResult<RecordType[]>,
] { ] {
const [sortStates, setSortStates] = React.useState<SortState<RecordType>[]>( const [sortStates, setSortStates] = React.useState<SortState<RecordType>[]>(
collectSortStates(mergedColumns, true), collectSortStates(mergedColumns, true),

View File

@ -177,6 +177,7 @@ Properties for expandable.
| Property | Description | Type | Default | Version | | Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| childrenColumnName | The column contains children to display | string | children | | | childrenColumnName | The column contains children to display | string | children | |
| columnTitle | Set the title of the expand column | ReactNode | - | 4.23.0 |
| columnWidth | Set the width of the expand column | string \| number | - | | | columnWidth | Set the width of the expand column | string \| number | - | |
| defaultExpandAllRows | Expand all rows initially | boolean | false | | | defaultExpandAllRows | Expand all rows initially | boolean | false | |
| defaultExpandedRowKeys | Initial expanded row keys | string\[] | - | | | defaultExpandedRowKeys | Initial expanded row keys | string\[] | - | |

View File

@ -178,6 +178,7 @@ const columns = [
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| childrenColumnName | 指定树形结构的列名 | string | children | | | childrenColumnName | 指定树形结构的列名 | string | children | |
| columnTitle | 自定义展开列表头 | ReactNode | - | 4.23.0 |
| columnWidth | 自定义展开列宽度 | string \| number | - | | | columnWidth | 自定义展开列宽度 | string \| number | - | |
| defaultExpandAllRows | 初始时,是否展开所有行 | boolean | false | | | defaultExpandAllRows | 初始时,是否展开所有行 | boolean | false | |
| defaultExpandedRowKeys | 默认展开的行 | string\[] | - | | | defaultExpandedRowKeys | 默认展开的行 | string\[] | - | |

View File

@ -64,7 +64,6 @@ const genExpandStyle: GenerateStyle<TableToken, CSSObject> = token => {
background: tableExpandIconBg, background: tableExpandIconBg,
border: tableBorder, border: tableBorder,
borderRadius: radiusBase, borderRadius: radiusBase,
outline: 'none',
transform: `scale(${checkboxSize / expandIconSize})`, transform: `scale(${checkboxSize / expandIconSize})`,
transition: `all ${motionDurationSlow}`, transition: `all ${motionDurationSlow}`,
userSelect: 'none', userSelect: 'none',

View File

@ -0,0 +1,12 @@
import type * as React from 'react';
import type { TabPaneProps } from 'rc-tabs/lib/TabPanelList/TabPane';
const TabPane: React.FC<TabPaneProps> = () => null;
if (process.env.NODE_ENV !== 'production') {
TabPane.displayName = 'DeprecatedTabPane';
}
export { TabPaneProps };
export default TabPane;

View File

@ -1088,6 +1088,150 @@ exports[`renders ./components/tabs/demo/custom-tab-bar-node.md extend context co
</div> </div>
`; `;
exports[`renders ./components/tabs/demo/deprecated.md extend context correctly 1`] = `
<div
class="ant-tabs ant-tabs-top"
>
<div
class="ant-tabs-nav"
role="tablist"
>
<div
class="ant-tabs-nav-wrap"
>
<div
class="ant-tabs-nav-list"
style="transform:translate(0px, 0px)"
>
<div
class="ant-tabs-tab ant-tabs-tab-active"
>
<div
aria-selected="true"
class="ant-tabs-tab-btn"
role="tab"
tabindex="0"
>
Tab 1
</div>
</div>
<div
class="ant-tabs-tab"
>
<div
aria-selected="false"
class="ant-tabs-tab-btn"
role="tab"
tabindex="0"
>
Tab 2
</div>
</div>
<div
class="ant-tabs-tab"
>
<div
aria-selected="false"
class="ant-tabs-tab-btn"
role="tab"
tabindex="0"
>
Tab 3
</div>
</div>
<div
class="ant-tabs-ink-bar ant-tabs-ink-bar-animated"
/>
</div>
</div>
<div
class="ant-tabs-nav-operations ant-tabs-nav-operations-hidden"
>
<button
aria-controls="null-more-popup"
aria-expanded="false"
aria-haspopup="listbox"
aria-hidden="true"
class="ant-tabs-nav-more"
id="null-more"
style="visibility:hidden;order:1"
tabindex="-1"
type="button"
>
<span
aria-label="ellipsis"
class="anticon anticon-ellipsis"
role="img"
>
<svg
aria-hidden="true"
data-icon="ellipsis"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"
/>
</svg>
</span>
</button>
<div>
<div
class="ant-tabs-dropdown"
style="opacity:0"
>
<ul
aria-label="expanded dropdown"
class="ant-tabs-dropdown-menu ant-tabs-dropdown-menu-root ant-tabs-dropdown-menu-vertical"
data-menu-list="true"
id="null-more-popup"
role="listbox"
tabindex="-1"
/>
<div
aria-hidden="true"
style="display:none"
/>
</div>
</div>
</div>
</div>
<div
class="ant-tabs-content-holder"
>
<div
class="ant-tabs-content ant-tabs-content-top"
>
<div
aria-hidden="false"
class="ant-tabs-tabpane ant-tabs-tabpane-active"
role="tabpanel"
tabindex="0"
>
Content of Tab Pane 1
</div>
<div
aria-hidden="true"
class="ant-tabs-tabpane"
role="tabpanel"
style="display:none"
tabindex="-1"
/>
<div
aria-hidden="true"
class="ant-tabs-tabpane"
role="tabpanel"
style="display:none"
tabindex="-1"
/>
</div>
</div>
</div>
`;
exports[`renders ./components/tabs/demo/disabled.md extend context correctly 1`] = ` exports[`renders ./components/tabs/demo/disabled.md extend context correctly 1`] = `
<div <div
class="ant-tabs ant-tabs-top" class="ant-tabs ant-tabs-top"

View File

@ -955,6 +955,131 @@ exports[`renders ./components/tabs/demo/custom-tab-bar-node.md correctly 1`] = `
</div> </div>
`; `;
exports[`renders ./components/tabs/demo/deprecated.md correctly 1`] = `
<div
class="ant-tabs ant-tabs-top"
>
<div
class="ant-tabs-nav"
role="tablist"
>
<div
class="ant-tabs-nav-wrap"
>
<div
class="ant-tabs-nav-list"
style="transform:translate(0px, 0px)"
>
<div
class="ant-tabs-tab ant-tabs-tab-active"
>
<div
aria-selected="true"
class="ant-tabs-tab-btn"
role="tab"
tabindex="0"
>
Tab 1
</div>
</div>
<div
class="ant-tabs-tab"
>
<div
aria-selected="false"
class="ant-tabs-tab-btn"
role="tab"
tabindex="0"
>
Tab 2
</div>
</div>
<div
class="ant-tabs-tab"
>
<div
aria-selected="false"
class="ant-tabs-tab-btn"
role="tab"
tabindex="0"
>
Tab 3
</div>
</div>
<div
class="ant-tabs-ink-bar ant-tabs-ink-bar-animated"
/>
</div>
</div>
<div
class="ant-tabs-nav-operations ant-tabs-nav-operations-hidden"
>
<button
aria-controls="null-more-popup"
aria-expanded="false"
aria-haspopup="listbox"
aria-hidden="true"
class="ant-tabs-nav-more"
id="null-more"
style="visibility:hidden;order:1"
tabindex="-1"
type="button"
>
<span
aria-label="ellipsis"
class="anticon anticon-ellipsis"
role="img"
>
<svg
aria-hidden="true"
data-icon="ellipsis"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"
/>
</svg>
</span>
</button>
</div>
</div>
<div
class="ant-tabs-content-holder"
>
<div
class="ant-tabs-content ant-tabs-content-top"
>
<div
aria-hidden="false"
class="ant-tabs-tabpane ant-tabs-tabpane-active"
role="tabpanel"
tabindex="0"
>
Content of Tab Pane 1
</div>
<div
aria-hidden="true"
class="ant-tabs-tabpane"
role="tabpanel"
style="display:none"
tabindex="-1"
/>
<div
aria-hidden="true"
class="ant-tabs-tabpane"
role="tabpanel"
style="display:none"
tabindex="-1"
/>
</div>
</div>
</div>
`;
exports[`renders ./components/tabs/demo/disabled.md correctly 1`] = ` exports[`renders ./components/tabs/demo/disabled.md correctly 1`] = `
<div <div
class="ant-tabs ant-tabs-top" class="ant-tabs ant-tabs-top"

View File

@ -3,6 +3,7 @@ import Tabs from '..';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest'; import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render } from '../../../tests/utils'; import { fireEvent, render } from '../../../tests/utils';
import { resetWarned } from '../../_util/warning';
const { TabPane } = Tabs; const { TabPane } = Tabs;
@ -105,4 +106,23 @@ describe('Tabs', () => {
); );
expect(wrapper2.firstChild).toMatchSnapshot(); expect(wrapper2.firstChild).toMatchSnapshot();
}); });
it('deprecated warning', () => {
resetWarned();
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const { container } = render(
<Tabs>
<TabPane />
invalidate
</Tabs>,
);
expect(container.querySelectorAll('.ant-tabs-tab')).toHaveLength(1);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Tabs] Tabs.TabPane is deprecated. Please use `items` directly.',
);
errorSpy.mockRestore();
});
}); });

View File

@ -17,24 +17,32 @@ Default activate first tab.
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import React from 'react'; import React from 'react';
const { TabPane } = Tabs;
const onChange = (key: string) => { const onChange = (key: string) => {
console.log(key); console.log(key);
}; };
const App: React.FC = () => ( const App: React.FC = () => (
<Tabs defaultActiveKey="1" onChange={onChange}> <Tabs
<TabPane tab="Tab 1" key="1"> defaultActiveKey="1"
Content of Tab Pane 1 onChange={onChange}
</TabPane> items={[
<TabPane tab="Tab 2" key="2"> {
Content of Tab Pane 2 label: `Tab 1`,
</TabPane> key: '1',
<TabPane tab="Tab 3" key="3"> children: `Content of Tab Pane 1`,
Content of Tab Pane 3 },
</TabPane> {
</Tabs> label: `Tab 2`,
key: '2',
children: `Content of Tab Pane 2`,
},
{
label: `Tab 3`,
key: '3',
children: `Content of Tab Pane 3`,
},
]}
/>
); );
export default App; export default App;

View File

@ -17,27 +17,24 @@ Should be used at the top of container, needs to override styles.
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import React from 'react'; import React from 'react';
const { TabPane } = Tabs; const items = new Array(3).fill(null).map((_, i) => {
const id = String(i + 1);
return {
label: `Tab Title ${id}`,
key: id,
children: (
<>
<p>Content of Tab Pane {id}</p>
<p>Content of Tab Pane {id}</p>
<p>Content of Tab Pane {id}</p>
</>
),
};
});
const App: React.FC = () => ( const App: React.FC = () => (
<div className="card-container"> <div className="card-container">
<Tabs type="card"> <Tabs type="card" items={items} />
<TabPane tab="Tab Title 1" key="1">
<p>Content of Tab Pane 1</p>
<p>Content of Tab Pane 1</p>
<p>Content of Tab Pane 1</p>
</TabPane>
<TabPane tab="Tab Title 2" key="2">
<p>Content of Tab Pane 2</p>
<p>Content of Tab Pane 2</p>
<p>Content of Tab Pane 2</p>
</TabPane>
<TabPane tab="Tab Title 3" key="3">
<p>Content of Tab Pane 3</p>
<p>Content of Tab Pane 3</p>
<p>Content of Tab Pane 3</p>
</TabPane>
</Tabs>
</div> </div>
); );

View File

@ -17,24 +17,23 @@ Another type of Tabs, which doesn't support vertical mode.
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import React from 'react'; import React from 'react';
const { TabPane } = Tabs;
const onChange = (key: string) => { const onChange = (key: string) => {
console.log(key); console.log(key);
}; };
const App: React.FC = () => ( const App: React.FC = () => (
<Tabs onChange={onChange} type="card"> <Tabs
<TabPane tab="Tab 1" key="1"> onChange={onChange}
Content of Tab Pane 1 type="card"
</TabPane> items={new Array(3).fill(null).map((_, i) => {
<TabPane tab="Tab 2" key="2"> const id = String(i + 1);
Content of Tab Pane 2 return {
</TabPane> label: `Tab ${id}`,
<TabPane tab="Tab 3" key="3"> key: id,
Content of Tab Pane 3 children: `Content of Tab Pane ${id}`,
</TabPane> };
</Tabs> })}
/>
); );
export default App; export default App;

View File

@ -17,20 +17,19 @@ Centered tabs.
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import React from 'react'; import React from 'react';
const { TabPane } = Tabs;
const App: React.FC = () => ( const App: React.FC = () => (
<Tabs defaultActiveKey="1" centered> <Tabs
<TabPane tab="Tab 1" key="1"> defaultActiveKey="1"
Content of Tab Pane 1 centered
</TabPane> items={new Array(3).fill(null).map((_, i) => {
<TabPane tab="Tab 2" key="2"> const id = String(i + 1);
Content of Tab Pane 2 return {
</TabPane> label: `Tab ${id}`,
<TabPane tab="Tab 3" key="3"> key: id,
Content of Tab Pane 3 children: `Content of Tab Pane ${id}`,
</TabPane> };
</Tabs> })}
/>
); );
export default App; export default App;

View File

@ -17,16 +17,14 @@ Hide default plus icon, and bind event for customized trigger.
import { Button, Tabs } from 'antd'; import { Button, Tabs } from 'antd';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
const { TabPane } = Tabs; const defaultPanes = new Array(2).fill(null).map((_, index) => {
const defaultPanes = Array.from({ length: 2 }).map((_, index) => {
const id = String(index + 1); const id = String(index + 1);
return { title: `Tab ${id}`, content: `Content of Tab Pane ${index + 1}`, key: id }; return { label: `Tab ${id}`, children: `Content of Tab Pane ${index + 1}`, key: id };
}); });
const App: React.FC = () => { const App: React.FC = () => {
const [activeKey, setActiveKey] = useState(defaultPanes[0].key); const [activeKey, setActiveKey] = useState(defaultPanes[0].key);
const [panes, setPanes] = useState(defaultPanes); const [items, setItems] = useState(defaultPanes);
const newTabIndex = useRef(0); const newTabIndex = useRef(0);
const onChange = (key: string) => { const onChange = (key: string) => {
@ -35,18 +33,18 @@ const App: React.FC = () => {
const add = () => { const add = () => {
const newActiveKey = `newTab${newTabIndex.current++}`; const newActiveKey = `newTab${newTabIndex.current++}`;
setPanes([...panes, { title: 'New Tab', content: 'New Tab Pane', key: newActiveKey }]); setItems([...items, { label: 'New Tab', children: 'New Tab Pane', key: newActiveKey }]);
setActiveKey(newActiveKey); setActiveKey(newActiveKey);
}; };
const remove = (targetKey: string) => { const remove = (targetKey: string) => {
const targetIndex = panes.findIndex(pane => pane.key === targetKey); const targetIndex = items.findIndex(pane => pane.key === targetKey);
const newPanes = panes.filter(pane => pane.key !== targetKey); const newPanes = items.filter(pane => pane.key !== targetKey);
if (newPanes.length && targetKey === activeKey) { if (newPanes.length && targetKey === activeKey) {
const { key } = newPanes[targetIndex === newPanes.length ? targetIndex - 1 : targetIndex]; const { key } = newPanes[targetIndex === newPanes.length ? targetIndex - 1 : targetIndex];
setActiveKey(key); setActiveKey(key);
} }
setPanes(newPanes); setItems(newPanes);
}; };
const onEdit = (targetKey: string, action: 'add' | 'remove') => { const onEdit = (targetKey: string, action: 'add' | 'remove') => {
@ -62,13 +60,14 @@ const App: React.FC = () => {
<div style={{ marginBottom: 16 }}> <div style={{ marginBottom: 16 }}>
<Button onClick={add}>ADD</Button> <Button onClick={add}>ADD</Button>
</div> </div>
<Tabs hideAdd onChange={onChange} activeKey={activeKey} type="editable-card" onEdit={onEdit}> <Tabs
{panes.map(pane => ( hideAdd
<TabPane tab={pane.title} key={pane.key}> onChange={onChange}
{pane.content} activeKey={activeKey}
</TabPane> type="editable-card"
))} onEdit={onEdit}
</Tabs> items={items}
/>
</div> </div>
); );
}; };

View File

@ -20,8 +20,6 @@ import React, { useRef, useState } from 'react';
import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
const { TabPane } = Tabs;
const type = 'DraggableTabNode'; const type = 'DraggableTabNode';
interface DraggableTabPaneProps extends React.HTMLAttributes<HTMLDivElement> { interface DraggableTabPaneProps extends React.HTMLAttributes<HTMLDivElement> {
index: React.Key; index: React.Key;
@ -62,16 +60,16 @@ const DraggableTabNode = ({ index, children, moveNode }: DraggableTabPaneProps)
); );
}; };
const DraggableTabs: React.FC<{ children: React.ReactNode }> = props => { const DraggableTabs: React.FC<TabsProps> = props => {
const { children } = props; const { items = [] } = props;
const [order, setOrder] = useState<React.Key[]>([]); const [order, setOrder] = useState<React.Key[]>([]);
const moveTabNode = (dragKey: React.Key, hoverKey: React.Key) => { const moveTabNode = (dragKey: React.Key, hoverKey: React.Key) => {
const newOrder = order.slice(); const newOrder = order.slice();
React.Children.forEach(children, (c: React.ReactElement) => { items.forEach(item => {
if (c.key && newOrder.indexOf(c.key) === -1) { if (item.key && newOrder.indexOf(item.key) === -1) {
newOrder.push(c.key); newOrder.push(item.key);
} }
}); });
@ -94,12 +92,7 @@ const DraggableTabs: React.FC<{ children: React.ReactNode }> = props => {
</DefaultTabBar> </DefaultTabBar>
); );
const tabs: React.ReactElement[] = []; const orderItems = [...items].sort((a, b) => {
React.Children.forEach(children, (c: React.ReactElement) => {
tabs.push(c);
});
const orderTabs = tabs.slice().sort((a, b) => {
const orderA = order.indexOf(a.key!); const orderA = order.indexOf(a.key!);
const orderB = order.indexOf(b.key!); const orderB = order.indexOf(b.key!);
@ -113,33 +106,30 @@ const DraggableTabs: React.FC<{ children: React.ReactNode }> = props => {
return 1; return 1;
} }
const ia = tabs.indexOf(a); const ia = items.indexOf(a);
const ib = tabs.indexOf(b); const ib = items.indexOf(b);
return ia - ib; return ia - ib;
}); });
return ( return (
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<Tabs renderTabBar={renderTabBar} {...props}> <Tabs renderTabBar={renderTabBar} {...props} items={orderItems} />
{orderTabs}
</Tabs>
</DndProvider> </DndProvider>
); );
}; };
const App: React.FC = () => ( const App: React.FC = () => (
<DraggableTabs> <DraggableTabs
<TabPane tab="tab 1" key="1"> items={new Array(3).fill(null).map((_, i) => {
Content of Tab Pane 1 const id = String(i + 1);
</TabPane> return {
<TabPane tab="tab 2" key="2"> label: `tab ${id}`,
Content of Tab Pane 2 key: id,
</TabPane> children: `Content of Tab Pane ${id}`,
<TabPane tab="tab 3" key="3"> };
Content of Tab Pane 3 })}
</TabPane> />
</DraggableTabs>
); );
export default App; export default App;

View File

@ -19,8 +19,6 @@ import { Tabs } from 'antd';
import React from 'react'; import React from 'react';
import { Sticky, StickyContainer } from 'react-sticky'; import { Sticky, StickyContainer } from 'react-sticky';
const { TabPane } = Tabs;
const renderTabBar: TabsProps['renderTabBar'] = (props, DefaultTabBar) => ( const renderTabBar: TabsProps['renderTabBar'] = (props, DefaultTabBar) => (
<Sticky bottomOffset={80}> <Sticky bottomOffset={80}>
{({ style }) => ( {({ style }) => (
@ -29,19 +27,19 @@ const renderTabBar: TabsProps['renderTabBar'] = (props, DefaultTabBar) => (
</Sticky> </Sticky>
); );
const items = new Array(3).fill(null).map((_, i) => {
const id = String(i + 1);
return {
label: `Tab ${id}`,
key: id,
children: `Content of Tab Pane ${id}`,
style: i === 0 ? { height: 200 } : undefined,
};
});
const App: React.FC = () => ( const App: React.FC = () => (
<StickyContainer> <StickyContainer>
<Tabs defaultActiveKey="1" renderTabBar={renderTabBar}> <Tabs defaultActiveKey="1" renderTabBar={renderTabBar} items={items} />
<TabPane tab="Tab 1" key="1" style={{ height: 200 }}>
Content of Tab Pane 1
</TabPane>
<TabPane tab="Tab 2" key="2">
Content of Tab Pane 2
</TabPane>
<TabPane tab="Tab 3" key="3">
Content of Tab Pane 3
</TabPane>
</Tabs>
</StickyContainer> </StickyContainer>
); );

View File

@ -0,0 +1,36 @@
---
order: -1
title:
zh-CN: 基础用法(废弃的语法糖)
en-US: Basic usage (deprecated syntactic sugar)
version: < 4.23.0
---
## zh-CN
默认选中第一项。
## en-US
Default activate first tab.
```tsx
import { Tabs } from 'antd';
import React from 'react';
const App: React.FC = () => (
<Tabs defaultActiveKey="1">
<Tabs.TabPane tab="Tab 1" key="1">
Content of Tab Pane 1
</Tabs.TabPane>
<Tabs.TabPane tab="Tab 2" key="2">
Content of Tab Pane 2
</Tabs.TabPane>
<Tabs.TabPane tab="Tab 3" key="3">
Content of Tab Pane 3
</Tabs.TabPane>
</Tabs>
);
export default App;
```

View File

@ -17,20 +17,28 @@ Disabled a tab.
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import React from 'react'; import React from 'react';
const { TabPane } = Tabs;
const App: React.FC = () => ( const App: React.FC = () => (
<Tabs defaultActiveKey="1"> <Tabs
<TabPane tab="Tab 1" key="1"> defaultActiveKey="1"
Tab 1 items={[
</TabPane> {
<TabPane tab="Tab 2" disabled key="2"> label: 'Tab 1',
Tab 2 key: '1',
</TabPane> children: 'Tab 1',
<TabPane tab="Tab 3" key="3"> },
Tab 3 {
</TabPane> label: 'Tab 2',
</Tabs> key: '2',
children: 'Tab 2',
disabled: true,
},
{
label: 'Tab 3',
key: '3',
children: 'Tab 3',
},
]}
/>
); );
export default App; export default App;

View File

@ -17,22 +17,20 @@ Only card type Tabs support adding & closable. +Use `closable={false}` to disabl
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
const { TabPane } = Tabs; const initialItems = [
{ label: 'Tab 1', children: 'Content of Tab 1', key: '1' },
const initialPanes = [ { label: 'Tab 2', children: 'Content of Tab 2', key: '2' },
{ title: 'Tab 1', content: 'Content of Tab 1', key: '1' },
{ title: 'Tab 2', content: 'Content of Tab 2', key: '2' },
{ {
title: 'Tab 3', label: 'Tab 3',
content: 'Content of Tab 3', children: 'Content of Tab 3',
key: '3', key: '3',
closable: false, closable: false,
}, },
]; ];
const App: React.FC = () => { const App: React.FC = () => {
const [activeKey, setActiveKey] = useState(initialPanes[0].key); const [activeKey, setActiveKey] = useState(initialItems[0].key);
const [panes, setPanes] = useState(initialPanes); const [items, setItems] = useState(initialItems);
const newTabIndex = useRef(0); const newTabIndex = useRef(0);
const onChange = (newActiveKey: string) => { const onChange = (newActiveKey: string) => {
@ -41,21 +39,21 @@ const App: React.FC = () => {
const add = () => { const add = () => {
const newActiveKey = `newTab${newTabIndex.current++}`; const newActiveKey = `newTab${newTabIndex.current++}`;
const newPanes = [...panes]; const newPanes = [...items];
newPanes.push({ title: 'New Tab', content: 'Content of new Tab', key: newActiveKey }); newPanes.push({ label: 'New Tab', children: 'Content of new Tab', key: newActiveKey });
setPanes(newPanes); setItems(newPanes);
setActiveKey(newActiveKey); setActiveKey(newActiveKey);
}; };
const remove = (targetKey: string) => { const remove = (targetKey: string) => {
let newActiveKey = activeKey; let newActiveKey = activeKey;
let lastIndex = -1; let lastIndex = -1;
panes.forEach((pane, i) => { items.forEach((item, i) => {
if (pane.key === targetKey) { if (item.key === targetKey) {
lastIndex = i - 1; lastIndex = i - 1;
} }
}); });
const newPanes = panes.filter(pane => pane.key !== targetKey); const newPanes = items.filter(item => item.key !== targetKey);
if (newPanes.length && newActiveKey === targetKey) { if (newPanes.length && newActiveKey === targetKey) {
if (lastIndex >= 0) { if (lastIndex >= 0) {
newActiveKey = newPanes[lastIndex].key; newActiveKey = newPanes[lastIndex].key;
@ -63,7 +61,7 @@ const App: React.FC = () => {
newActiveKey = newPanes[0].key; newActiveKey = newPanes[0].key;
} }
} }
setPanes(newPanes); setItems(newPanes);
setActiveKey(newActiveKey); setActiveKey(newActiveKey);
}; };
@ -76,13 +74,13 @@ const App: React.FC = () => {
}; };
return ( return (
<Tabs type="editable-card" onChange={onChange} activeKey={activeKey} onEdit={onEdit}> <Tabs
{panes.map(pane => ( type="editable-card"
<TabPane tab={pane.title} key={pane.key} closable={pane.closable}> onChange={onChange}
{pane.content} activeKey={activeKey}
</TabPane> onEdit={onEdit}
))} items={items}
</Tabs> />
); );
}; };

View File

@ -17,8 +17,6 @@ You can add extra actions to the right or left or even both side of Tabs.
import { Button, Checkbox, Divider, Tabs } from 'antd'; import { Button, Checkbox, Divider, Tabs } from 'antd';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
const { TabPane } = Tabs;
const CheckboxGroup = Checkbox.Group; const CheckboxGroup = Checkbox.Group;
const operations = <Button>Extra Action</Button>; const operations = <Button>Extra Action</Button>;
@ -32,6 +30,15 @@ const options = ['left', 'right'];
type PositionType = 'left' | 'right'; type PositionType = 'left' | 'right';
const items = new Array(3).fill(null).map((_, i) => {
const id = String(i + 1);
return {
label: `Tab ${id}`,
key: id,
children: `Content of tab ${id}`,
};
});
const App: React.FC = () => { const App: React.FC = () => {
const [position, setPosition] = useState<PositionType[]>(['left', 'right']); const [position, setPosition] = useState<PositionType[]>(['left', 'right']);
@ -46,17 +53,7 @@ const App: React.FC = () => {
return ( return (
<> <>
<Tabs tabBarExtraContent={operations}> <Tabs tabBarExtraContent={operations} items={items} />
<TabPane tab="Tab 1" key="1">
Content of tab 1
</TabPane>
<TabPane tab="Tab 2" key="2">
Content of tab 2
</TabPane>
<TabPane tab="Tab 3" key="3">
Content of tab 3
</TabPane>
</Tabs>
<br /> <br />
<br /> <br />
<br /> <br />
@ -71,17 +68,7 @@ const App: React.FC = () => {
/> />
<br /> <br />
<br /> <br />
<Tabs tabBarExtraContent={slot}> <Tabs tabBarExtraContent={slot} items={items} />
<TabPane tab="Tab 1" key="1">
Content of tab 1
</TabPane>
<TabPane tab="Tab 2" key="2">
Content of tab 2
</TabPane>
<TabPane tab="Tab 3" key="3">
Content of tab 3
</TabPane>
</Tabs>
</> </>
); );
}; };

View File

@ -18,33 +18,24 @@ import { AndroidOutlined, AppleOutlined } from '@ant-design/icons';
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import React from 'react'; import React from 'react';
const { TabPane } = Tabs;
const App: React.FC = () => ( const App: React.FC = () => (
<Tabs defaultActiveKey="2"> <Tabs
<TabPane defaultActiveKey="2"
tab={ items={[AppleOutlined, AndroidOutlined].map((Icon, i) => {
<span> const id = String(i + 1);
<AppleOutlined />
Tab 1 return {
</span> label: (
} <span>
key="1" <Icon />
> Tab {id}
Tab 1 </span>
</TabPane> ),
<TabPane key: id,
tab={ children: `Tab ${id}`,
<span> };
<AndroidOutlined /> })}
Tab 2 />
</span>
}
key="2"
>
Tab 2
</TabPane>
</Tabs>
); );
export default App; export default App;

View File

@ -18,13 +18,10 @@ Default activate first tab.
import { Select, Tabs } from 'antd'; import { Select, Tabs } from 'antd';
import React, { useState } from 'react'; import React, { useState } from 'react';
const { TabPane } = Tabs;
const { Option } = Select; const { Option } = Select;
const positionList = ['left', 'right', 'top', 'bottom']; const positionList = ['left', 'right', 'top', 'bottom'];
const list = Array.from({ length: 20 }).map((_, index) => index);
const App: React.FC = () => { const App: React.FC = () => {
const [parentPos, setParentPos] = useState(undefined); const [parentPos, setParentPos] = useState(undefined);
const [childPos, setChildPos] = useState(undefined); const [childPos, setChildPos] = useState(undefined);
@ -81,25 +78,39 @@ const App: React.FC = () => {
<Option value="editable-card">Parent - card edit</Option> <Option value="editable-card">Parent - card edit</Option>
</Select> </Select>
<Tabs defaultActiveKey="1" tabPosition={parentPos} type={parentType}> <Tabs
<TabPane tab="Tab 1" key="1"> defaultActiveKey="1"
<Tabs tabPosition={parentPos}
defaultActiveKey="1" type={parentType}
tabPosition={childPos} items={[
type={childType} {
style={{ height: 300 }} label: 'Tab 1',
> key: '1',
{list.map(key => ( children: (
<TabPane tab={`Tab ${key}`} key={key}> <Tabs
TTTT {key} defaultActiveKey="1"
</TabPane> tabPosition={childPos}
))} type={childType}
</Tabs> style={{ height: 300 }}
</TabPane> items={new Array(20).fill(null).map((_, index) => {
<TabPane tab="Tab 2" key="2"> const key = String(index);
Content of Tab Pane 2
</TabPane> return {
</Tabs> label: `Tab ${key}`,
key,
children: `TTTT ${key}`,
};
})}
/>
),
},
{
label: 'Tab 2',
key: '2',
children: 'Content of Tab Pane 2',
},
]}
/>
</div> </div>
); );
}; };

View File

@ -18,8 +18,6 @@ import type { RadioChangeEvent } from 'antd';
import { Radio, Space, Tabs } from 'antd'; import { Radio, Space, Tabs } from 'antd';
import React, { useState } from 'react'; import React, { useState } from 'react';
const { TabPane } = Tabs;
type TabPosition = 'left' | 'right' | 'top' | 'bottom'; type TabPosition = 'left' | 'right' | 'top' | 'bottom';
const App: React.FC = () => { const App: React.FC = () => {
@ -40,17 +38,17 @@ const App: React.FC = () => {
<Radio.Button value="right">right</Radio.Button> <Radio.Button value="right">right</Radio.Button>
</Radio.Group> </Radio.Group>
</Space> </Space>
<Tabs tabPosition={tabPosition}> <Tabs
<TabPane tab="Tab 1" key="1"> tabPosition={tabPosition}
Content of Tab 1 items={new Array(3).fill(null).map((_, i) => {
</TabPane> const id = String(i + 1);
<TabPane tab="Tab 2" key="2"> return {
Content of Tab 2 label: `Tab ${id}`,
</TabPane> key: id,
<TabPane tab="Tab 3" key="3"> children: `Content of Tab ${id}`,
Content of Tab 3 };
</TabPane> })}
</Tabs> />
</> </>
); );
}; };

View File

@ -19,8 +19,6 @@ import { Radio, Tabs } from 'antd';
import type { SizeType } from 'antd/es/config-provider/SizeContext'; import type { SizeType } from 'antd/es/config-provider/SizeContext';
import React, { useState } from 'react'; import React, { useState } from 'react';
const { TabPane } = Tabs;
const App: React.FC = () => { const App: React.FC = () => {
const [size, setSize] = useState<SizeType>('small'); const [size, setSize] = useState<SizeType>('small');
@ -35,28 +33,32 @@ const App: React.FC = () => {
<Radio.Button value="middle">Middle</Radio.Button> <Radio.Button value="middle">Middle</Radio.Button>
<Radio.Button value="large">Large</Radio.Button> <Radio.Button value="large">Large</Radio.Button>
</Radio.Group> </Radio.Group>
<Tabs defaultActiveKey="1" size={size} style={{ marginBottom: 32 }}> <Tabs
<TabPane tab="Tab 1" key="1"> defaultActiveKey="1"
Content of tab 1 size={size}
</TabPane> style={{ marginBottom: 32 }}
<TabPane tab="Tab 2" key="2"> items={new Array(3).fill(null).map((_, i) => {
Content of tab 2 const id = String(i + 1);
</TabPane> return {
<TabPane tab="Tab 3" key="3"> label: `Tab ${id}`,
Content of tab 3 key: id,
</TabPane> children: `Content of tab ${id}`,
</Tabs> };
<Tabs defaultActiveKey="1" type="card" size={size}> })}
<TabPane tab="Card Tab 1" key="1"> />
Content of card tab 1 <Tabs
</TabPane> defaultActiveKey="1"
<TabPane tab="Card Tab 2" key="2"> type="card"
Content of card tab 2 size={size}
</TabPane> items={new Array(3).fill(null).map((_, i) => {
<TabPane tab="Card Tab 3" key="3"> const id = String(i + 1);
Content of card tab 3 return {
</TabPane> label: `Card Tab ${id}`,
</Tabs> key: id,
children: `Content of card tab ${id}`,
};
})}
/>
</div> </div>
); );
}; };

View File

@ -18,8 +18,6 @@ import type { RadioChangeEvent } from 'antd';
import { Radio, Tabs } from 'antd'; import { Radio, Tabs } from 'antd';
import React, { useState } from 'react'; import React, { useState } from 'react';
const { TabPane } = Tabs;
type TabPosition = 'left' | 'right' | 'top' | 'bottom'; type TabPosition = 'left' | 'right' | 'top' | 'bottom';
const App: React.FC = () => { const App: React.FC = () => {
@ -35,13 +33,20 @@ const App: React.FC = () => {
<Radio.Button value="top">Horizontal</Radio.Button> <Radio.Button value="top">Horizontal</Radio.Button>
<Radio.Button value="left">Vertical</Radio.Button> <Radio.Button value="left">Vertical</Radio.Button>
</Radio.Group> </Radio.Group>
<Tabs defaultActiveKey="1" tabPosition={mode} style={{ height: 220 }}> <Tabs
{[...Array.from({ length: 30 }, (_, i) => i)].map(i => ( defaultActiveKey="1"
<TabPane tab={`Tab-${i}`} key={i} disabled={i === 28}> tabPosition={mode}
Content of tab {i} style={{ height: 220 }}
</TabPane> items={new Array(30).fill(null).map((_, i) => {
))} const id = String(i);
</Tabs> return {
label: `Tab-${id}`,
key: id,
disabled: i === 28,
children: `Content of tab ${id}`,
};
})}
/>
</div> </div>
); );
}; };

View File

@ -0,0 +1,35 @@
import * as React from 'react';
import toArray from 'rc-util/lib/Children/toArray';
import type { Tab } from 'rc-tabs/lib/interface';
import type { TabsProps, TabPaneProps } from '..';
import warning from '../../_util/warning';
function filter<T>(items: (T | null)[]): T[] {
return items.filter(item => item) as T[];
}
export default function useLegacyItems(items?: TabsProps['items'], children?: React.ReactNode) {
if (items) {
return items;
}
warning(!children, 'Tabs', 'Tabs.TabPane is deprecated. Please use `items` directly.');
const childrenItems = toArray(children).map((node: React.ReactElement<TabPaneProps>) => {
if (React.isValidElement(node)) {
const { key, props } = node;
const { tab, ...restProps } = props || {};
const item: Tab = {
key: String(key),
...restProps,
label: tab,
};
return item;
}
return null;
});
return filter(childrenItems);
}

View File

@ -16,6 +16,32 @@ Ant Design has 3 types of Tabs for different situations.
- Normal Tabs: for functional aspects of a page. - Normal Tabs: for functional aspects of a page.
- [Radio.Button](/components/radio/#components-radio-demo-radiobutton): for secondary tabs. - [Radio.Button](/components/radio/#components-radio-demo-radiobutton): for secondary tabs.
### Usage upgrade after 4.23.0
```__react
import Alert from '../alert';
ReactDOM.render(<Alert message="After version 4.23.0, we provide a simpler usage <Tabs items={[...]} /> with better performance and potential of writing simpler code style in your applications. Meanwhile, we deprecated the old usage in browser console, we will remove it in antd 5.0." />, mountNode);
```
```jsx
// works when >=4.23.0, recommended ✅
const items = [
{ label: 'Tab 1', key: 'item-1', children: 'Content 1' }, // remember to pass the key prop
{ label: 'Tab 2', key: 'item-2', children: 'Content 2' },
];
return <Tabs items={items} />;
// works when <4.23.0, deprecated when >=4.23.0 🙅🏻‍♀️
<Tabs>
<Tabs.TabPane tab="Tab 1" key="item-1">
Content 1
</Tabs.TabPane>
<Tabs.TabPane tab="Tab 2" key="item-2">
Content 2
</Tabs.TabPane>
</Tabs>;
```
## API ## API
### Tabs ### Tabs

View File

@ -3,7 +3,7 @@ import EllipsisOutlined from '@ant-design/icons/EllipsisOutlined';
import PlusOutlined from '@ant-design/icons/PlusOutlined'; import PlusOutlined from '@ant-design/icons/PlusOutlined';
import classNames from 'classnames'; import classNames from 'classnames';
import type { TabsProps as RcTabsProps } from 'rc-tabs'; import type { TabsProps as RcTabsProps } from 'rc-tabs';
import RcTabs, { TabPane, TabPaneProps } from 'rc-tabs'; import RcTabs from 'rc-tabs';
import type { EditableConfig } from 'rc-tabs/lib/interface'; import type { EditableConfig } from 'rc-tabs/lib/interface';
import * as React from 'react'; import * as React from 'react';
@ -11,6 +11,8 @@ import { ConfigContext } from '../config-provider';
import type { SizeType } from '../config-provider/SizeContext'; import type { SizeType } from '../config-provider/SizeContext';
import SizeContext from '../config-provider/SizeContext'; import SizeContext from '../config-provider/SizeContext';
import warning from '../_util/warning'; import warning from '../_util/warning';
import useLegacyItems from './hooks/useLegacyItems';
import TabPane, { TabPaneProps } from './TabPane';
import useStyle from './style'; import useStyle from './style';
@ -26,6 +28,7 @@ export interface TabsProps extends Omit<RcTabsProps, 'editable'> {
centered?: boolean; centered?: boolean;
addIcon?: React.ReactNode; addIcon?: React.ReactNode;
onEdit?: (e: React.MouseEvent | React.KeyboardEvent | string, action: 'add' | 'remove') => void; onEdit?: (e: React.MouseEvent | React.KeyboardEvent | string, action: 'add' | 'remove') => void;
children?: React.ReactNode;
} }
function Tabs({ function Tabs({
@ -37,6 +40,8 @@ function Tabs({
centered, centered,
addIcon, addIcon,
popupClassName, popupClassName,
children,
items,
...props ...props
}: TabsProps) { }: TabsProps) {
const { prefixCls: customizePrefixCls, moreIcon = <EllipsisOutlined /> } = props; const { prefixCls: customizePrefixCls, moreIcon = <EllipsisOutlined /> } = props;
@ -63,6 +68,8 @@ function Tabs({
'`onPrevClick` and `onNextClick` has been removed. Please use `onTabScroll` instead.', '`onPrevClick` and `onNextClick` has been removed. Please use `onTabScroll` instead.',
); );
const mergedItems = useLegacyItems(items, children);
return wrapSSR( return wrapSSR(
<SizeContext.Consumer> <SizeContext.Consumer>
{contextSize => { {contextSize => {
@ -72,6 +79,7 @@ function Tabs({
direction={direction} direction={direction}
moreTransitionName={`${rootPrefixCls}-slide-up`} moreTransitionName={`${rootPrefixCls}-slide-up`}
{...props} {...props}
items={mergedItems}
className={classNames( className={classNames(
{ {
[`${prefixCls}-${size}`]: size, [`${prefixCls}-${size}`]: size,

View File

@ -19,6 +19,32 @@ Ant Design 依次提供了三级选项卡,分别用于不同的场景。
- 既可用于容器顶部,也可用于容器内部,是最通用的 Tabs。 - 既可用于容器顶部,也可用于容器内部,是最通用的 Tabs。
- [Radio.Button](/components/radio/#components-radio-demo-radiobutton) 可作为更次级的页签来使用。 - [Radio.Button](/components/radio/#components-radio-demo-radiobutton) 可作为更次级的页签来使用。
### 4.23.0 用法升级
```__react
import Alert from '../alert';
ReactDOM.render(<Alert message="在 4.23.0 版本后,我们提供了 <Tabs items={[...]} /> 的简写方式,有更好的性能和更方便的数据组织方式,开发者不再需要自行拼接 JSX。同时我们废弃了原先的写法你还是可以在 4.x 继续使用,但会在控制台看到警告,并会在 5.0 后移除。" />, mountNode);
```
```jsx
// >=4.23.0 可用,推荐的写法 ✅
const items = [
{ label: '项目 1', key: 'item-1', children: '内容 1' }, // 务必填写 key
{ label: '项目 2', key: 'item-2', children: '内容 2' },
];
return <Tabs items={items} />;
// <4.23.0 可用>=4.23.0 时不推荐 🙅🏻‍♀️
<Tabs>
<Tabs.TabPane tab="项目 1" key="item-1">
内容 1
</Tabs.TabPane>
<Tabs.TabPane tab="项目 2" key="item-2">
内容 2
</Tabs.TabPane>
</Tabs>;
```
## API ## API
### Tabs ### Tabs
@ -31,6 +57,7 @@ Ant Design 依次提供了三级选项卡,分别用于不同的场景。
| centered | 标签居中展示 | boolean | false | 4.4.0 | | centered | 标签居中展示 | boolean | false | 4.4.0 |
| defaultActiveKey | 初始化选中面板的 key如果没有设置 activeKey | string | `第一个面板` | | | defaultActiveKey | 初始化选中面板的 key如果没有设置 activeKey | string | `第一个面板` | |
| hideAdd | 是否隐藏加号图标,在 `type="editable-card"` 时有效 | boolean | false | | | hideAdd | 是否隐藏加号图标,在 `type="editable-card"` 时有效 | boolean | false | |
| items | 配置选项卡内容 | [TabItem](#TabItem) | [] | 4.23.0 |
| moreIcon | 自定义折叠 icon | ReactNode | &lt;EllipsisOutlined /> | 4.14.0 | | moreIcon | 自定义折叠 icon | ReactNode | &lt;EllipsisOutlined /> | 4.14.0 |
| popupClassName | 更多菜单的 `className` | string | - | 4.21.0 | | popupClassName | 更多菜单的 `className` | string | - | 4.21.0 |
| renderTabBar | 替换 TabBar用于二次封装标签头 | (props: DefaultTabBarProps, DefaultTabBar: React.ComponentClass) => React.ReactElement | - | | | renderTabBar | 替换 TabBar用于二次封装标签头 | (props: DefaultTabBarProps, DefaultTabBar: React.ComponentClass) => React.ReactElement | - | |
@ -48,7 +75,7 @@ Ant Design 依次提供了三级选项卡,分别用于不同的场景。
> 更多属性查看 [rc-tabs tabs](https://github.com/react-component/tabs#tabs) > 更多属性查看 [rc-tabs tabs](https://github.com/react-component/tabs#tabs)
### Tabs.TabPane ### TabItem
| 参数 | 说明 | 类型 | 默认值 | | 参数 | 说明 | 类型 | 默认值 |
| ----------- | ----------------------------------------------- | --------- | ------ | | ----------- | ----------------------------------------------- | --------- | ------ |
@ -56,6 +83,5 @@ Ant Design 依次提供了三级选项卡,分别用于不同的场景。
| disabled | 禁用某一项 | boolean | false | | disabled | 禁用某一项 | boolean | false |
| forceRender | 被隐藏时是否渲染 DOM 结构 | boolean | false | | forceRender | 被隐藏时是否渲染 DOM 结构 | boolean | false |
| key | 对应 activeKey | string | - | | key | 对应 activeKey | string | - |
| tab | 选项卡头显示文字 | ReactNode | - | | label | 选项卡头显示文字 | ReactNode | - |
| children | 选项卡头显示内容 | ReactNode | - |
> 更多属性查看 [rc-tabs tabpane](https://github.com/react-component/tabs#tabpane)

View File

@ -87,6 +87,20 @@ describe('TimePicker', () => {
expect(wrapper.find('RangePicker').last().prop('dropdownClassName')).toEqual(popupClassName); expect(wrapper.find('RangePicker').last().prop('dropdownClassName')).toEqual(popupClassName);
}); });
it('RangePicker should show warning when use dropdownClassName', () => {
mount(<TimePicker.RangePicker dropdownClassName="myCustomClassName" />);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: RangePicker] `dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
);
});
it('TimePicker should show warning when use dropdownClassName', () => {
mount(<TimePicker dropdownClassName="myCustomClassName" />);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: TimePicker] `dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
);
});
it('should support bordered', () => { it('should support bordered', () => {
const wrapper = mount( const wrapper = mount(
<TimePicker <TimePicker

View File

@ -14,31 +14,50 @@ export interface TimePickerLocale {
} }
export interface TimeRangePickerProps extends Omit<RangePickerTimeProps<Dayjs>, 'picker'> { export interface TimeRangePickerProps extends Omit<RangePickerTimeProps<Dayjs>, 'picker'> {
/**
* @deprecated `dropdownClassName` is deprecated which will be removed in next major
* version.Please use `popupClassName` instead.
*/
dropdownClassName?: string;
popupClassName?: string; popupClassName?: string;
} }
const RangePicker = React.forwardRef<any, TimeRangePickerProps>((props, ref) => ( const RangePicker = React.forwardRef<any, TimeRangePickerProps>((props, ref) => {
<InternalRangePicker const { dropdownClassName, popupClassName } = props;
{...props} warning(
dropdownClassName={props.popupClassName} !dropdownClassName,
picker="time" 'RangePicker',
mode={undefined} '`dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
ref={ref} );
/> return (
)); <InternalRangePicker
{...props}
dropdownClassName={popupClassName || dropdownClassName}
picker="time"
mode={undefined}
ref={ref}
/>
);
});
export interface TimePickerProps extends Omit<PickerTimeProps<Dayjs>, 'picker'> { export interface TimePickerProps extends Omit<PickerTimeProps<Dayjs>, 'picker'> {
addon?: () => React.ReactNode; addon?: () => React.ReactNode;
popupClassName?: string; popupClassName?: string;
/**
* @deprecated `dropdownClassName` is deprecated which will be removed in next major
* version.Please use `popupClassName` instead.
*/
dropdownClassName?: string;
status?: InputStatus; status?: InputStatus;
} }
const TimePicker = React.forwardRef<any, TimePickerProps>( const TimePicker = React.forwardRef<any, TimePickerProps>(
({ addon, renderExtraFooter, popupClassName, ...restProps }, ref) => { ({ addon, renderExtraFooter, popupClassName, dropdownClassName, ...restProps }, ref) => {
const internalRenderExtraFooter = React.useMemo(() => { const internalRenderExtraFooter = React.useMemo(() => {
if (renderExtraFooter) { if (renderExtraFooter) {
return renderExtraFooter; return renderExtraFooter;
} }
if (addon) { if (addon) {
warning( warning(
false, false,
@ -50,10 +69,16 @@ const TimePicker = React.forwardRef<any, TimePickerProps>(
return undefined; return undefined;
}, [addon, renderExtraFooter]); }, [addon, renderExtraFooter]);
warning(
!dropdownClassName,
'TimePicker',
'`dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
);
return ( return (
<InternalTimePicker <InternalTimePicker
dropdownClassName={popupClassName || dropdownClassName}
{...restProps} {...restProps}
dropdownClassName={popupClassName}
mode={undefined} mode={undefined}
ref={ref} ref={ref}
renderExtraFooter={internalRenderExtraFooter} renderExtraFooter={internalRenderExtraFooter}

View File

@ -2,7 +2,7 @@ import type { CSSObject } from '@ant-design/cssinjs';
import type { FullToken, GenerateStyle } from '../../theme'; import type { FullToken, GenerateStyle } from '../../theme';
import { genComponentStyleHook, mergeToken } from '../../theme'; import { genComponentStyleHook, mergeToken } from '../../theme';
import { operationUnit, resetComponent, resetIcon } from '../../style'; import { resetComponent, resetIcon } from '../../style';
export interface ComponentToken { export interface ComponentToken {
listWidth: number; listWidth: number;
@ -105,6 +105,7 @@ const genTransferListStyle: GenerateStyle<TransferToken> = (token: TransferToken
paddingSM, paddingSM,
controlLineType, controlLineType,
iconCls, iconCls,
motionDurationSlow,
} = token; } = token;
return { return {
@ -195,7 +196,7 @@ const genTransferListStyle: GenerateStyle<TransferToken> = (token: TransferToken
alignItems: 'center', alignItems: 'center',
minHeight: transferItemHeight, minHeight: transferItemHeight,
padding: `${transferItemPaddingVertical}px ${paddingSM}px`, padding: `${transferItemPaddingVertical}px ${paddingSM}px`,
transition: `all ${token.motionDurationSlow}`, transition: `all ${motionDurationSlow}`,
'> *:not(:last-child)': { '> *:not(:last-child)': {
marginInlineEnd: marginXS, marginInlineEnd: marginXS,
@ -213,19 +214,21 @@ const genTransferListStyle: GenerateStyle<TransferToken> = (token: TransferToken
}, },
'&-remove': { '&-remove': {
...operationUnit(token),
position: 'relative', position: 'relative',
color: colorBorder, color: colorBorder,
cursor: 'pointer',
transition: `all ${motionDurationSlow}`,
'&:hover': {
color: token.colorLinkHover,
},
'&::after': { '&::after': {
position: 'absolute', position: 'absolute',
insert: `-${transferItemPaddingVertical}px -50%`, insert: `-${transferItemPaddingVertical}px -50%`,
content: '""', content: '""',
}, },
'&:hover': {
color: token.colorLinkHover,
},
}, },
'&:not(&-disabled)': { '&:not(&-disabled)': {

View File

@ -50,4 +50,13 @@ describe('TreeSelect', () => {
const wrapper = mount(<TreeSelect treeIcon open notFoundContent="notFoundContent" />); const wrapper = mount(<TreeSelect treeIcon open notFoundContent="notFoundContent" />);
expect(wrapper.text()).toBe('notFoundContent'); expect(wrapper.text()).toBe('notFoundContent');
}); });
it('should show warning when use dropdownClassName', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
mount(<TreeSelect dropdownClassName="myCustomClassName" />);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: TreeSelect] `dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
);
errorSpy.mockRestore();
});
}); });

View File

@ -22,7 +22,7 @@ Tree selection control.
| bordered | Whether has border style | boolean | true | | | bordered | Whether has border style | boolean | true | |
| defaultValue | To set the initial selected treeNode(s) | string \| string\[] | - | | | defaultValue | To set the initial selected treeNode(s) | string \| string\[] | - | |
| disabled | Disabled or not | boolean | false | | | disabled | Disabled or not | boolean | false | |
| dropdownClassName | The className of dropdown menu | string | - | | | popupClassName | The className of dropdown menu | string | - | 4.23.0 |
| dropdownMatchSelectWidth | Determine whether the dropdown menu and the select input are the same width. Default set `min-width` same as input. Will ignore when value less than select width. `false` will disable virtual scroll | boolean \| number | true | | | dropdownMatchSelectWidth | Determine whether the dropdown menu and the select input are the same width. Default set `min-width` same as input. Will ignore when value less than select width. `false` will disable virtual scroll | boolean \| number | true | |
| dropdownRender | Customize dropdown content | (originNode: ReactNode, props) => ReactNode | - | | | dropdownRender | Customize dropdown content | (originNode: ReactNode, props) => ReactNode | - | |
| dropdownStyle | To set the style of the dropdown menu | CSSProperties | - | | | dropdownStyle | To set the style of the dropdown menu | CSSProperties | - | |

View File

@ -54,6 +54,12 @@ export interface TreeSelectProps<
size?: SizeType; size?: SizeType;
disabled?: boolean; disabled?: boolean;
placement?: SelectCommonPlacement; placement?: SelectCommonPlacement;
/**
* @deprecated `dropdownClassName` is deprecated which will be removed in next major
* version.Please use `popupClassName` instead.
*/
dropdownClassName?: string;
popupClassName?: string;
bordered?: boolean; bordered?: boolean;
treeLine?: TreeProps['showLine']; treeLine?: TreeProps['showLine'];
status?: InputStatus; status?: InputStatus;
@ -77,6 +83,7 @@ const InternalTreeSelect = <OptionType extends BaseOptionType | DefaultOptionTyp
treeLine, treeLine,
getPopupContainer, getPopupContainer,
dropdownClassName, dropdownClassName,
popupClassName,
treeIcon = false, treeIcon = false,
transitionName, transitionName,
choiceTransitionName = '', choiceTransitionName = '',
@ -104,6 +111,13 @@ const InternalTreeSelect = <OptionType extends BaseOptionType | DefaultOptionTyp
); );
const rootPrefixCls = getPrefixCls(); const rootPrefixCls = getPrefixCls();
warning(
!dropdownClassName,
'TreeSelect',
'`dropdownClassName` is deprecated which will be removed in next major version. Please use `popupClassName` instead.',
);
const prefixCls = getPrefixCls('select', customizePrefixCls); const prefixCls = getPrefixCls('select', customizePrefixCls);
const treePrefixCls = getPrefixCls('select-tree', customizePrefixCls); const treePrefixCls = getPrefixCls('select-tree', customizePrefixCls);
const treeSelectPrefixCls = getPrefixCls('tree-select', customizePrefixCls); const treeSelectPrefixCls = getPrefixCls('tree-select', customizePrefixCls);
@ -112,7 +126,7 @@ const InternalTreeSelect = <OptionType extends BaseOptionType | DefaultOptionTyp
const [wrapTreeSelectSSR] = useStyle(treeSelectPrefixCls, treePrefixCls); const [wrapTreeSelectSSR] = useStyle(treeSelectPrefixCls, treePrefixCls);
const mergedDropdownClassName = classNames( const mergedDropdownClassName = classNames(
dropdownClassName, popupClassName || dropdownClassName,
`${treeSelectPrefixCls}-dropdown`, `${treeSelectPrefixCls}-dropdown`,
{ {
[`${treeSelectPrefixCls}-dropdown-rtl`]: direction === 'rtl', [`${treeSelectPrefixCls}-dropdown-rtl`]: direction === 'rtl',

View File

@ -23,7 +23,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Ax4DA0njr/TreeSelect.svg
| bordered | 是否显示边框 | boolean | true | | | bordered | 是否显示边框 | boolean | true | |
| defaultValue | 指定默认选中的条目 | string \| string\[] | - | | | defaultValue | 指定默认选中的条目 | string \| string\[] | - | |
| disabled | 是否禁用 | boolean | false | | | disabled | 是否禁用 | boolean | false | |
| dropdownClassName | 下拉菜单的 className 属性 | string | - | | | popupClassName | 下拉菜单的 className 属性 | string | - | 4.23.0 |
| dropdownMatchSelectWidth | 下拉菜单和选择器同宽。默认将设置 `min-width`当值小于选择框宽度时会被忽略。false 时会关闭虚拟滚动 | boolean \| number | true | | | dropdownMatchSelectWidth | 下拉菜单和选择器同宽。默认将设置 `min-width`当值小于选择框宽度时会被忽略。false 时会关闭虚拟滚动 | boolean \| number | true | |
| dropdownRender | 自定义下拉框内容 | (originNode: ReactNode, props) => ReactNode | - | | | dropdownRender | 自定义下拉框内容 | (originNode: ReactNode, props) => ReactNode | - | |
| dropdownStyle | 下拉菜单的样式 | object | - | | | dropdownStyle | 下拉菜单的样式 | object | - | |

View File

@ -99,6 +99,19 @@ Yes, you can [import `antd` with script tag](https://ant.design/docs/react/intro
If you need some features which should not be included in antd, try to extend antd's component with [HOC](https://gist.github.com/sebmarkbage/ef0bf1f338a7182b6775). [more](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750#.eeu8q01s1) If you need some features which should not be included in antd, try to extend antd's component with [HOC](https://gist.github.com/sebmarkbage/ef0bf1f338a7182b6775). [more](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750#.eeu8q01s1)
## How to get the definition which is not export?
antd 会透出组件定义,但是随着重构可能导致内部一些定义命名或者属性变化。因而更推荐直接使用 Typescript 原生能力获取: antd will export mainly definitions, but not export internal definitions which may be rename or changed. So we recommend you to use Typescript's native ability to get the definition if needed:
```tsx
import { Table } from 'antd';
type Props<T extends (...args: any) => any> = Parameters<T>[0];
type TableProps = Props<typeof Table<{ key: string, name: string, age: number }>>;
type DataSource = TableProps['dataSource'];
```
## Date-related components locale is not working? ## Date-related components locale is not working?
Please check whether import dayjs locale correctly. Please check whether import dayjs locale correctly.

View File

@ -113,6 +113,19 @@ antd 内部会对 props 进行浅比较实现性能优化。当状态变更,
如果你需要一些 antd 没有包含的功能,你可以尝试通过 [HOC](https://gist.github.com/sebmarkbage/ef0bf1f338a7182b6775) 拓展 antd 的组件。 [更多](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750#.eeu8q01s1) 如果你需要一些 antd 没有包含的功能,你可以尝试通过 [HOC](https://gist.github.com/sebmarkbage/ef0bf1f338a7182b6775) 拓展 antd 的组件。 [更多](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750#.eeu8q01s1)
## 如何获取未导出的属性定义?
antd 会透出组件定义,但是随着重构可能导致内部一些定义命名或者属性变化。因而更推荐直接使用 Typescript 原生能力获取:
```tsx
import { Table } from 'antd';
type Props<T extends (...args: any) => any> = Parameters<T>[0];
type TableProps = Props<typeof Table<{ key: string, name: string, age: number }>>;
type DataSource = TableProps['dataSource'];
```
## 我的组件默认语言是英文的?如何切回中文的。 ## 我的组件默认语言是英文的?如何切回中文的。
请尝试使用 [ConfigProvider](/components/config-provider/#components-config-provider-demo-locale) 组件来包裹你的应用。 请尝试使用 [ConfigProvider](/components/config-provider/#components-config-provider-demo-locale) 组件来包裹你的应用。

View File

@ -192,6 +192,14 @@ For parts that cannot be modified automatically, codemod will prompt on the comm
`@ant-design/codemod-v4` will help you migrate to antd v4. Obsolete components will be kept running through @ant-design/compatible. Generally, you don't need to migrate manually. The following sections detail the overall migration and changes. `@ant-design/codemod-v4` will help you migrate to antd v4. Obsolete components will be kept running through @ant-design/compatible. Generally, you don't need to migrate manually. The following sections detail the overall migration and changes.
#### Install compatible package
Install `@ant-design/compatible` with `v4-compatible-v3` tag:
```bash
npm install --save @ant-design/compatible@v4-compatible-v3
```
#### Import the obsolete Form and Mention components via @ant-design/compatible package #### Import the obsolete Form and Mention components via @ant-design/compatible package
```diff ```diff

View File

@ -193,6 +193,14 @@ antd4-codemod src
`@ant-design/codemod-v4` 会帮你迁移到 antd v4, 废弃的组件则通过 `@ant-design/compatible` 保持运行, 一般来说你无需手动迁移。下方内容详细介绍了整体的迁移和变化,你也可以参照变动手动修改。 `@ant-design/codemod-v4` 会帮你迁移到 antd v4, 废弃的组件则通过 `@ant-design/compatible` 保持运行, 一般来说你无需手动迁移。下方内容详细介绍了整体的迁移和变化,你也可以参照变动手动修改。
#### 安装兼容包
安装 `@ant-design/compatible` 通过指定 `v4-compatible-v3` tag 确认为 v4 兼容 v3 版本:
```bash
npm install --save @ant-design/compatible@v4-compatible-v3
```
#### 将已废弃的 `Form``Mention` 组件通过 `@ant-design/compatible` 包引入 #### 将已废弃的 `Form``Mention` 组件通过 `@ant-design/compatible` 包引入
```diff ```diff

View File

@ -128,9 +128,9 @@
"rc-dropdown": "~4.0.0", "rc-dropdown": "~4.0.0",
"rc-field-form": "~1.27.0", "rc-field-form": "~1.27.0",
"rc-image": "~5.7.0", "rc-image": "~5.7.0",
"rc-input": "~0.0.1-alpha.5", "rc-input": "~0.1.2",
"rc-input-number": "~7.3.5", "rc-input-number": "~7.3.5",
"rc-mentions": "~1.9.0", "rc-mentions": "~1.9.1",
"rc-menu": "~9.6.0", "rc-menu": "~9.6.0",
"rc-motion": "^2.6.1", "rc-motion": "^2.6.1",
"rc-notification": "~5.0.0-alpha.9", "rc-notification": "~5.0.0-alpha.9",
@ -144,8 +144,8 @@
"rc-slider": "~10.0.0", "rc-slider": "~10.0.0",
"rc-steps": "~4.1.0", "rc-steps": "~4.1.0",
"rc-switch": "~3.2.0", "rc-switch": "~3.2.0",
"rc-table": "~7.25.3", "rc-table": "~7.26.0",
"rc-tabs": "~11.16.0", "rc-tabs": "~12.0.0-alpha.1",
"rc-textarea": "~0.3.0", "rc-textarea": "~0.3.0",
"rc-tooltip": "~5.2.0", "rc-tooltip": "~5.2.0",
"rc-tree": "~5.6.5", "rc-tree": "~5.6.5",

View File

@ -1,11 +1,10 @@
.operation-unit() { .operation-unit() {
color: @link-color; color: @link-color;
text-decoration: none;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
transition: color 0.3s; transition: color 0.3s;
&:focus, &:focus-visible,
&:hover { &:hover {
color: @link-hover-color; color: @link-hover-color;
} }

View File

@ -497,7 +497,11 @@ ReactDOM.render(<Demo />, document.getElementById('container'));
); );
if (meta.version) { if (meta.version) {
codeBox = <Badge.Ribbon text={meta.version}>{codeBox}</Badge.Ribbon>; codeBox = (
<Badge.Ribbon text={meta.version} color={meta.version.includes('<') ? 'red' : null}>
{codeBox}
</Badge.Ribbon>
);
} }
return codeBox; return codeBox;

View File

@ -13,60 +13,46 @@ interface CategoryProps {
intl: any; intl: any;
} }
interface CategoryState { const Category: React.FC<CategoryProps> = props => {
justCopied: string | null; const { icons, title, newIcons, theme, intl } = props;
} const [justCopied, setJustCopied] = React.useState<string | null>(null);
const copyId = React.useRef<NodeJS.Timeout | null>(null);
class Category extends React.Component<CategoryProps, CategoryState> { const onCopied = React.useCallback((type: string, text: string) => {
copyId?: number;
state = {
justCopied: null,
};
componentWillUnmount() {
window.clearTimeout(this.copyId);
}
onCopied = (type: string, text: string) => {
message.success( message.success(
<span> <span>
<code className="copied-code">{text}</code> copied 🎉 <code className="copied-code">{text}</code> copied 🎉
</span>, </span>,
); );
this.setState({ justCopied: type }, () => { setJustCopied(type);
this.copyId = window.setTimeout(() => { copyId.current = setTimeout(() => {
this.setState({ justCopied: null }); setJustCopied(null);
}, 2000); }, 2000);
}); }, []);
}; React.useEffect(
() => () => {
render() { if (copyId.current) {
const { clearTimeout(copyId.current);
icons, }
title, },
newIcons, [],
theme, );
intl: { messages }, return (
} = this.props; <div>
const items = icons.map(name => ( <h3>{intl.messages[`app.docs.components.icon.category.${title}`]}</h3>
<CopyableIcon <ul className="anticons-list">
key={name} {icons.map(name => (
name={name} <CopyableIcon
theme={theme} key={name}
isNew={newIcons.indexOf(name) >= 0} name={name}
justCopied={this.state.justCopied} theme={theme}
onCopied={this.onCopied} isNew={newIcons.includes(name)}
/> justCopied={justCopied}
)); onCopied={onCopied}
/>
return ( ))}
<div> </ul>
<h3>{messages[`app.docs.components.icon.category.${title}`]}</h3> </div>
<ul className="anticons-list">{items}</ul> );
</div> };
);
}
}
export default injectIntl(Category); export default injectIntl(Category);

View File

@ -1,12 +1,10 @@
import React, { Component } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Upload, Tooltip, Popover, Modal, Progress, message, Spin, Result } from 'antd'; import { Upload, Tooltip, Popover, Modal, Progress, message, Spin, Result } from 'antd';
import CopyToClipboard from 'react-copy-to-clipboard'; import CopyToClipboard from 'react-copy-to-clipboard';
import { injectIntl } from 'react-intl'; import { injectIntl } from 'react-intl';
import * as AntdIcons from '@ant-design/icons'; import * as AntdIcons from '@ant-design/icons';
const allIcons: { const allIcons: { [key: string]: any } = AntdIcons;
[key: string]: any;
} = AntdIcons;
const { Dragger } = Upload; const { Dragger } = Upload;
interface AntdIconClassifier { interface AntdIconClassifier {
@ -27,8 +25,8 @@ interface PicSearcherState {
loading: boolean; loading: boolean;
modalVisible: boolean; modalVisible: boolean;
popoverVisible: boolean; popoverVisible: boolean;
icons: Array<string>; icons: iconObject[];
fileList: Array<any>; fileList: any[];
error: boolean; error: boolean;
modelLoaded: boolean; modelLoaded: boolean;
} }
@ -38,8 +36,9 @@ interface iconObject {
score: number; score: number;
} }
class PicSearcher extends Component<PicSearcherProps, PicSearcherState> { const PicSearcher: React.FC<PicSearcherProps> = ({ intl }) => {
state = { const { messages } = intl;
const [state, setState] = useState<PicSearcherState>({
loading: false, loading: false,
modalVisible: false, modalVisible: false,
popoverVisible: false, popoverVisible: false,
@ -47,83 +46,64 @@ class PicSearcher extends Component<PicSearcherProps, PicSearcherState> {
fileList: [], fileList: [],
error: false, error: false,
modelLoaded: false, modelLoaded: false,
}; });
const predict = (imgEl: HTMLImageElement) => {
componentDidMount() {
this.loadModel();
this.setState({ popoverVisible: !localStorage.getItem('disableIconTip') });
}
componentWillUnmount() {
document.removeEventListener('paste', this.onPaste);
}
loadModel = () => {
const script = document.createElement('script');
script.onload = async () => {
await window.antdIconClassifier.load();
this.setState({ modelLoaded: true });
document.addEventListener('paste', this.onPaste);
};
script.src = 'https://cdn.jsdelivr.net/gh/lewis617/antd-icon-classifier@0.0/dist/main.js';
document.head.appendChild(script);
};
onPaste = (event: ClipboardEvent) => {
const items = event.clipboardData && event.clipboardData.items;
let file = null;
if (items && items.length) {
for (let i = 0; i < items.length; i += 1) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile();
break;
}
}
}
if (file) this.uploadFile(file);
};
uploadFile = (file: File) => {
this.setState(() => ({ loading: true }));
const reader: FileReader = new FileReader();
reader.onload = () => {
this.toImage(reader.result).then(this.predict);
this.setState(() => ({
fileList: [{ uid: 1, name: file.name, status: 'done', url: reader.result }],
}));
};
reader.readAsDataURL(file);
};
// eslint-disable-next-line class-methods-use-this
toImage = (url: any) =>
new Promise(resolve => {
const img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
img.src = url;
img.onload = function onload() {
resolve(img);
};
});
predict = (imgEl: any) => {
try { try {
let icons = window.antdIconClassifier.predict(imgEl); let icons: any[] = window.antdIconClassifier.predict(imgEl);
if (gtag && icons.length >= 1) { if (gtag && icons.length) {
gtag('event', 'icon', { gtag('event', 'icon', {
event_category: 'search-by-image', event_category: 'search-by-image',
event_label: icons[0].className, event_label: icons[0].className,
}); });
} }
icons = icons.map((i: any) => ({ score: i.score, type: i.className.replace(/\s/g, '-') })); icons = icons.map(i => ({ score: i.score, type: i.className.replace(/\s/g, '-') }));
this.setState(() => ({ icons, loading: false, error: false })); setState(prev => ({ ...prev, loading: false, error: false, icons }));
} catch (err) { } catch {
this.setState(() => ({ loading: false, error: true })); setState(prev => ({ ...prev, loading: false, error: true }));
} }
}; };
// eslint-disable-next-line class-methods-use-this
const toImage = (url: string) =>
new Promise(resolve => {
const img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
img.src = url;
img.onload = () => {
resolve(img);
};
});
toggleModal = () => { const uploadFile = useCallback((file: File) => {
this.setState(prev => ({ setState(prev => ({ ...prev, loading: true }));
const reader = new FileReader();
reader.onload = () => {
toImage(reader.result as string).then(predict);
setState(prev => ({
...prev,
fileList: [{ uid: 1, name: file.name, status: 'done', url: reader.result }],
}));
};
reader.readAsDataURL(file);
}, []);
const onPaste = useCallback((event: ClipboardEvent) => {
const items = event.clipboardData && event.clipboardData.items;
let file = null;
if (items && items.length) {
for (let i = 0; i < items.length; i++) {
if (items[i].type.includes('image')) {
file = items[i].getAsFile();
break;
}
}
}
if (file) {
uploadFile(file);
}
}, []);
const toggleModal = useCallback(() => {
setState(prev => ({
...prev,
modalVisible: !prev.modalVisible, modalVisible: !prev.modalVisible,
popoverVisible: false, popoverVisible: false,
fileList: [], fileList: [],
@ -132,120 +112,128 @@ class PicSearcher extends Component<PicSearcherProps, PicSearcherState> {
if (!localStorage.getItem('disableIconTip')) { if (!localStorage.getItem('disableIconTip')) {
localStorage.setItem('disableIconTip', 'true'); localStorage.setItem('disableIconTip', 'true');
} }
}; }, []);
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
onCopied = (text: string) => { const onCopied = useCallback((text: string) => {
message.success( message.success(
<span> <span>
<code className="copied-code">{text}</code> copied 🎉 <code className="copied-code">{text}</code> copied 🎉
</span>, </span>,
); );
}; }, []);
useEffect(() => {
const script = document.createElement('script');
script.onload = async () => {
await window.antdIconClassifier.load();
setState(prev => ({ ...prev, modelLoaded: true }));
document.addEventListener('paste', onPaste);
};
script.src = 'https://cdn.jsdelivr.net/gh/lewis617/antd-icon-classifier@0.0/dist/main.js';
document.head.appendChild(script);
setState(prev => ({ ...prev, popoverVisible: !localStorage.getItem('disableIconTip') }));
return () => {
document.removeEventListener('paste', onPaste);
};
}, []);
render() { return (
const { <div className="icon-pic-searcher">
intl: { messages }, <Popover
} = this.props; content={messages[`app.docs.components.icon.pic-searcher.intro`]}
const { modalVisible, popoverVisible, icons, fileList, loading, modelLoaded, error } = visible={state.popoverVisible}
this.state; >
return ( <AntdIcons.CameraOutlined className="icon-pic-btn" onClick={toggleModal} />
<div className="icon-pic-searcher"> </Popover>
<Popover <Modal
content={messages[`app.docs.components.icon.pic-searcher.intro`]} title={messages[`app.docs.components.icon.pic-searcher.title`]}
visible={popoverVisible} visible={state.modalVisible}
> onCancel={toggleModal}
<AntdIcons.CameraOutlined className="icon-pic-btn" onClick={this.toggleModal} /> footer={null}
</Popover> >
<Modal {state.modelLoaded || (
title={messages[`app.docs.components.icon.pic-searcher.title`]} <Spin
visible={modalVisible} spinning={!state.modelLoaded}
onCancel={this.toggleModal} tip={messages['app.docs.components.icon.pic-searcher.modelloading']}
footer={null} >
> <div style={{ height: 100 }} />
{modelLoaded || (
<Spin
spinning={!modelLoaded}
tip={messages['app.docs.components.icon.pic-searcher.modelloading']}
>
<div style={{ height: 100 }} />
</Spin>
)}
{modelLoaded && (
<Dragger
accept="image/jpeg, image/png"
listType="picture"
customRequest={(o: any) => this.uploadFile(o.file)}
fileList={fileList}
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
>
<p className="ant-upload-drag-icon">
<AntdIcons.InboxOutlined />
</p>
<p className="ant-upload-text">
{messages['app.docs.components.icon.pic-searcher.upload-text']}
</p>
<p className="ant-upload-hint">
{messages['app.docs.components.icon.pic-searcher.upload-hint']}
</p>
</Dragger>
)}
<Spin spinning={loading} tip={messages['app.docs.components.icon.pic-searcher.matching']}>
<div className="icon-pic-search-result">
{icons.length > 0 && (
<div className="result-tip">
{messages['app.docs.components.icon.pic-searcher.result-tip']}
</div>
)}
<table>
{icons.length > 0 && (
<thead>
<tr>
<th className="col-icon">
{messages['app.docs.components.icon.pic-searcher.th-icon']}
</th>
<th>{messages['app.docs.components.icon.pic-searcher.th-score']}</th>
</tr>
</thead>
)}
<tbody>
{icons.map((icon: iconObject) => {
const { type } = icon;
const iconName = `${type
.split('-')
.map(str => `${str[0].toUpperCase()}${str.slice(1)}`)
.join('')}Outlined`;
return (
<tr key={iconName}>
<td className="col-icon">
<CopyToClipboard text={`<${iconName} />`} onCopy={this.onCopied}>
<Tooltip title={icon.type} placement="right">
{React.createElement(allIcons[iconName])}
</Tooltip>
</CopyToClipboard>
</td>
<td>
<Progress percent={Math.ceil(icon.score * 100)} />
</td>
</tr>
);
})}
</tbody>
</table>
{error && (
<Result
status="500"
title="503"
subTitle={messages['app.docs.components.icon.pic-searcher.server-error']}
/>
)}
</div>
</Spin> </Spin>
</Modal> )}
</div> {state.modelLoaded && (
); <Dragger
} accept="image/jpeg, image/png"
} listType="picture"
customRequest={o => uploadFile(o.file as File)}
fileList={state.fileList}
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
>
<p className="ant-upload-drag-icon">
<AntdIcons.InboxOutlined />
</p>
<p className="ant-upload-text">
{messages['app.docs.components.icon.pic-searcher.upload-text']}
</p>
<p className="ant-upload-hint">
{messages['app.docs.components.icon.pic-searcher.upload-hint']}
</p>
</Dragger>
)}
<Spin
spinning={state.loading}
tip={messages['app.docs.components.icon.pic-searcher.matching']}
>
<div className="icon-pic-search-result">
{state.icons.length > 0 && (
<div className="result-tip">
{messages['app.docs.components.icon.pic-searcher.result-tip']}
</div>
)}
<table>
{state.icons.length > 0 && (
<thead>
<tr>
<th className="col-icon">
{messages['app.docs.components.icon.pic-searcher.th-icon']}
</th>
<th>{messages['app.docs.components.icon.pic-searcher.th-score']}</th>
</tr>
</thead>
)}
<tbody>
{state.icons.map(icon => {
const { type } = icon;
const iconName = `${type
.split('-')
.map(str => `${str[0].toUpperCase()}${str.slice(1)}`)
.join('')}Outlined`;
return (
<tr key={iconName}>
<td className="col-icon">
<CopyToClipboard text={`<${iconName} />`} onCopy={onCopied}>
<Tooltip title={icon.type} placement="right">
{React.createElement(allIcons[iconName])}
</Tooltip>
</CopyToClipboard>
</td>
<td>
<Progress percent={Math.ceil(icon.score * 100)} />
</td>
</tr>
);
})}
</tbody>
</table>
{state.error && (
<Result
status="500"
title="503"
subTitle={messages['app.docs.components.icon.pic-searcher.server-error']}
/>
)}
</div>
</Spin>
</Modal>
</div>
);
};
export default injectIntl(PicSearcher); export default injectIntl(PicSearcher);

View File

@ -7,7 +7,7 @@ import debounce from 'lodash/debounce';
import Category from './Category'; import Category from './Category';
import IconPicSearcher from './IconPicSearcher'; import IconPicSearcher from './IconPicSearcher';
import { FilledIcon, OutlinedIcon, TwoToneIcon } from './themeIcons'; import { FilledIcon, OutlinedIcon, TwoToneIcon } from './themeIcons';
import type { Categories, CategoriesKeys } from './fields'; import type { CategoriesKeys } from './fields';
import { categories } from './fields'; import { categories } from './fields';
export enum ThemeType { export enum ThemeType {
@ -16,9 +16,7 @@ export enum ThemeType {
TwoTone = 'TwoTone', TwoTone = 'TwoTone',
} }
const allIcons: { const allIcons: { [key: string]: any } = AntdIcons;
[key: string]: any;
} = AntdIcons;
interface IconDisplayProps { interface IconDisplayProps {
intl: any; intl: any;
@ -29,36 +27,28 @@ interface IconDisplayState {
searchKey: string; searchKey: string;
} }
class IconDisplay extends React.PureComponent<IconDisplayProps, IconDisplayState> { const IconDisplay: React.FC<IconDisplayProps> = ({ intl }) => {
static categories: Categories = categories; const { messages } = intl;
const [displayState, setDisplayState] = React.useState<IconDisplayState>({
static newIconNames: string[] = [];
state: IconDisplayState = {
theme: ThemeType.Outlined, theme: ThemeType.Outlined,
searchKey: '', searchKey: '',
}; });
constructor(props: IconDisplayProps) { const newIconNames: string[] = [];
super(props);
this.handleSearchIcon = debounce(this.handleSearchIcon, 300);
}
handleChangeTheme = (e: RadioChangeEvent) => { const handleSearchIcon = React.useCallback(
this.setState({ debounce((searchKey: string) => {
theme: e.target.value as ThemeType, setDisplayState(prevState => ({ ...prevState, searchKey }));
}); }),
}; [],
);
handleSearchIcon = (searchKey: string) => { const handleChangeTheme = React.useCallback((e: RadioChangeEvent) => {
this.setState(prevState => ({ setDisplayState(prevState => ({ ...prevState, theme: e.target.value as ThemeType }));
...prevState, }, []);
searchKey,
}));
};
renderCategories() { const renderCategories = React.useMemo<React.ReactNode | React.ReactNode[]>(() => {
const { searchKey = '', theme } = this.state; const { searchKey = '', theme } = displayState;
const categoriesResult = Object.keys(categories) const categoriesResult = Object.keys(categories)
.map((key: CategoriesKeys) => { .map((key: CategoriesKeys) => {
@ -87,50 +77,43 @@ class IconDisplay extends React.PureComponent<IconDisplayProps, IconDisplayState
title={category as CategoriesKeys} title={category as CategoriesKeys}
theme={theme} theme={theme}
icons={icons} icons={icons}
newIcons={IconDisplay.newIconNames} newIcons={newIconNames}
/> />
)); ));
return categoriesResult.length === 0 ? <Empty style={{ margin: '2em 0' }} /> : categoriesResult; return categoriesResult.length === 0 ? <Empty style={{ margin: '2em 0' }} /> : categoriesResult;
} }, [displayState.searchKey, displayState.theme]);
return (
render() { <>
const { <div style={{ display: 'flex', justifyContent: 'space-between' }}>
intl: { messages }, <Radio.Group
} = this.props; value={displayState.theme}
return ( onChange={handleChangeTheme}
<> size="large"
<div style={{ display: 'flex', justifyContent: 'space-between' }}> buttonStyle="solid"
<Radio.Group >
value={this.state.theme} <Radio.Button value={ThemeType.Outlined}>
onChange={this.handleChangeTheme} <Icon component={OutlinedIcon} /> {messages['app.docs.components.icon.outlined']}
size="large" </Radio.Button>
buttonStyle="solid" <Radio.Button value={ThemeType.Filled}>
> <Icon component={FilledIcon} /> {messages['app.docs.components.icon.filled']}
<Radio.Button value={ThemeType.Outlined}> </Radio.Button>
<Icon component={OutlinedIcon} /> {messages['app.docs.components.icon.outlined']} <Radio.Button value={ThemeType.TwoTone}>
</Radio.Button> <Icon component={TwoToneIcon} /> {messages['app.docs.components.icon.two-tone']}
<Radio.Button value={ThemeType.Filled}> </Radio.Button>
<Icon component={FilledIcon} /> {messages['app.docs.components.icon.filled']} </Radio.Group>
</Radio.Button> <Input.Search
<Radio.Button value={ThemeType.TwoTone}> placeholder={messages['app.docs.components.icon.search.placeholder']}
<Icon component={TwoToneIcon} /> {messages['app.docs.components.icon.two-tone']} style={{ margin: '0 10px', flex: 1 }}
</Radio.Button> allowClear
</Radio.Group> onChange={e => handleSearchIcon(e.currentTarget.value)}
<Input.Search size="large"
placeholder={messages['app.docs.components.icon.search.placeholder']} autoFocus
style={{ margin: '0 10px', flex: 1 }} suffix={<IconPicSearcher />}
allowClear />
onChange={e => this.handleSearchIcon(e.currentTarget.value)} </div>
size="large" {renderCategories}
autoFocus </>
suffix={<IconPicSearcher />} );
/> };
</div>
{this.renderCategories()}
</>
);
}
}
export default injectIntl(IconDisplay); export default injectIntl(IconDisplay);

View File

@ -1,8 +1,6 @@
import React from 'react'; import React, { useMemo } from 'react';
import { message } from 'antd';
import RcFooter from 'rc-footer'; import RcFooter from 'rc-footer';
import { Link } from 'bisheng/router'; import { Link } from 'bisheng/router';
import { presetPalettes } from '@ant-design/colors';
import type { WrappedComponentProps } from 'react-intl'; import type { WrappedComponentProps } from 'react-intl';
import { FormattedMessage, injectIntl } from 'react-intl'; import { FormattedMessage, injectIntl } from 'react-intl';
import { import {
@ -19,21 +17,13 @@ import {
QuestionCircleOutlined, QuestionCircleOutlined,
BgColorsOutlined, BgColorsOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import ColorPicker from '../Color/ColorPicker'; import type { FooterColumn } from 'rc-footer/lib/column';
import { loadScript, getLocalizedPathname } from '../utils'; import { getLocalizedPathname } from '../utils';
class Footer extends React.Component<WrappedComponentProps & { location: any }> {
lessLoaded = false;
state = {
color: presetPalettes.blue.primary,
};
getColumns() {
const { intl, location } = this.props;
const Footer: React.FC<WrappedComponentProps & { location: any }> = props => {
const { intl, location } = props;
const getColumns = useMemo<FooterColumn[]>(() => {
const isZhCN = intl.locale === 'zh-CN'; const isZhCN = intl.locale === 'zh-CN';
const getLinkHash = (path: string, hash: { zhCN: string; enUS: string }) => { const getLinkHash = (path: string, hash: { zhCN: string; enUS: string }) => {
const pathName = getLocalizedPathname(path, isZhCN, location.query, hash); const pathName = getLocalizedPathname(path, isZhCN, location.query, hash);
const { pathname, query = {} } = pathName; const { pathname, query = {} } = pathName;
@ -182,7 +172,7 @@ class Footer extends React.Component<WrappedComponentProps & { location: any }>
enUS: 'JoinUs', enUS: 'JoinUs',
}), }),
LinkComponent: Link, LinkComponent: Link,
} as any); } as unknown as typeof col2['items'][number]);
} }
const col3 = { const col3 = {
@ -318,82 +308,23 @@ class Footer extends React.Component<WrappedComponentProps & { location: any }>
}, },
], ],
}; };
return [col1, col2, col3, col4]; return [col1, col2, col3, col4];
} }, [intl.locale, location.query]);
handleColorChange = (color: string) => { return (
const { <RcFooter
intl: { messages }, columns={getColumns}
} = this.props; bottom={
message.loading({ <>
content: messages['app.footer.primary-color-changing'] as string, Made with <span style={{ color: '#fff' }}></span> by
key: 'change-primary-color', {/* eslint-disable-next-line react/jsx-curly-brace-presence */}{' '}
}); <a target="_blank" rel="noopener noreferrer" href="https://xtech.antfin.com">
const changeColor = () => { <FormattedMessage id="app.footer.company" />
(window as any).less </a>
.modifyVars({ </>
'@primary-color': color, }
}) />
.then(() => { );
message.success({ };
content: messages['app.footer.primary-color-changed'] as string,
key: 'change-primary-color',
});
this.setState({ color });
});
};
const lessUrl = 'https://gw.alipayobjects.com/os/lib/less/3.10.3/dist/less.min.js';
if (this.lessLoaded) {
changeColor();
} else {
(window as any).less = {
async: true,
javascriptEnabled: true,
};
loadScript(lessUrl).then(() => {
this.lessLoaded = true;
changeColor();
});
}
};
renderThemeChanger() {
const { color } = this.state;
const colors = Object.keys(presetPalettes).filter(item => item !== 'grey');
return (
<ColorPicker
small
color={color}
position="top"
presetColors={[
...colors.map(c => presetPalettes[c][5]),
...colors.map(c => presetPalettes[c][4]),
...colors.map(c => presetPalettes[c][6]),
]}
onChangeComplete={this.handleColorChange}
/>
);
}
render() {
return (
<RcFooter
columns={this.getColumns()}
bottom={
<>
Made with <span style={{ color: '#fff' }}></span> by
{/* eslint-disable-next-line react/jsx-curly-brace-presence */}{' '}
<a target="_blank" rel="noopener noreferrer" href="https://xtech.antfin.com">
<FormattedMessage id="app.footer.company" />
</a>
</>
}
/>
);
}
}
export default injectIntl(Footer); export default injectIntl(Footer);

View File

@ -1,9 +1,11 @@
import React from 'react'; import React, { useCallback, useContext, useEffect, useRef, useState, useMemo } from 'react';
import type { WrappedComponentProps } from 'react-intl';
import { FormattedMessage, injectIntl } from 'react-intl'; import { FormattedMessage, injectIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Select, Row, Col, Popover, Button, Modal } from 'antd'; import { Select, Row, Col, Popover, Button, Modal } from 'antd';
import { MenuOutlined } from '@ant-design/icons'; import { MenuOutlined } from '@ant-design/icons';
import canUseDom from 'rc-util/lib/Dom/canUseDom'; import canUseDom from 'rc-util/lib/Dom/canUseDom';
import type { DirectionType } from 'antd/es/config-provider';
import * as utils from '../../utils'; import * as utils from '../../utils';
import packageJson from '../../../../../package.json'; import packageJson from '../../../../../package.json';
import Logo from './Logo'; import Logo from './Logo';
@ -26,13 +28,11 @@ const { Option } = Select;
const antdVersion: string = packageJson.version; const antdVersion: string = packageJson.version;
export interface HeaderProps { export interface HeaderProps {
intl: { intl: { locale: string };
locale: string;
};
location: { pathname: string; query: any }; location: { pathname: string; query: any };
router: any; router: any;
themeConfig: { docVersions: Record<string, string> }; themeConfig: { docVersions: Record<string, string> };
changeDirection: (direction: string) => void; changeDirection: (direction: DirectionType) => void;
} }
let docsearch: any; let docsearch: any;
@ -61,7 +61,7 @@ function initDocSearch({ isZhCN, router }: { isZhCN: boolean; router: any }) {
transformData: AlgoliaConfig.transformData, transformData: AlgoliaConfig.transformData,
debug: AlgoliaConfig.debug, debug: AlgoliaConfig.debug,
// https://docsearch.algolia.com/docs/behavior#handleselected // https://docsearch.algolia.com/docs/behavior#handleselected
handleSelected: (input: any, _$1: unknown, suggestion: any) => { handleSelected(input: any, _$1: unknown, suggestion: any) {
router.push(suggestion.url); router.push(suggestion.url);
setTimeout(() => { setTimeout(() => {
input.setVal(''); input.setVal('');
@ -88,37 +88,44 @@ interface HeaderState {
showTechUIButton: boolean; showTechUIButton: boolean;
} }
class Header extends React.Component<HeaderProps, HeaderState> { const Header: React.FC<HeaderProps & WrappedComponentProps<'intl'>> = props => {
static contextType = SiteContext; const { intl, router, location, themeConfig, changeDirection } = props;
const [headerState, setHeaderState] = useState<HeaderState>({
pingTimer: NodeJS.Timeout;
state = {
menuVisible: false, menuVisible: false,
windowWidth: 1400, windowWidth: 1400,
searching: false, searching: false,
showTechUIButton: false, showTechUIButton: false,
}; });
const { direction } = useContext<SiteContextProps>(SiteContext);
const pingTimer = useRef<NodeJS.Timeout | null>(null);
context: SiteContextProps; const handleHideMenu = useCallback(() => {
setHeaderState(prev => ({ ...prev, menuVisible: false }));
}, []);
const onWindowResize = useCallback(() => {
setHeaderState(prev => ({ ...prev, windowWidth: window.innerWidth }));
}, []);
const onTriggerSearching = useCallback((searching: boolean) => {
setHeaderState(prev => ({ ...prev, searching }));
}, []);
const handleShowMenu = useCallback(() => {
setHeaderState(prev => ({ ...prev, menuVisible: true }));
}, []);
const onMenuVisibleChange = useCallback((visible: boolean) => {
setHeaderState(prev => ({ ...prev, menuVisible: visible }));
}, []);
const onDirectionChange = useCallback(() => {
changeDirection(direction !== 'rtl' ? 'rtl' : 'ltr');
}, [direction]);
componentDidMount() { useEffect(() => {
const { intl, router } = this.props; router.listen(handleHideMenu);
router.listen(this.handleHideMenu); initDocSearch({ isZhCN: intl.locale === 'zh-CN', router });
onWindowResize();
initDocSearch({ window.addEventListener('resize', onWindowResize);
isZhCN: intl.locale === 'zh-CN', pingTimer.current = ping(status => {
router,
});
window.addEventListener('resize', this.onWindowResize);
this.onWindowResize();
this.pingTimer = ping(status => {
if (status !== 'timeout' && status !== 'error') { if (status !== 'timeout' && status !== 'error') {
this.setState({ setHeaderState(prev => ({ ...prev, showTechUIButton: true }));
showTechUIButton: true,
});
if ( if (
process.env.NODE_ENV === 'production' && process.env.NODE_ENV === 'production' &&
window.location.host !== 'ant-design.antgroup.com' && window.location.host !== 'ant-design.antgroup.com' &&
@ -128,86 +135,29 @@ class Header extends React.Component<HeaderProps, HeaderState> {
title: '提示', title: '提示',
content: '内网用户推荐访问国内镜像以获得极速体验~', content: '内网用户推荐访问国内镜像以获得极速体验~',
okText: '🚀 立刻前往', okText: '🚀 立刻前往',
onOk: () => { cancelText: '不再弹出',
closable: true,
onOk() {
window.open('https://ant-design.antgroup.com', '_self'); window.open('https://ant-design.antgroup.com', '_self');
disableAntdMirrorModal(); disableAntdMirrorModal();
}, },
cancelText: '不再弹出', onCancel() {
onCancel: () => {
disableAntdMirrorModal(); disableAntdMirrorModal();
}, },
closable: true,
}); });
} }
} }
}); });
} return () => {
window.removeEventListener('resize', onWindowResize);
componentWillUnmount() { if (pingTimer.current) {
window.removeEventListener('resize', this.onWindowResize); clearTimeout(pingTimer.current);
clearTimeout(this.pingTimer); }
} };
}, []);
onWindowResize = () => {
this.setState({
windowWidth: window.innerWidth,
});
};
onTriggerSearching = (searching: boolean) => {
this.setState({ searching });
};
handleShowMenu = () => {
this.setState({
menuVisible: true,
});
};
handleHideMenu = () => {
this.setState({
menuVisible: false,
});
};
onDirectionChange = () => {
const { changeDirection } = this.props;
const { direction } = this.context;
if (direction !== 'rtl') {
changeDirection('rtl');
} else {
changeDirection('ltr');
}
};
getNextDirectionText = () => {
const { direction } = this.context;
if (direction !== 'rtl') {
return 'RTL';
}
return 'LTR';
};
getDropdownStyle = (): React.CSSProperties => {
const { direction } = this.context;
if (direction === 'rtl') {
return {
direction: 'ltr',
textAlign: 'right',
};
}
return {};
};
onMenuVisibleChange = (visible: boolean) => {
this.setState({
menuVisible: visible,
});
};
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
handleVersionChange = (url: string) => { const handleVersionChange = useCallback((url: string) => {
const currentUrl = window.location.href; const currentUrl = window.location.href;
const currentPathname = window.location.pathname; const currentPathname = window.location.pathname;
if (/overview/.test(currentPathname) && /0?[1-39][0-3]?x/.test(url)) { if (/overview/.test(currentPathname) && /0?[1-39][0-3]?x/.test(url)) {
@ -218,184 +168,168 @@ class Header extends React.Component<HeaderProps, HeaderState> {
return; return;
} }
window.location.href = currentUrl.replace(window.location.origin, url); window.location.href = currentUrl.replace(window.location.origin, url);
}; }, []);
onLangChange = () => { const onLangChange = useCallback(() => {
const { const { pathname, query } = location;
location: { pathname, query },
} = this.props;
const currentProtocol = `${window.location.protocol}//`; const currentProtocol = `${window.location.protocol}//`;
const currentHref = window.location.href.slice(currentProtocol.length); const currentHref = window.location.href.slice(currentProtocol.length);
if (utils.isLocalStorageNameSupported()) { if (utils.isLocalStorageNameSupported()) {
localStorage.setItem('locale', utils.isZhCN(pathname) ? 'en-US' : 'zh-CN'); localStorage.setItem('locale', utils.isZhCN(pathname) ? 'en-US' : 'zh-CN');
} }
window.location.href = window.location.href =
currentProtocol + currentProtocol +
currentHref.replace( currentHref.replace(
window.location.pathname, window.location.pathname,
utils.getLocalizedPathname(pathname, !utils.isZhCN(pathname), query).pathname, utils.getLocalizedPathname(pathname, !utils.isZhCN(pathname), query).pathname,
); );
}; }, [location]);
render() { const getNextDirectionText = useMemo<string>(
return ( () => (direction !== 'rtl' ? 'RTL' : 'LTR'),
<SiteContext.Consumer> [direction],
{({ isMobile }) => { );
const { menuVisible, windowWidth, searching, showTechUIButton } = this.state;
const { direction } = this.context;
const {
location,
themeConfig,
intl: { locale },
router,
} = this.props;
const docVersions: Record<string, string> = {
[antdVersion]: antdVersion,
...themeConfig.docVersions,
};
const versionOptions = Object.keys(docVersions).map(version => (
<Option value={docVersions[version]} key={version}>
{version}
</Option>
));
const pathname = location.pathname.replace(/(^\/|\/$)/g, ''); const getDropdownStyle = useMemo<React.CSSProperties>(
() => (direction === 'rtl' ? { direction: 'ltr', textAlign: 'right' } : {}),
[direction],
);
const isHome = ['', 'index', 'index-cn'].includes(pathname); return (
<SiteContext.Consumer>
{({ isMobile }) => {
const { menuVisible, windowWidth, searching, showTechUIButton } = headerState;
const docVersions: Record<string, string> = {
[antdVersion]: antdVersion,
...themeConfig.docVersions,
};
const versionOptions = Object.keys(docVersions).map(version => (
<Option value={docVersions[version]} key={version}>
{version}
</Option>
));
const isZhCN = locale === 'zh-CN'; const pathname = location.pathname.replace(/(^\/|\/$)/g, '');
const isRTL = direction === 'rtl';
let responsive: null | 'narrow' | 'crowded' = null;
if (windowWidth < RESPONSIVE_XS) {
responsive = 'crowded';
} else if (windowWidth < RESPONSIVE_SM) {
responsive = 'narrow';
}
const headerClassName = classNames({ const isHome = ['', 'index', 'index-cn'].includes(pathname);
clearfix: true,
'home-header': isHome,
});
const sharedProps = { const isZhCN = intl.locale === 'zh-CN';
isZhCN, const isRTL = direction === 'rtl';
isRTL, let responsive: null | 'narrow' | 'crowded' = null;
}; if (windowWidth < RESPONSIVE_XS) {
responsive = 'crowded';
} else if (windowWidth < RESPONSIVE_SM) {
responsive = 'narrow';
}
const navigationNode = ( const headerClassName = classNames({
<Navigation clearfix: true,
key="nav" 'home-header': isHome,
{...sharedProps} });
location={location}
responsive={responsive}
isMobile={isMobile}
showTechUIButton={showTechUIButton}
pathname={pathname}
directionText={this.getNextDirectionText()}
onLangChange={this.onLangChange}
onDirectionChange={this.onDirectionChange}
/>
);
let menu: (React.ReactElement | null)[] = [ const sharedProps = {
navigationNode, isZhCN,
<Select isRTL,
key="version" };
className="version"
size="small"
defaultValue={antdVersion}
onChange={this.handleVersionChange}
dropdownStyle={this.getDropdownStyle()}
getPopupContainer={trigger => trigger.parentNode}
>
{versionOptions}
</Select>,
<Button
size="small"
onClick={this.onLangChange}
className="header-button header-lang-button"
key="lang-button"
>
<FormattedMessage id="app.header.lang" />
</Button>,
<Button
size="small"
onClick={this.onDirectionChange}
className="header-button header-direction-button"
key="direction-button"
>
{this.getNextDirectionText()}
</Button>,
<More key="more" {...sharedProps} />,
<Github key="github" responsive={responsive} />,
];
if (windowWidth < RESPONSIVE_XS) { const navigationNode = (
menu = searching ? [] : [navigationNode]; <Navigation
} else if (windowWidth < RESPONSIVE_SM) { key="nav"
menu = searching ? [] : menu; {...sharedProps}
} location={location}
responsive={responsive}
isMobile={isMobile}
showTechUIButton={showTechUIButton}
pathname={pathname}
directionText={getNextDirectionText}
onLangChange={onLangChange}
onDirectionChange={onDirectionChange}
/>
);
const colProps = isHome let menu: (React.ReactElement | null)[] = [
? [{ flex: 'none' }, { flex: 'auto' }] navigationNode,
: [ <Select
{ key="version"
xxl: 4, className="version"
xl: 5, size="small"
lg: 6, defaultValue={antdVersion}
md: 6, onChange={handleVersionChange}
sm: 24, dropdownStyle={getDropdownStyle}
xs: 24, getPopupContainer={trigger => trigger.parentNode}
}, >
{ {versionOptions}
xxl: 20, </Select>,
xl: 19, <Button
lg: 18, size="small"
md: 18, onClick={onLangChange}
sm: 0, className="header-button header-lang-button"
xs: 0, key="lang-button"
}, >
]; <FormattedMessage id="app.header.lang" />
</Button>,
<Button
size="small"
onClick={onDirectionChange}
className="header-button header-direction-button"
key="direction-button"
>
{getNextDirectionText}
</Button>,
<More key="more" {...sharedProps} />,
<Github key="github" responsive={responsive} />,
];
return ( if (windowWidth < RESPONSIVE_XS) {
<header id="header" className={headerClassName}> menu = searching ? [] : [navigationNode];
{isMobile && ( } else if (windowWidth < RESPONSIVE_SM) {
<Popover menu = searching ? [] : menu;
overlayClassName="popover-menu" }
placement="bottomRight"
content={menu}
trigger="click"
visible={menuVisible}
arrowPointAtCenter
onVisibleChange={this.onMenuVisibleChange}
>
<MenuOutlined className="nav-phone-icon" onClick={this.handleShowMenu} />
</Popover>
)}
<Row style={{ flexFlow: 'nowrap', height: 64 }}>
<Col {...colProps[0]}>
<Logo {...sharedProps} location={location} />
</Col>
<Col {...colProps[1]} className="menu-row">
<SearchBar
key="search"
{...sharedProps}
router={router}
algoliaConfig={AlgoliaConfig}
responsive={responsive}
onTriggerFocus={this.onTriggerSearching}
/>
{!isMobile && menu}
</Col>
</Row>
</header>
);
}}
</SiteContext.Consumer>
);
}
}
export default injectIntl(Header as any); const colProps = isHome
? [{ flex: 'none' }, { flex: 'auto' }]
: [
{ xxl: 4, xl: 5, lg: 6, md: 6, sm: 24, xs: 24 },
{ xxl: 20, xl: 19, lg: 18, md: 18, sm: 0, xs: 0 },
];
return (
<header id="header" className={headerClassName}>
{isMobile && (
<Popover
overlayClassName="popover-menu"
placement="bottomRight"
content={menu}
trigger="click"
visible={menuVisible}
arrowPointAtCenter
onVisibleChange={onMenuVisibleChange}
>
<MenuOutlined className="nav-phone-icon" onClick={handleShowMenu} />
</Popover>
)}
<Row style={{ flexFlow: 'nowrap', height: 64 }}>
<Col {...colProps[0]}>
<Logo {...sharedProps} location={location} />
</Col>
<Col {...colProps[1]} className="menu-row">
<SearchBar
key="search"
{...sharedProps}
router={router}
algoliaConfig={AlgoliaConfig}
responsive={responsive}
onTriggerFocus={onTriggerSearching}
/>
{!isMobile && menu}
</Col>
</Row>
</header>
);
}}
</SiteContext.Consumer>
);
};
export default injectIntl(Header);

View File

@ -1,8 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import type { DirectionType } from 'antd/es/config-provider';
export interface SiteContextProps { export interface SiteContextProps {
isMobile: boolean; isMobile: boolean;
direction: string; direction: DirectionType;
} }
const SiteContext = React.createContext<SiteContextProps>({ const SiteContext = React.createContext<SiteContextProps>({