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();
});
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 = () => (
<AutoComplete
dropdownClassName="certain-category-search-dropdown"
popupClassName="certain-category-search-dropdown"
dropdownMatchSelectWidth={500}
style={{ width: 250 }}
options={options}

View File

@ -31,7 +31,7 @@ The differences with Select are:
| defaultOpen | Initial open state of dropdown | boolean | - | |
| defaultValue | Initial selected option | string | - | |
| 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 | |
| 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` | |

View File

@ -41,6 +41,12 @@ export interface AutoCompleteProps<
> {
dataSource?: DataSourceItemType[];
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 {
@ -51,7 +57,14 @@ const AutoComplete: React.ForwardRefRenderFunction<RefSelectProps, AutoCompleteP
props,
ref,
) => {
const { prefixCls: customizePrefixCls, className, children, dataSource } = props;
const {
prefixCls: customizePrefixCls,
className,
popupClassName,
dropdownClassName,
children,
dataSource,
} = props;
const childNodes: React.ReactElement[] = toArray(children);
// ============================= Input =============================
@ -112,6 +125,12 @@ const AutoComplete: React.ForwardRefRenderFunction<RefSelectProps, AutoCompleteP
'`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(
!customizeInput || !('size' in props),
'AutoComplete',
@ -128,6 +147,7 @@ const AutoComplete: React.ForwardRefRenderFunction<RefSelectProps, AutoCompleteP
ref={ref}
{...omit(props, ['dataSource'])}
prefixCls={prefixCls}
dropdownClassName={popupClassName || dropdownClassName}
className={classNames(`${prefixCls}-auto-complete`, className)}
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 | - | |
| defaultValue | 指定默认选中的条目 | string | - | |
| disabled | 是否禁用 | boolean | false | |
| dropdownClassName | 下拉菜单的 className 属性 | string | - | |
| popupClassName | 下拉菜单的 className 属性 | string | - | 4.23.0 |
| dropdownMatchSelectWidth | 下拉菜单和选择器同宽。默认将设置 `min-width`当值小于选择框宽度时会被忽略。false 时会关闭虚拟滚动 | boolean \| number | true | |
| filterOption | 是否根据输入项进行筛选。当其为一个函数时,会接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 true反之则返回 false | boolean \| function(inputValue, option) | true | |
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。[示例](https://codesandbox.io/s/4j168r7jw0) | function(triggerNode) | () => document.body | |

View File

@ -540,7 +540,7 @@ describe('Cascader', () => {
it('popupClassName', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const { container } = render(
<Cascader open popupPlacement="bottomLeft" popupClassName="mock-cls" />,
<Cascader open popupPlacement="bottomLeft" dropdownClassName="mock-cls" />,
);
expect(container.querySelector('.mock-cls')).toBeTruthy();
@ -549,7 +549,7 @@ describe('Cascader', () => {
expect(global.triggerProps.popupPlacement).toEqual('bottomLeft');
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();

View File

@ -30,7 +30,7 @@ Cascade selection box.
| defaultValue | Initial selected value | string\[] \| number\[] | \[] | |
| disabled | Whether disabled select | boolean | false | |
| 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 |
| 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` | |

View File

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

View File

@ -31,7 +31,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg
| defaultValue | 默认的选中项 | string\[] \| number\[] | \[] | |
| disabled | 禁用 | boolean | false | |
| 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 |
| expandIcon | 自定义次级菜单展开图标 | ReactNode | - | 4.4.0 |
| expandTrigger | 次级菜单的展开方式,可选 'click' 和 'hover' | string | `click` | |

View File

@ -188,6 +188,20 @@ describe('DatePicker', () => {
).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', () => {
const startDate = dayjs('1982-02-12');
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 { getRangePlaceholder, transPlacement2DropdownAlign } from '../util';
import type { CommonPickerMethods, PickerComponentClass } from './interface';
import warning from '../../_util/warning';
import useStyle from '../style';
@ -28,7 +29,14 @@ export default function generateRangePicker<DateType>(
const RangePicker = forwardRef<
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) => {
const {
prefixCls: customizePrefixCls,
@ -39,8 +47,9 @@ export default function generateRangePicker<DateType>(
disabled: customDisabled,
bordered = true,
placeholder,
status: customStatus,
popupClassName,
dropdownClassName,
status: customStatus,
...restProps
} = props;
@ -59,6 +68,12 @@ export default function generateRangePicker<DateType>(
...(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 =====================
const size = React.useContext(SizeContext);
const mergedSize = customizeSize || size;
@ -128,7 +143,7 @@ export default function generateRangePicker<DateType>(
generateConfig={generateConfig}
components={Components}
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> & {
status?: InputStatus;
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>(
picker?: PickerMode,
displayName?: string,
@ -43,18 +48,13 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
bordered = true,
placement,
placeholder,
popupClassName,
dropdownClassName,
disabled: customDisabled,
status: customStatus,
dropdownClassName,
...restProps
} = props;
warning(
picker !== 'quarter',
displayName!,
`DatePicker.${displayName} is legacy usage. Please use DatePicker[picker='${picker}'] directly.`,
);
const { getPrefixCls, direction, getPopupContainer } = useContext(ConfigContext);
const prefixCls = getPrefixCls('picker', customizePrefixCls);
const innerRef = React.useRef<RCPicker<DateType>>(null);
@ -86,6 +86,18 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
};
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 =====================
const size = React.useContext(SizeContext);
const mergedSize = customizeSize || size;
@ -146,7 +158,7 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
components={Components}
direction={direction}
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 | - | |
| disabled | Determine whether the DatePicker is disabled | boolean | false | |
| 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) | - | |
| 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) | |

View File

@ -60,7 +60,7 @@ import locale from 'antd/es/locale/zh_CN';
| dateRender | 自定义日期单元格的内容 | function(currentDate: dayjs, today: dayjs) => React.ReactNode | - | |
| disabled | 禁用 | boolean | false | |
| disabledDate | 不可选择的日期 | (currentDate: dayjs) => boolean | - | |
| dropdownClassName | 额外的弹出日历 className | string | - | |
| popupClassName | 额外的弹出日历 className | string | - | 4.23.0 |
| getPopupContainer | 定义浮层的容器,默认为 body 上新建 div | function(trigger) | - | |
| inputReadOnly | 设置输入框为只读(避免在移动设备上打开虚拟键盘) | boolean | false | |
| 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
[`&-left ${wrapperCls}`]: {
[`&-left > ${wrapperCls}`]: {
top: 0,
bottom: 0,
left: {
@ -78,7 +78,7 @@ const genDrawerStyle: GenerateStyle<DrawerToken> = (token: DrawerToken) => {
},
boxShadow: token.boxShadowDrawerRight,
},
[`&-right ${wrapperCls}`]: {
[`&-right > ${wrapperCls}`]: {
top: 0,
right: {
_skip_check_: true,
@ -87,12 +87,12 @@ const genDrawerStyle: GenerateStyle<DrawerToken> = (token: DrawerToken) => {
bottom: 0,
boxShadow: token.boxShadowDrawerLeft,
},
[`&-top ${wrapperCls}`]: {
[`&-top > ${wrapperCls}`]: {
top: 0,
insetInline: 0,
boxShadow: token.boxShadowDrawerDown,
},
[`&-bottom ${wrapperCls}`]: {
[`&-bottom > ${wrapperCls}`]: {
bottom: 0,
insetInline: 0,
boxShadow: token.boxShadowDrawerUp,

View File

@ -4246,7 +4246,7 @@ exports[`renders ./components/dropdown/demo/dropdown-button.md extend context co
</div>
<div
class="ant-space-item"
style="padding-bottom:8px"
style="margin-right:8px;padding-bottom:8px"
>
<button
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
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>
`;

View File

@ -334,7 +334,7 @@ exports[`renders ./components/dropdown/demo/dropdown-button.md correctly 1`] = `
</div>
<div
class="ant-space-item"
style="padding-bottom:8px"
style="margin-right:8px;padding-bottom:8px"
>
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
@ -375,6 +375,47 @@ exports[`renders ./components/dropdown/demo/dropdown-button.md correctly 1`] = `
</div>
</button>
</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>
`;

View File

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

View File

@ -17,6 +17,7 @@ export type DropdownButtonType = 'default' | 'primary' | 'ghost' | 'dashed' | 'l
export interface DropdownButtonProps extends ButtonGroupProps, DropdownProps {
type?: DropdownButtonType;
htmlType?: ButtonHTMLType;
danger?: boolean;
disabled?: boolean;
loading?: ButtonProps['loading'];
onClick?: React.MouseEventHandler<HTMLButtonElement>;
@ -41,6 +42,7 @@ const DropdownButton: DropdownButtonInterface = props => {
const {
prefixCls: customizePrefixCls,
type = 'default',
danger,
disabled,
loading,
onClick,
@ -97,6 +99,7 @@ const DropdownButton: DropdownButtonInterface = props => {
const leftButton = (
<Button
type={type}
danger={danger}
disabled={disabled}
loading={loading}
onClick={onClick}
@ -108,7 +111,7 @@ const DropdownButton: DropdownButtonInterface = props => {
</Button>
);
const rightButton = <Button type={type} icon={icon} />;
const rightButton = <Button type={type} danger={danger} icon={icon} />;
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\[] | - | |
| 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 | - | |
| icon | Icon (appears on the right) | ReactNode | - | |
| 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\[] | - | |
| loading | 设置按钮载入状态 | boolean \| { delay: number } | false | |
| danger | 设置危险按钮 | boolean | - | 4.23.0 |
| disabled | 菜单是否禁用 | boolean | - | |
| icon | 右侧的 icon | ReactNode | - | |
| overlay | 菜单 | [Menu](/components/menu/) | - | |

View File

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

View File

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

View File

@ -272,12 +272,14 @@ describe('should support showCount', () => {
const { container } = render(
<Input
maxLength={5}
showCount={{ formatter: ({ count, maxLength }) => `${count}, ${maxLength}` }}
showCount={{
formatter: ({ value, count, maxLength }) => `${value}, ${count}, ${maxLength}`,
}}
value="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(
<TextArea
maxLength={5}
showCount={{ formatter: ({ count, maxLength }) => `${count}, ${maxLength}` }}
showCount={{
formatter: ({ value, count, maxLength }) => `${value}, ${count}, ${maxLength}`,
}}
value="12345"
/>,
);
expect(container.querySelector('textarea').value).toBe('12345');
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 | |
| id | The ID for input | string | - | |
| 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 |
| 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` | - | |
@ -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 |
| defaultValue | The initial input content | string | - | |
| 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 | - | |
| 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 }) | - | |

View File

@ -27,7 +27,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/xS9YEJhfe/Input.svg
| disabled | 是否禁用状态,默认为 false | boolean | false | |
| id | 输入框的 id | string | - | |
| 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 |
| prefix | 带有前缀图标的 input | ReactNode | - | |
| size | 控件大小。注:标准表单内的输入框大小限制为 `middle` | `large` \| `middle` \| `small` | - | |
@ -50,7 +50,7 @@ Input 的其他属性和 React 自带的 [input](https://reactjs.org/docs/dom-el
| bordered | 是否有边框 | boolean | true | 4.5.0 |
| defaultValue | 输入框默认内容 | string | - | |
| 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 | - | |
| onPressEnter | 按下回车的回调 | function(e) | - | |
| onResize | resize 回调 | function({ width, height }) | - | |

View File

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

View File

@ -91,6 +91,16 @@ describe('Select', () => {
expect(container.querySelectorAll('.anticon-down').length).toBe(0);
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', () => {
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 | - | |
| defaultValue | Initial selected option | string \| string\[]<br />number \| number\[]<br />LabeledValue \| LabeledValue\[] | - | |
| 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 | |
| dropdownRender | Customize dropdown content | (originNode: ReactNode) => ReactNode | - | |
| 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 { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import getIcons from './utils/iconUtil';
import warning from '../_util/warning';
import useStyle from './style';
import genPurePanel from '../_util/PurePanel';
@ -56,6 +57,12 @@ export interface SelectProps<
placement?: SelectCommonPlacement;
mode?: 'multiple' | 'tags';
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';
@ -67,6 +74,7 @@ const InternalSelect = <OptionType extends BaseOptionType | DefaultOptionType =
className,
getPopupContainer,
dropdownClassName,
popupClassName,
listHeight = 256,
placement,
listItemHeight = 24,
@ -112,6 +120,13 @@ const InternalSelect = <OptionType extends BaseOptionType | DefaultOptionType =
const mergedShowArrow =
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 =====================
const {
status: contextStatus,
@ -144,7 +159,7 @@ const InternalSelect = <OptionType extends BaseOptionType | DefaultOptionType =
const selectProps = omit(props as typeof props & { itemIcon: any }, ['suffixIcon', 'itemIcon']);
const rcSelectRtlDropdownClassName = classNames(
dropdownClassName,
popupClassName || dropdownClassName,
{
[`${prefixCls}-dropdown-${direction}`]: direction === 'rtl',
},

View File

@ -34,7 +34,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg
| defaultOpen | 是否默认展开下拉菜单 | boolean | - | |
| defaultValue | 指定默认选中的条目 | string \| string\[]<br />number \| number\[]<br />LabeledValue \| LabeledValue\[] | - | |
| disabled | 是否禁用 | boolean | false | |
| dropdownClassName | 下拉菜单的 className 属性 | string | - | |
| popupClassName | 下拉菜单的 className 属性 | string | - | 4.23.0 |
| dropdownMatchSelectWidth | 下拉菜单和选择器同宽。默认将设置 `min-width`当值小于选择框宽度时会被忽略。false 时会关闭虚拟滚动 | boolean \| number | true | |
| dropdownRender | 自定义下拉框内容 | (originNode: ReactNode) => ReactNode | - | |
| 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;
};
filters: Record<string, FilterValue | null>;
sorter: SorterResult<RecordType> | SorterResult<RecordType>[];
sorter: SorterResult<RecordType> | SorterResult<RecordType[]>;
filterStates: FilterState<RecordType>[];
sorterStates: SortState<RecordType>[];
@ -94,7 +94,7 @@ export interface TableProps<RecordType>
onChange?: (
pagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<RecordType> | SorterResult<RecordType>[],
sorter: SorterResult<RecordType> | SorterResult<RecordType[]>,
extra: TableCurrentDataSource<RecordType>,
) => void;
rowSelection?: TableRowSelection<RecordType>;
@ -265,7 +265,7 @@ function InternalTable<RecordType extends object = any>(
// ============================ Sorter =============================
const onSorterChange = (
sorter: SorterResult<RecordType> | SorterResult<RecordType>[],
sorter: SorterResult<RecordType> | SorterResult<RecordType[]>,
sorterStates: SortState<RecordType>[],
) => {
triggerOnChange(

View File

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

View File

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

View File

@ -177,6 +177,7 @@ Properties for expandable.
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| 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 | - | |
| defaultExpandAllRows | Expand all rows initially | boolean | false | |
| defaultExpandedRowKeys | Initial expanded row keys | string\[] | - | |

View File

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

View File

@ -64,7 +64,6 @@ const genExpandStyle: GenerateStyle<TableToken, CSSObject> = token => {
background: tableExpandIconBg,
border: tableBorder,
borderRadius: radiusBase,
outline: 'none',
transform: `scale(${checkboxSize / expandIconSize})`,
transition: `all ${motionDurationSlow}`,
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>
`;
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`] = `
<div
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>
`;
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`] = `
<div
class="ant-tabs ant-tabs-top"

View File

@ -3,6 +3,7 @@ import Tabs from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render } from '../../../tests/utils';
import { resetWarned } from '../../_util/warning';
const { TabPane } = Tabs;
@ -105,4 +106,23 @@ describe('Tabs', () => {
);
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 React from 'react';
const { TabPane } = Tabs;
const onChange = (key: string) => {
console.log(key);
};
const App: React.FC = () => (
<Tabs defaultActiveKey="1" onChange={onChange}>
<TabPane tab="Tab 1" key="1">
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>
<Tabs
defaultActiveKey="1"
onChange={onChange}
items={[
{
label: `Tab 1`,
key: '1',
children: `Content of Tab Pane 1`,
},
{
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;

View File

@ -17,27 +17,24 @@ Should be used at the top of container, needs to override styles.
import { Tabs } from 'antd';
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 = () => (
<div className="card-container">
<Tabs type="card">
<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>
<Tabs type="card" items={items} />
</div>
);

View File

@ -17,24 +17,23 @@ Another type of Tabs, which doesn't support vertical mode.
import { Tabs } from 'antd';
import React from 'react';
const { TabPane } = Tabs;
const onChange = (key: string) => {
console.log(key);
};
const App: React.FC = () => (
<Tabs onChange={onChange} type="card">
<TabPane tab="Tab 1" key="1">
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>
<Tabs
onChange={onChange}
type="card"
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}`,
};
})}
/>
);
export default App;

View File

@ -17,20 +17,19 @@ Centered tabs.
import { Tabs } from 'antd';
import React from 'react';
const { TabPane } = Tabs;
const App: React.FC = () => (
<Tabs defaultActiveKey="1" centered>
<TabPane tab="Tab 1" key="1">
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>
<Tabs
defaultActiveKey="1"
centered
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}`,
};
})}
/>
);
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 React, { useRef, useState } from 'react';
const { TabPane } = Tabs;
const defaultPanes = Array.from({ length: 2 }).map((_, index) => {
const defaultPanes = new Array(2).fill(null).map((_, index) => {
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 [activeKey, setActiveKey] = useState(defaultPanes[0].key);
const [panes, setPanes] = useState(defaultPanes);
const [items, setItems] = useState(defaultPanes);
const newTabIndex = useRef(0);
const onChange = (key: string) => {
@ -35,18 +33,18 @@ const App: React.FC = () => {
const add = () => {
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);
};
const remove = (targetKey: string) => {
const targetIndex = panes.findIndex(pane => pane.key === targetKey);
const newPanes = panes.filter(pane => pane.key !== targetKey);
const targetIndex = items.findIndex(pane => pane.key === targetKey);
const newPanes = items.filter(pane => pane.key !== targetKey);
if (newPanes.length && targetKey === activeKey) {
const { key } = newPanes[targetIndex === newPanes.length ? targetIndex - 1 : targetIndex];
setActiveKey(key);
}
setPanes(newPanes);
setItems(newPanes);
};
const onEdit = (targetKey: string, action: 'add' | 'remove') => {
@ -62,13 +60,14 @@ const App: React.FC = () => {
<div style={{ marginBottom: 16 }}>
<Button onClick={add}>ADD</Button>
</div>
<Tabs hideAdd onChange={onChange} activeKey={activeKey} type="editable-card" onEdit={onEdit}>
{panes.map(pane => (
<TabPane tab={pane.title} key={pane.key}>
{pane.content}
</TabPane>
))}
</Tabs>
<Tabs
hideAdd
onChange={onChange}
activeKey={activeKey}
type="editable-card"
onEdit={onEdit}
items={items}
/>
</div>
);
};

View File

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

View File

@ -19,8 +19,6 @@ import { Tabs } from 'antd';
import React from 'react';
import { Sticky, StickyContainer } from 'react-sticky';
const { TabPane } = Tabs;
const renderTabBar: TabsProps['renderTabBar'] = (props, DefaultTabBar) => (
<Sticky bottomOffset={80}>
{({ style }) => (
@ -29,19 +27,19 @@ const renderTabBar: TabsProps['renderTabBar'] = (props, DefaultTabBar) => (
</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 = () => (
<StickyContainer>
<Tabs defaultActiveKey="1" renderTabBar={renderTabBar}>
<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>
<Tabs defaultActiveKey="1" renderTabBar={renderTabBar} items={items} />
</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 React from 'react';
const { TabPane } = Tabs;
const App: React.FC = () => (
<Tabs defaultActiveKey="1">
<TabPane tab="Tab 1" key="1">
Tab 1
</TabPane>
<TabPane tab="Tab 2" disabled key="2">
Tab 2
</TabPane>
<TabPane tab="Tab 3" key="3">
Tab 3
</TabPane>
</Tabs>
<Tabs
defaultActiveKey="1"
items={[
{
label: 'Tab 1',
key: '1',
children: 'Tab 1',
},
{
label: 'Tab 2',
key: '2',
children: 'Tab 2',
disabled: true,
},
{
label: 'Tab 3',
key: '3',
children: 'Tab 3',
},
]}
/>
);
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 React, { useRef, useState } from 'react';
const { TabPane } = Tabs;
const initialPanes = [
{ title: 'Tab 1', content: 'Content of Tab 1', key: '1' },
{ title: 'Tab 2', content: 'Content of Tab 2', key: '2' },
const initialItems = [
{ label: 'Tab 1', children: 'Content of Tab 1', key: '1' },
{ label: 'Tab 2', children: 'Content of Tab 2', key: '2' },
{
title: 'Tab 3',
content: 'Content of Tab 3',
label: 'Tab 3',
children: 'Content of Tab 3',
key: '3',
closable: false,
},
];
const App: React.FC = () => {
const [activeKey, setActiveKey] = useState(initialPanes[0].key);
const [panes, setPanes] = useState(initialPanes);
const [activeKey, setActiveKey] = useState(initialItems[0].key);
const [items, setItems] = useState(initialItems);
const newTabIndex = useRef(0);
const onChange = (newActiveKey: string) => {
@ -41,21 +39,21 @@ const App: React.FC = () => {
const add = () => {
const newActiveKey = `newTab${newTabIndex.current++}`;
const newPanes = [...panes];
newPanes.push({ title: 'New Tab', content: 'Content of new Tab', key: newActiveKey });
setPanes(newPanes);
const newPanes = [...items];
newPanes.push({ label: 'New Tab', children: 'Content of new Tab', key: newActiveKey });
setItems(newPanes);
setActiveKey(newActiveKey);
};
const remove = (targetKey: string) => {
let newActiveKey = activeKey;
let lastIndex = -1;
panes.forEach((pane, i) => {
if (pane.key === targetKey) {
items.forEach((item, i) => {
if (item.key === targetKey) {
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 (lastIndex >= 0) {
newActiveKey = newPanes[lastIndex].key;
@ -63,7 +61,7 @@ const App: React.FC = () => {
newActiveKey = newPanes[0].key;
}
}
setPanes(newPanes);
setItems(newPanes);
setActiveKey(newActiveKey);
};
@ -76,13 +74,13 @@ const App: React.FC = () => {
};
return (
<Tabs type="editable-card" onChange={onChange} activeKey={activeKey} onEdit={onEdit}>
{panes.map(pane => (
<TabPane tab={pane.title} key={pane.key} closable={pane.closable}>
{pane.content}
</TabPane>
))}
</Tabs>
<Tabs
type="editable-card"
onChange={onChange}
activeKey={activeKey}
onEdit={onEdit}
items={items}
/>
);
};

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 React, { useMemo, useState } from 'react';
const { TabPane } = Tabs;
const CheckboxGroup = Checkbox.Group;
const operations = <Button>Extra Action</Button>;
@ -32,6 +30,15 @@ const options = ['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 [position, setPosition] = useState<PositionType[]>(['left', 'right']);
@ -46,17 +53,7 @@ const App: React.FC = () => {
return (
<>
<Tabs tabBarExtraContent={operations}>
<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>
<Tabs tabBarExtraContent={operations} items={items} />
<br />
<br />
<br />
@ -71,17 +68,7 @@ const App: React.FC = () => {
/>
<br />
<br />
<Tabs tabBarExtraContent={slot}>
<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>
<Tabs tabBarExtraContent={slot} items={items} />
</>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,32 @@ Ant Design 依次提供了三级选项卡,分别用于不同的场景。
- 既可用于容器顶部,也可用于容器内部,是最通用的 Tabs。
- [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
### Tabs
@ -31,6 +57,7 @@ Ant Design 依次提供了三级选项卡,分别用于不同的场景。
| centered | 标签居中展示 | boolean | false | 4.4.0 |
| defaultActiveKey | 初始化选中面板的 key如果没有设置 activeKey | string | `第一个面板` | |
| hideAdd | 是否隐藏加号图标,在 `type="editable-card"` 时有效 | boolean | false | |
| items | 配置选项卡内容 | [TabItem](#TabItem) | [] | 4.23.0 |
| moreIcon | 自定义折叠 icon | ReactNode | &lt;EllipsisOutlined /> | 4.14.0 |
| popupClassName | 更多菜单的 `className` | string | - | 4.21.0 |
| 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)
### Tabs.TabPane
### TabItem
| 参数 | 说明 | 类型 | 默认值 |
| ----------- | ----------------------------------------------- | --------- | ------ |
@ -56,6 +83,5 @@ Ant Design 依次提供了三级选项卡,分别用于不同的场景。
| disabled | 禁用某一项 | boolean | false |
| forceRender | 被隐藏时是否渲染 DOM 结构 | boolean | false |
| key | 对应 activeKey | string | - |
| tab | 选项卡头显示文字 | ReactNode | - |
> 更多属性查看 [rc-tabs tabpane](https://github.com/react-component/tabs#tabpane)
| label | 选项卡头显示文字 | ReactNode | - |
| children | 选项卡头显示内容 | ReactNode | - |

View File

@ -87,6 +87,20 @@ describe('TimePicker', () => {
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', () => {
const wrapper = mount(
<TimePicker

View File

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

View File

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

View File

@ -50,4 +50,13 @@ describe('TreeSelect', () => {
const wrapper = mount(<TreeSelect treeIcon open notFoundContent="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 | |
| defaultValue | To set the initial selected treeNode(s) | string \| string\[] | - | |
| 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 | |
| dropdownRender | Customize dropdown content | (originNode: ReactNode, props) => ReactNode | - | |
| dropdownStyle | To set the style of the dropdown menu | CSSProperties | - | |

View File

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

View File

@ -23,7 +23,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Ax4DA0njr/TreeSelect.svg
| bordered | 是否显示边框 | boolean | true | |
| defaultValue | 指定默认选中的条目 | string \| string\[] | - | |
| disabled | 是否禁用 | boolean | false | |
| dropdownClassName | 下拉菜单的 className 属性 | string | - | |
| popupClassName | 下拉菜单的 className 属性 | string | - | 4.23.0 |
| dropdownMatchSelectWidth | 下拉菜单和选择器同宽。默认将设置 `min-width`当值小于选择框宽度时会被忽略。false 时会关闭虚拟滚动 | boolean \| number | true | |
| dropdownRender | 自定义下拉框内容 | (originNode: ReactNode, props) => ReactNode | - | |
| 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)
## 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?
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 会透出组件定义,但是随着重构可能导致内部一些定义命名或者属性变化。因而更推荐直接使用 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) 组件来包裹你的应用。

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.
#### 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
```diff

View File

@ -193,6 +193,14 @@ antd4-codemod src
`@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` 包引入
```diff

View File

@ -128,9 +128,9 @@
"rc-dropdown": "~4.0.0",
"rc-field-form": "~1.27.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-mentions": "~1.9.0",
"rc-mentions": "~1.9.1",
"rc-menu": "~9.6.0",
"rc-motion": "^2.6.1",
"rc-notification": "~5.0.0-alpha.9",
@ -144,8 +144,8 @@
"rc-slider": "~10.0.0",
"rc-steps": "~4.1.0",
"rc-switch": "~3.2.0",
"rc-table": "~7.25.3",
"rc-tabs": "~11.16.0",
"rc-table": "~7.26.0",
"rc-tabs": "~12.0.0-alpha.1",
"rc-textarea": "~0.3.0",
"rc-tooltip": "~5.2.0",
"rc-tree": "~5.6.5",

View File

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

View File

@ -497,7 +497,11 @@ ReactDOM.render(<Demo />, document.getElementById('container'));
);
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;

View File

@ -13,60 +13,46 @@ interface CategoryProps {
intl: any;
}
interface CategoryState {
justCopied: string | null;
}
class Category extends React.Component<CategoryProps, CategoryState> {
copyId?: number;
state = {
justCopied: null,
};
componentWillUnmount() {
window.clearTimeout(this.copyId);
}
onCopied = (type: string, text: string) => {
const Category: React.FC<CategoryProps> = props => {
const { icons, title, newIcons, theme, intl } = props;
const [justCopied, setJustCopied] = React.useState<string | null>(null);
const copyId = React.useRef<NodeJS.Timeout | null>(null);
const onCopied = React.useCallback((type: string, text: string) => {
message.success(
<span>
<code className="copied-code">{text}</code> copied 🎉
</span>,
);
this.setState({ justCopied: type }, () => {
this.copyId = window.setTimeout(() => {
this.setState({ justCopied: null });
}, 2000);
});
};
render() {
const {
icons,
title,
newIcons,
theme,
intl: { messages },
} = this.props;
const items = icons.map(name => (
<CopyableIcon
key={name}
name={name}
theme={theme}
isNew={newIcons.indexOf(name) >= 0}
justCopied={this.state.justCopied}
onCopied={this.onCopied}
/>
));
return (
<div>
<h3>{messages[`app.docs.components.icon.category.${title}`]}</h3>
<ul className="anticons-list">{items}</ul>
</div>
);
}
}
setJustCopied(type);
copyId.current = setTimeout(() => {
setJustCopied(null);
}, 2000);
}, []);
React.useEffect(
() => () => {
if (copyId.current) {
clearTimeout(copyId.current);
}
},
[],
);
return (
<div>
<h3>{intl.messages[`app.docs.components.icon.category.${title}`]}</h3>
<ul className="anticons-list">
{icons.map(name => (
<CopyableIcon
key={name}
name={name}
theme={theme}
isNew={newIcons.includes(name)}
justCopied={justCopied}
onCopied={onCopied}
/>
))}
</ul>
</div>
);
};
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 CopyToClipboard from 'react-copy-to-clipboard';
import { injectIntl } from 'react-intl';
import * as AntdIcons from '@ant-design/icons';
const allIcons: {
[key: string]: any;
} = AntdIcons;
const allIcons: { [key: string]: any } = AntdIcons;
const { Dragger } = Upload;
interface AntdIconClassifier {
@ -27,8 +25,8 @@ interface PicSearcherState {
loading: boolean;
modalVisible: boolean;
popoverVisible: boolean;
icons: Array<string>;
fileList: Array<any>;
icons: iconObject[];
fileList: any[];
error: boolean;
modelLoaded: boolean;
}
@ -38,8 +36,9 @@ interface iconObject {
score: number;
}
class PicSearcher extends Component<PicSearcherProps, PicSearcherState> {
state = {
const PicSearcher: React.FC<PicSearcherProps> = ({ intl }) => {
const { messages } = intl;
const [state, setState] = useState<PicSearcherState>({
loading: false,
modalVisible: false,
popoverVisible: false,
@ -47,83 +46,64 @@ class PicSearcher extends Component<PicSearcherProps, PicSearcherState> {
fileList: [],
error: false,
modelLoaded: false,
};
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) => {
});
const predict = (imgEl: HTMLImageElement) => {
try {
let icons = window.antdIconClassifier.predict(imgEl);
if (gtag && icons.length >= 1) {
let icons: any[] = window.antdIconClassifier.predict(imgEl);
if (gtag && icons.length) {
gtag('event', 'icon', {
event_category: 'search-by-image',
event_label: icons[0].className,
});
}
icons = icons.map((i: any) => ({ score: i.score, type: i.className.replace(/\s/g, '-') }));
this.setState(() => ({ icons, loading: false, error: false }));
} catch (err) {
this.setState(() => ({ loading: false, error: true }));
icons = icons.map(i => ({ score: i.score, type: i.className.replace(/\s/g, '-') }));
setState(prev => ({ ...prev, loading: false, error: false, icons }));
} catch {
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 = () => {
this.setState(prev => ({
const uploadFile = useCallback((file: File) => {
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,
popoverVisible: false,
fileList: [],
@ -132,120 +112,128 @@ class PicSearcher extends Component<PicSearcherProps, PicSearcherState> {
if (!localStorage.getItem('disableIconTip')) {
localStorage.setItem('disableIconTip', 'true');
}
};
}, []);
// eslint-disable-next-line class-methods-use-this
onCopied = (text: string) => {
const onCopied = useCallback((text: string) => {
message.success(
<span>
<code className="copied-code">{text}</code> copied 🎉
</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() {
const {
intl: { messages },
} = this.props;
const { modalVisible, popoverVisible, icons, fileList, loading, modelLoaded, error } =
this.state;
return (
<div className="icon-pic-searcher">
<Popover
content={messages[`app.docs.components.icon.pic-searcher.intro`]}
visible={popoverVisible}
>
<AntdIcons.CameraOutlined className="icon-pic-btn" onClick={this.toggleModal} />
</Popover>
<Modal
title={messages[`app.docs.components.icon.pic-searcher.title`]}
visible={modalVisible}
onCancel={this.toggleModal}
footer={null}
>
{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>
return (
<div className="icon-pic-searcher">
<Popover
content={messages[`app.docs.components.icon.pic-searcher.intro`]}
visible={state.popoverVisible}
>
<AntdIcons.CameraOutlined className="icon-pic-btn" onClick={toggleModal} />
</Popover>
<Modal
title={messages[`app.docs.components.icon.pic-searcher.title`]}
visible={state.modalVisible}
onCancel={toggleModal}
footer={null}
>
{state.modelLoaded || (
<Spin
spinning={!state.modelLoaded}
tip={messages['app.docs.components.icon.pic-searcher.modelloading']}
>
<div style={{ height: 100 }} />
</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);

View File

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

View File

@ -1,8 +1,6 @@
import React from 'react';
import { message } from 'antd';
import React, { useMemo } from 'react';
import RcFooter from 'rc-footer';
import { Link } from 'bisheng/router';
import { presetPalettes } from '@ant-design/colors';
import type { WrappedComponentProps } from 'react-intl';
import { FormattedMessage, injectIntl } from 'react-intl';
import {
@ -19,21 +17,13 @@ import {
QuestionCircleOutlined,
BgColorsOutlined,
} from '@ant-design/icons';
import ColorPicker from '../Color/ColorPicker';
import { loadScript, getLocalizedPathname } from '../utils';
class Footer extends React.Component<WrappedComponentProps & { location: any }> {
lessLoaded = false;
state = {
color: presetPalettes.blue.primary,
};
getColumns() {
const { intl, location } = this.props;
import type { FooterColumn } from 'rc-footer/lib/column';
import { getLocalizedPathname } from '../utils';
const Footer: React.FC<WrappedComponentProps & { location: any }> = props => {
const { intl, location } = props;
const getColumns = useMemo<FooterColumn[]>(() => {
const isZhCN = intl.locale === 'zh-CN';
const getLinkHash = (path: string, hash: { zhCN: string; enUS: string }) => {
const pathName = getLocalizedPathname(path, isZhCN, location.query, hash);
const { pathname, query = {} } = pathName;
@ -182,7 +172,7 @@ class Footer extends React.Component<WrappedComponentProps & { location: any }>
enUS: 'JoinUs',
}),
LinkComponent: Link,
} as any);
} as unknown as typeof col2['items'][number]);
}
const col3 = {
@ -318,82 +308,23 @@ class Footer extends React.Component<WrappedComponentProps & { location: any }>
},
],
};
return [col1, col2, col3, col4];
}
}, [intl.locale, location.query]);
handleColorChange = (color: string) => {
const {
intl: { messages },
} = this.props;
message.loading({
content: messages['app.footer.primary-color-changing'] as string,
key: 'change-primary-color',
});
const changeColor = () => {
(window as any).less
.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>
</>
}
/>
);
}
}
return (
<RcFooter
columns={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);

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 classNames from 'classnames';
import { Select, Row, Col, Popover, Button, Modal } from 'antd';
import { MenuOutlined } from '@ant-design/icons';
import canUseDom from 'rc-util/lib/Dom/canUseDom';
import type { DirectionType } from 'antd/es/config-provider';
import * as utils from '../../utils';
import packageJson from '../../../../../package.json';
import Logo from './Logo';
@ -26,13 +28,11 @@ const { Option } = Select;
const antdVersion: string = packageJson.version;
export interface HeaderProps {
intl: {
locale: string;
};
intl: { locale: string };
location: { pathname: string; query: any };
router: any;
themeConfig: { docVersions: Record<string, string> };
changeDirection: (direction: string) => void;
changeDirection: (direction: DirectionType) => void;
}
let docsearch: any;
@ -61,7 +61,7 @@ function initDocSearch({ isZhCN, router }: { isZhCN: boolean; router: any }) {
transformData: AlgoliaConfig.transformData,
debug: AlgoliaConfig.debug,
// 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);
setTimeout(() => {
input.setVal('');
@ -88,37 +88,44 @@ interface HeaderState {
showTechUIButton: boolean;
}
class Header extends React.Component<HeaderProps, HeaderState> {
static contextType = SiteContext;
pingTimer: NodeJS.Timeout;
state = {
const Header: React.FC<HeaderProps & WrappedComponentProps<'intl'>> = props => {
const { intl, router, location, themeConfig, changeDirection } = props;
const [headerState, setHeaderState] = useState<HeaderState>({
menuVisible: false,
windowWidth: 1400,
searching: 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() {
const { intl, router } = this.props;
router.listen(this.handleHideMenu);
initDocSearch({
isZhCN: intl.locale === 'zh-CN',
router,
});
window.addEventListener('resize', this.onWindowResize);
this.onWindowResize();
this.pingTimer = ping(status => {
useEffect(() => {
router.listen(handleHideMenu);
initDocSearch({ isZhCN: intl.locale === 'zh-CN', router });
onWindowResize();
window.addEventListener('resize', onWindowResize);
pingTimer.current = ping(status => {
if (status !== 'timeout' && status !== 'error') {
this.setState({
showTechUIButton: true,
});
setHeaderState(prev => ({ ...prev, showTechUIButton: true }));
if (
process.env.NODE_ENV === 'production' &&
window.location.host !== 'ant-design.antgroup.com' &&
@ -128,86 +135,29 @@ class Header extends React.Component<HeaderProps, HeaderState> {
title: '提示',
content: '内网用户推荐访问国内镜像以获得极速体验~',
okText: '🚀 立刻前往',
onOk: () => {
cancelText: '不再弹出',
closable: true,
onOk() {
window.open('https://ant-design.antgroup.com', '_self');
disableAntdMirrorModal();
},
cancelText: '不再弹出',
onCancel: () => {
onCancel() {
disableAntdMirrorModal();
},
closable: true,
});
}
}
});
}
componentWillUnmount() {
window.removeEventListener('resize', this.onWindowResize);
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,
});
};
return () => {
window.removeEventListener('resize', onWindowResize);
if (pingTimer.current) {
clearTimeout(pingTimer.current);
}
};
}, []);
// eslint-disable-next-line class-methods-use-this
handleVersionChange = (url: string) => {
const handleVersionChange = useCallback((url: string) => {
const currentUrl = window.location.href;
const currentPathname = window.location.pathname;
if (/overview/.test(currentPathname) && /0?[1-39][0-3]?x/.test(url)) {
@ -218,184 +168,168 @@ class Header extends React.Component<HeaderProps, HeaderState> {
return;
}
window.location.href = currentUrl.replace(window.location.origin, url);
};
}, []);
onLangChange = () => {
const {
location: { pathname, query },
} = this.props;
const onLangChange = useCallback(() => {
const { pathname, query } = location;
const currentProtocol = `${window.location.protocol}//`;
const currentHref = window.location.href.slice(currentProtocol.length);
if (utils.isLocalStorageNameSupported()) {
localStorage.setItem('locale', utils.isZhCN(pathname) ? 'en-US' : 'zh-CN');
}
window.location.href =
currentProtocol +
currentHref.replace(
window.location.pathname,
utils.getLocalizedPathname(pathname, !utils.isZhCN(pathname), query).pathname,
);
};
}, [location]);
render() {
return (
<SiteContext.Consumer>
{({ 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 getNextDirectionText = useMemo<string>(
() => (direction !== 'rtl' ? 'RTL' : 'LTR'),
[direction],
);
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 isRTL = direction === 'rtl';
let responsive: null | 'narrow' | 'crowded' = null;
if (windowWidth < RESPONSIVE_XS) {
responsive = 'crowded';
} else if (windowWidth < RESPONSIVE_SM) {
responsive = 'narrow';
}
const pathname = location.pathname.replace(/(^\/|\/$)/g, '');
const headerClassName = classNames({
clearfix: true,
'home-header': isHome,
});
const isHome = ['', 'index', 'index-cn'].includes(pathname);
const sharedProps = {
isZhCN,
isRTL,
};
const isZhCN = intl.locale === 'zh-CN';
const isRTL = direction === 'rtl';
let responsive: null | 'narrow' | 'crowded' = null;
if (windowWidth < RESPONSIVE_XS) {
responsive = 'crowded';
} else if (windowWidth < RESPONSIVE_SM) {
responsive = 'narrow';
}
const navigationNode = (
<Navigation
key="nav"
{...sharedProps}
location={location}
responsive={responsive}
isMobile={isMobile}
showTechUIButton={showTechUIButton}
pathname={pathname}
directionText={this.getNextDirectionText()}
onLangChange={this.onLangChange}
onDirectionChange={this.onDirectionChange}
/>
);
const headerClassName = classNames({
clearfix: true,
'home-header': isHome,
});
let menu: (React.ReactElement | null)[] = [
navigationNode,
<Select
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} />,
];
const sharedProps = {
isZhCN,
isRTL,
};
if (windowWidth < RESPONSIVE_XS) {
menu = searching ? [] : [navigationNode];
} else if (windowWidth < RESPONSIVE_SM) {
menu = searching ? [] : menu;
}
const navigationNode = (
<Navigation
key="nav"
{...sharedProps}
location={location}
responsive={responsive}
isMobile={isMobile}
showTechUIButton={showTechUIButton}
pathname={pathname}
directionText={getNextDirectionText}
onLangChange={onLangChange}
onDirectionChange={onDirectionChange}
/>
);
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,
},
];
let menu: (React.ReactElement | null)[] = [
navigationNode,
<Select
key="version"
className="version"
size="small"
defaultValue={antdVersion}
onChange={handleVersionChange}
dropdownStyle={getDropdownStyle}
getPopupContainer={trigger => trigger.parentNode}
>
{versionOptions}
</Select>,
<Button
size="small"
onClick={onLangChange}
className="header-button header-lang-button"
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 (
<header id="header" className={headerClassName}>
{isMobile && (
<Popover
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>
);
}
}
if (windowWidth < RESPONSIVE_XS) {
menu = searching ? [] : [navigationNode];
} else if (windowWidth < RESPONSIVE_SM) {
menu = searching ? [] : menu;
}
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 type { DirectionType } from 'antd/es/config-provider';
export interface SiteContextProps {
isMobile: boolean;
direction: string;
direction: DirectionType;
}
const SiteContext = React.createContext<SiteContextProps>({