chore: merge master to next

This commit is contained in:
afc163 2022-06-24 11:32:42 +08:00
commit a5fe576d48
26 changed files with 5417 additions and 741 deletions

View File

@ -9,8 +9,8 @@ import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled';
import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined'; import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined';
import classNames from 'classnames'; import classNames from 'classnames';
import CSSMotion from 'rc-motion'; import CSSMotion from 'rc-motion';
import type { ReactElement } from 'react';
import * as React from 'react'; import * as React from 'react';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import getDataOrAriaProps from '../_util/getDataOrAriaProps'; import getDataOrAriaProps from '../_util/getDataOrAriaProps';
import { replaceElement } from '../_util/reactNode'; import { replaceElement } from '../_util/reactNode';
@ -65,6 +65,43 @@ const iconMapOutlined = {
warning: ExclamationCircleOutlined, warning: ExclamationCircleOutlined,
}; };
interface IconNodeProps {
type: AlertProps['type'];
icon: AlertProps['icon'];
prefixCls: AlertProps['prefixCls'];
description: AlertProps['description'];
}
const IconNode: React.FC<IconNodeProps> = props => {
const { description, icon, prefixCls, type } = props;
const iconType = (description ? iconMapOutlined : iconMapFilled)[type!] || null;
if (icon) {
return replaceElement(icon, <span className={`${prefixCls}-icon`}>{icon}</span>, () => ({
className: classNames(`${prefixCls}-icon`, {
[(icon as ReactElement).props.className]: (icon as ReactElement).props.className,
}),
})) as ReactElement;
}
return React.createElement(iconType, { className: `${prefixCls}-icon` });
};
interface CloseIconProps {
isClosable: boolean;
prefixCls: AlertProps['prefixCls'];
closeText: AlertProps['closeText'];
closeIcon: AlertProps['closeIcon'];
handleClose: AlertProps['onClose'];
}
const CloseIcon: React.FC<CloseIconProps> = props => {
const { isClosable, closeText, prefixCls, closeIcon, handleClose } = props;
return isClosable ? (
<button type="button" onClick={handleClose} className={`${prefixCls}-close-icon`} tabIndex={0}>
{closeText ? <span className={`${prefixCls}-close-text`}>{closeText}</span> : closeIcon}
</button>
) : null;
};
interface AlertInterface extends React.FC<AlertProps> { interface AlertInterface extends React.FC<AlertProps> {
ErrorBoundary: typeof ErrorBoundary; ErrorBoundary: typeof ErrorBoundary;
} }
@ -112,32 +149,6 @@ const Alert: AlertInterface = ({
const isClosable = closeText ? true : closable; const isClosable = closeText ? true : closable;
const type = getType(); const type = getType();
const renderIconNode = () => {
const { icon } = props;
// use outline icon in alert with description
const iconType = (description ? iconMapOutlined : iconMapFilled)[type] || null;
if (icon) {
return replaceElement(icon, <span className={`${prefixCls}-icon`}>{icon}</span>, () => ({
className: classNames(`${prefixCls}-icon`, {
[(icon as any).props.className]: (icon as any).props.className,
}),
}));
}
return React.createElement(iconType, { className: `${prefixCls}-icon` });
};
const renderCloseIcon = () =>
isClosable ? (
<button
type="button"
onClick={handleClose}
className={`${prefixCls}-close-icon`}
tabIndex={0}
>
{closeText ? <span className={`${prefixCls}-close-text`}>{closeText}</span> : closeIcon}
</button>
) : null;
// banner 模式默认有 Icon // banner 模式默认有 Icon
const isShowIcon = banner && showIcon === undefined ? true : showIcon; const isShowIcon = banner && showIcon === undefined ? true : showIcon;
@ -179,13 +190,26 @@ const Alert: AlertInterface = ({
role="alert" role="alert"
{...dataOrAriaProps} {...dataOrAriaProps}
> >
{isShowIcon ? renderIconNode() : null} {isShowIcon ? (
<IconNode
description={description}
icon={props.icon}
prefixCls={prefixCls}
type={type}
/>
) : null}
<div className={`${prefixCls}-content`}> <div className={`${prefixCls}-content`}>
{message ? <div className={`${prefixCls}-message`}>{message}</div> : null} {message ? <div className={`${prefixCls}-message`}>{message}</div> : null}
{description ? <div className={`${prefixCls}-description`}>{description}</div> : null} {description ? <div className={`${prefixCls}-description`}>{description}</div> : null}
</div> </div>
{action ? <div className={`${prefixCls}-action`}>{action}</div> : null} {action ? <div className={`${prefixCls}-action`}>{action}</div> : null}
{renderCloseIcon()} <CloseIcon
isClosable={!!isClosable}
closeText={closeText}
prefixCls={prefixCls}
closeIcon={closeIcon}
handleClose={handleClose}
/>
</div> </div>
)} )}
</CSSMotion>, </CSSMotion>,

View File

@ -816,6 +816,7 @@ describe('Menu', () => {
); );
expect(onOpen).not.toHaveBeenCalled(); expect(onOpen).not.toHaveBeenCalled();
expect(onClose).not.toHaveBeenCalled(); expect(onClose).not.toHaveBeenCalled();
errorSpy.mockRestore();
}); });
// https://github.com/ant-design/ant-design/issues/18825 // https://github.com/ant-design/ant-design/issues/18825
@ -1002,4 +1003,13 @@ describe('Menu', () => {
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.render()).toMatchSnapshot();
}); });
it('should not warning deprecated message when items={undefined}', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
mount(<Menu items={undefined} />);
expect(errorSpy).not.toHaveBeenCalledWith(
expect.stringContaining('`children` will be removed in next major version'),
);
errorSpy.mockRestore();
});
}); });

View File

@ -91,7 +91,7 @@ const InternalMenu = forwardRef<MenuRef, InternalMenuProps>((props, ref) => {
); );
warning( warning(
!!items && !children, 'items' in props && !children,
'Menu', 'Menu',
'`children` will be removed in next major version. Please use `items` instead.', '`children` will be removed in next major version. Please use `items` instead.',
); );

View File

@ -66,7 +66,7 @@ const App: React.FC = () => {
Error Error
</Button> </Button>
</Space> </Space>
{/* `contextHolder` should always under the context you want to access */} {/* `contextHolder` should always be placed under the context you want to access */}
{contextHolder} {contextHolder}
{/* Can not access this context since `contextHolder` is not in it */} {/* Can not access this context since `contextHolder` is not in it */}

View File

@ -1,8 +1,8 @@
import { mount } from 'enzyme';
import React from 'react'; import React from 'react';
import Pagination from '..'; import Pagination from '..';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest'; import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render } from '../../../tests/utils';
import ConfigProvider from '../../config-provider'; import ConfigProvider from '../../config-provider';
import Select from '../../select'; import Select from '../../select';
@ -20,13 +20,15 @@ describe('Pagination', () => {
} }
return originalElement; return originalElement;
}; };
const wrapper = mount(<Pagination defaultCurrent={1} total={50} itemRender={itemRender} />); const { container } = render(
expect(wrapper.find('button').at(0).props().disabled).toBe(true); <Pagination defaultCurrent={1} total={50} itemRender={itemRender} />,
);
expect(container.querySelector('button').disabled).toBe(true);
}); });
it('should autometically be small when size is not specified', async () => { it('should autometically be small when size is not specified', async () => {
const wrapper = mount(<Pagination responsive />); const { container } = render(<Pagination responsive />);
expect(wrapper.find('ul').at(0).hasClass('ant-pagination-mini')).toBe(true); expect(container.querySelector('ul').className.includes('ant-pagination-mini')).toBe(true);
}); });
// https://github.com/ant-design/ant-design/issues/24913 // https://github.com/ant-design/ant-design/issues/24913
@ -34,7 +36,7 @@ describe('Pagination', () => {
it('should onChange called when pageSize change', () => { it('should onChange called when pageSize change', () => {
const onChange = jest.fn(); const onChange = jest.fn();
const onShowSizeChange = jest.fn(); const onShowSizeChange = jest.fn();
const wrapper = mount( const { container } = render(
<Pagination <Pagination
defaultCurrent={1} defaultCurrent={1}
total={500} total={500}
@ -42,9 +44,11 @@ describe('Pagination', () => {
onShowSizeChange={onShowSizeChange} onShowSizeChange={onShowSizeChange}
/>, />,
); );
wrapper.find('.ant-select-selector').simulate('mousedown');
expect(wrapper.find('.ant-select-item-option').length).toBe(4); fireEvent.mouseDown(container.querySelector('.ant-select-selector'));
wrapper.find('.ant-select-item-option').at(1).simulate('click');
expect(container.querySelectorAll('.ant-select-item-option').length).toBe(4);
fireEvent.click(container.querySelectorAll('.ant-select-item-option')[1]);
expect(onChange).toHaveBeenCalledWith(1, 20); expect(onChange).toHaveBeenCalledWith(1, 20);
}); });
@ -55,30 +59,30 @@ describe('Pagination', () => {
CustomSelect.Option = Select.Option; CustomSelect.Option = Select.Option;
const wrapper = mount( const { container } = render(
<Pagination defaultCurrent={1} total={500} selectComponentClass={CustomSelect} />, <Pagination defaultCurrent={1} total={500} selectComponentClass={CustomSelect} />,
); );
expect(wrapper.find('.custom-select').length).toBeTruthy(); expect(container.querySelectorAll('.custom-select').length).toBeTruthy();
}); });
describe('ConfigProvider', () => { describe('ConfigProvider', () => {
it('should be rendered correctly in RTL', () => { it('should be rendered correctly in RTL', () => {
const wrapper = mount( const { asFragment } = render(
<ConfigProvider direction="rtl"> <ConfigProvider direction="rtl">
<Pagination defaultCurrent={1} total={50} /> <Pagination defaultCurrent={1} total={50} />
</ConfigProvider>, </ConfigProvider>,
); );
expect(wrapper.render()).toMatchSnapshot(); expect(asFragment().firstChild).toMatchSnapshot();
}); });
it('should be rendered correctly when componentSize is large', () => { it('should be rendered correctly when componentSize is large', () => {
const wrapper = mount( const { container, asFragment } = render(
<ConfigProvider componentSize="large"> <ConfigProvider componentSize="large">
<Pagination defaultCurrent={1} total={50} showSizeChanger /> <Pagination defaultCurrent={1} total={50} showSizeChanger />
</ConfigProvider>, </ConfigProvider>,
); );
expect(wrapper.render()).toMatchSnapshot(); expect(asFragment().firstChild).toMatchSnapshot();
expect(wrapper.find('.ant-select-lg').length).toBe(0); expect(container.querySelectorAll('.ant-select-lg').length).toBe(0);
}); });
}); });
}); });

View File

@ -292,6 +292,79 @@ describe('Table.rowSelection', () => {
]); ]);
}); });
it('reset last select key after performing select and bulk operations', async () => {
jest.useFakeTimers();
const onChange = jest.fn();
const { container, baseElement } = render(
createTable({
checkbox: true,
rowSelection: {
selections: [Table.SELECTION_NONE],
onChange: keys => onChange(keys),
},
}),
);
const last = () => {
const elements = container.querySelectorAll('td input');
return elements[elements.length - 1];
};
const first = () => {
const elements = container.querySelectorAll('td input');
return elements[0];
};
const allElement = () => container.querySelector('th input');
// Multiple select normal
fireEvent.click(last());
expect(onChange).toHaveBeenLastCalledWith([3]);
fireEvent.click(first(), {
shiftKey: true,
});
expect(onChange).toHaveBeenLastCalledWith([3, 0, 1, 2]);
fireEvent.click(allElement());
console.log(baseElement.innerHTML);
expect(onChange).toHaveBeenLastCalledWith([]);
// Reset last select key when select all
fireEvent.click(last());
expect(onChange).toHaveBeenLastCalledWith([3]);
fireEvent.click(allElement());
fireEvent.click(allElement());
expect(onChange).toHaveBeenLastCalledWith([]);
fireEvent.click(first(), {
shiftKey: true,
});
expect(onChange).toHaveBeenLastCalledWith([0]);
// Reset last select key when deselect
fireEvent.click(last());
expect(onChange).toHaveBeenLastCalledWith([0, 3]);
fireEvent.click(first());
expect(onChange).toHaveBeenLastCalledWith([3]);
fireEvent.click(first(), {
shiftKey: true,
});
expect(onChange).toHaveBeenLastCalledWith([3, 0]);
// Reset last select key when bulk operations
fireEvent.mouseEnter(container.querySelector('.ant-dropdown-trigger'));
act(() => {
jest.runAllTimers();
});
fireEvent.click(baseElement.querySelector('li.ant-dropdown-menu-item'));
expect(onChange).toHaveBeenLastCalledWith([]);
fireEvent.click(first(), {
shiftKey: true,
});
expect(onChange).toHaveBeenLastCalledWith([0]);
jest.useRealTimers();
});
it('fires selectAll event', () => { it('fires selectAll event', () => {
const order = []; const order = [];
const handleSelectAll = jest.fn().mockImplementation(() => { const handleSelectAll = jest.fn().mockImplementation(() => {

File diff suppressed because it is too large Load Diff

View File

@ -16,8 +16,8 @@ To see if bordered style applied to other tables.
```tsx ```tsx
import { DownOutlined } from '@ant-design/icons'; import { DownOutlined } from '@ant-design/icons';
import type { TableColumnsType } from 'antd';
import { Badge, Dropdown, Form, Menu, Space, Switch, Table } from 'antd'; import { Badge, Dropdown, Form, Menu, Space, Switch, Table } from 'antd';
import type { ColumnsType } from 'antd/lib/table';
import React, { useState } from 'react'; import React, { useState } from 'react';
interface DataType { interface DataType {
@ -93,7 +93,7 @@ const App: React.FC = () => {
return <Table columns={columns} dataSource={data} pagination={false} bordered={bordered} />; return <Table columns={columns} dataSource={data} pagination={false} bordered={bordered} />;
}; };
const columns: ColumnsType<DataType> = [ const columns: TableColumnsType<DataType> = [
{ title: 'Name', dataIndex: 'name', key: 'name' }, { title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Platform', dataIndex: 'platform', key: 'platform' }, { title: 'Platform', dataIndex: 'platform', key: 'platform' },
{ title: 'Version', dataIndex: 'version', key: 'version' }, { title: 'Version', dataIndex: 'version', key: 'version' },
@ -135,7 +135,6 @@ const App: React.FC = () => {
<Table <Table
title={() => 'cool'} title={() => 'cool'}
footer={() => 'cool'} footer={() => 'cool'}
className="components-table-demo-nested"
columns={columns} columns={columns}
expandable={{ expandedRowRender: createExpandedRowRender(childTableBordered) }} expandable={{ expandedRowRender: createExpandedRowRender(childTableBordered) }}
dataSource={data} dataSource={data}

View File

@ -15,8 +15,8 @@ Showing more detailed info of every row.
```tsx ```tsx
import { DownOutlined } from '@ant-design/icons'; import { DownOutlined } from '@ant-design/icons';
import type { TableColumnsType } from 'antd';
import { Badge, Dropdown, Menu, Space, Table } from 'antd'; import { Badge, Dropdown, Menu, Space, Table } from 'antd';
import type { ColumnsType } from 'antd/lib/table';
import React from 'react'; import React from 'react';
interface DataType { interface DataType {
@ -47,7 +47,7 @@ const menu = (
const App: React.FC = () => { const App: React.FC = () => {
const expandedRowRender = () => { const expandedRowRender = () => {
const columns: ColumnsType<ExpandedDataType> = [ const columns: TableColumnsType<ExpandedDataType> = [
{ title: 'Date', dataIndex: 'date', key: 'date' }, { title: 'Date', dataIndex: 'date', key: 'date' },
{ title: 'Name', dataIndex: 'name', key: 'name' }, { title: 'Name', dataIndex: 'name', key: 'name' },
{ {
@ -82,7 +82,7 @@ const App: React.FC = () => {
const data = []; const data = [];
for (let i = 0; i < 3; ++i) { for (let i = 0; i < 3; ++i) {
data.push({ data.push({
key: i, key: i.toString(),
date: '2014-12-24 23:12:00', date: '2014-12-24 23:12:00',
name: 'This is production name', name: 'This is production name',
upgradeNum: 'Upgraded: 56', upgradeNum: 'Upgraded: 56',
@ -91,7 +91,7 @@ const App: React.FC = () => {
return <Table columns={columns} dataSource={data} pagination={false} />; return <Table columns={columns} dataSource={data} pagination={false} />;
}; };
const columns: ColumnsType<DataType> = [ const columns: TableColumnsType<DataType> = [
{ title: 'Name', dataIndex: 'name', key: 'name' }, { title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Platform', dataIndex: 'platform', key: 'platform' }, { title: 'Platform', dataIndex: 'platform', key: 'platform' },
{ title: 'Version', dataIndex: 'version', key: 'version' }, { title: 'Version', dataIndex: 'version', key: 'version' },
@ -104,7 +104,7 @@ const App: React.FC = () => {
const data: DataType[] = []; const data: DataType[] = [];
for (let i = 0; i < 3; ++i) { for (let i = 0; i < 3; ++i) {
data.push({ data.push({
key: i, key: i.toString(),
name: 'Screem', name: 'Screem',
platform: 'iOS', platform: 'iOS',
version: '10.3.4.5654', version: '10.3.4.5654',
@ -115,12 +115,25 @@ const App: React.FC = () => {
} }
return ( return (
<Table <>
className="components-table-demo-nested" <Table
columns={columns} columns={columns}
expandable={{ expandedRowRender }} expandable={{ expandedRowRender, defaultExpandedRowKeys: ['0'] }}
dataSource={data} dataSource={data}
/> />
<Table
columns={columns}
expandable={{ expandedRowRender, defaultExpandedRowKeys: ['0'] }}
dataSource={data}
size="middle"
/>
<Table
columns={columns}
expandable={{ expandedRowRender, defaultExpandedRowKeys: ['0'] }}
dataSource={data}
size="small"
/>
</>
); );
}; };

View File

@ -271,75 +271,83 @@ export default function useSelection<RecordType>(
const selectionList: INTERNAL_SELECTION_ITEM[] = const selectionList: INTERNAL_SELECTION_ITEM[] =
selections === true ? [SELECTION_ALL, SELECTION_INVERT, SELECTION_NONE] : selections; selections === true ? [SELECTION_ALL, SELECTION_INVERT, SELECTION_NONE] : selections;
return selectionList.map((selection: INTERNAL_SELECTION_ITEM) => { return selectionList
if (selection === SELECTION_ALL) { .map((selection: INTERNAL_SELECTION_ITEM) => {
return { if (selection === SELECTION_ALL) {
key: 'all', return {
text: tableLocale.selectionAll, key: 'all',
onSelect() { text: tableLocale.selectionAll,
setSelectedKeys( onSelect() {
data setSelectedKeys(
.map((record, index) => getRowKey(record, index)) data
.filter(key => { .map((record, index) => getRowKey(record, index))
const checkProps = checkboxPropsMap.get(key); .filter(key => {
return !checkProps?.disabled || derivedSelectedKeySet.has(key); const checkProps = checkboxPropsMap.get(key);
}), return !checkProps?.disabled || derivedSelectedKeySet.has(key);
'all', }),
); 'all',
},
};
}
if (selection === SELECTION_INVERT) {
return {
key: 'invert',
text: tableLocale.selectInvert,
onSelect() {
const keySet = new Set(derivedSelectedKeySet);
pageData.forEach((record, index) => {
const key = getRowKey(record, index);
const checkProps = checkboxPropsMap.get(key);
if (!checkProps?.disabled) {
if (keySet.has(key)) {
keySet.delete(key);
} else {
keySet.add(key);
}
}
});
const keys = Array.from(keySet);
if (onSelectInvert) {
warning(
false,
'Table',
'`onSelectInvert` will be removed in future. Please use `onChange` instead.',
); );
onSelectInvert(keys); },
} };
}
setSelectedKeys(keys, 'invert'); if (selection === SELECTION_INVERT) {
}, return {
}; key: 'invert',
} text: tableLocale.selectInvert,
if (selection === SELECTION_NONE) { onSelect() {
return { const keySet = new Set(derivedSelectedKeySet);
key: 'none', pageData.forEach((record, index) => {
text: tableLocale.selectNone, const key = getRowKey(record, index);
onSelect() {
onSelectNone?.();
setSelectedKeys(
Array.from(derivedSelectedKeySet).filter(key => {
const checkProps = checkboxPropsMap.get(key); const checkProps = checkboxPropsMap.get(key);
return checkProps?.disabled;
}), if (!checkProps?.disabled) {
'none', if (keySet.has(key)) {
); keySet.delete(key);
}, } else {
}; keySet.add(key);
} }
return selection as SelectionItem; }
}); });
const keys = Array.from(keySet);
if (onSelectInvert) {
warning(
false,
'Table',
'`onSelectInvert` will be removed in future. Please use `onChange` instead.',
);
onSelectInvert(keys);
}
setSelectedKeys(keys, 'invert');
},
};
}
if (selection === SELECTION_NONE) {
return {
key: 'none',
text: tableLocale.selectNone,
onSelect() {
onSelectNone?.();
setSelectedKeys(
Array.from(derivedSelectedKeySet).filter(key => {
const checkProps = checkboxPropsMap.get(key);
return checkProps?.disabled;
}),
'none',
);
},
};
}
return selection as SelectionItem;
})
.map(selection => ({
...selection,
onSelect: (...rest) => {
selection.onSelect?.(...rest);
setLastSelectedKey(null);
},
}));
}, [selections, derivedSelectedKeySet, pageData, getRowKey, onSelectInvert, setSelectedKeys]); }, [selections, derivedSelectedKeySet, pageData, getRowKey, onSelectInvert, setSelectedKeys]);
// ======================= Columns ======================== // ======================= Columns ========================
@ -393,6 +401,7 @@ export default function useSelection<RecordType>(
); );
setSelectedKeys(keys, 'all'); setSelectedKeys(keys, 'all');
setLastSelectedKey(null);
}; };
// ===================== Render ===================== // ===================== Render =====================
@ -605,7 +614,11 @@ export default function useSelection<RecordType>(
} }
} }
setLastSelectedKey(key); if (checked) {
setLastSelectedKey(null);
} else {
setLastSelectedKey(key);
}
}} }}
/> />
), ),

View File

@ -10,12 +10,12 @@ const genExpandStyle: GenerateStyle<TableToken, CSSObject> = token => {
controlInteractiveSize: checkboxSize, controlInteractiveSize: checkboxSize,
motionDurationSlow, motionDurationSlow,
controlLineWidth, controlLineWidth,
padding,
paddingXXS, paddingXXS,
paddingXS, paddingXS,
controlLineType, controlLineType,
tableBorderColor, tableBorderColor,
tableExpandIconBg, tableExpandIconBg,
tableExpandColumnWidth,
radiusBase, radiusBase,
tablePaddingVertical, tablePaddingVertical,
tablePaddingHorizontal, tablePaddingHorizontal,
@ -30,7 +30,7 @@ const genExpandStyle: GenerateStyle<TableToken, CSSObject> = token => {
return { return {
[`${componentCls}-wrapper`]: { [`${componentCls}-wrapper`]: {
[`${componentCls}-expand-icon-col`]: { [`${componentCls}-expand-icon-col`]: {
width: checkboxSize + padding * 2, width: tableExpandColumnWidth,
}, },
[`${componentCls}-row-expand-icon-cell`]: { [`${componentCls}-row-expand-icon-cell`]: {

View File

@ -12,6 +12,7 @@
// @table-sticky-zindex: calc(@zindex-table-fixed + 1); // @table-sticky-zindex: calc(@zindex-table-fixed + 1);
// @table-sticky-scroll-bar-active-bg: fade(@table-sticky-scroll-bar-bg, 80%); // @table-sticky-scroll-bar-active-bg: fade(@table-sticky-scroll-bar-bg, 80%);
// @table-filter-dropdown-max-height: 264px; // @table-filter-dropdown-max-height: 264px;
// @table-expand-column-width: 48px;
// .@{table-prefix-cls}-wrapper { // .@{table-prefix-cls}-wrapper {
// clear: both; // clear: both;
@ -130,8 +131,8 @@
// > .@{table-prefix-cls}-wrapper:only-child, // > .@{table-prefix-cls}-wrapper:only-child,
// > .@{table-prefix-cls}-expanded-row-fixed > .@{table-prefix-cls}-wrapper:only-child { // > .@{table-prefix-cls}-expanded-row-fixed > .@{table-prefix-cls}-wrapper:only-child {
// .@{table-prefix-cls} { // .@{table-prefix-cls} {
// margin: -@table-padding-vertical -@table-padding-horizontal -@table-padding-vertical (@table-padding-horizontal + // margin: -@table-padding-vertical -@table-padding-horizontal -@table-padding-vertical (@table-expand-column-width -
// ceil(@font-size-sm * 1.4)); // @table-padding-horizontal);
// &-tbody > tr:last-child > td { // &-tbody > tr:last-child > td {
// border-bottom: 0; // border-bottom: 0;
@ -476,7 +477,7 @@
// // ========================== Expandable ========================== // // ========================== Expandable ==========================
// &-expand-icon-col { // &-expand-icon-col {
// width: 48px; // width: @table-expand-column-width;
// } // }
// &-row-expand-icon-cell { // &-row-expand-icon-cell {

View File

@ -57,6 +57,7 @@ export interface TableToken extends FullToken<'Table'> {
tableFontSizeSmall: number; tableFontSizeSmall: number;
tableSelectionColumnWidth: number; tableSelectionColumnWidth: number;
tableExpandIconBg: string; tableExpandIconBg: string;
tableExpandColumnWidth: number;
tableExpandedRowBg: string; tableExpandedRowBg: string;
tableFilterDropdownWidth: number; tableFilterDropdownWidth: number;
tableFilterDropdownSearchWidth: number; tableFilterDropdownSearchWidth: number;
@ -181,7 +182,9 @@ const genTableStyle: GenerateStyle<TableToken, CSSObject> = token => {
`]: { `]: {
[componentCls]: { [componentCls]: {
marginBlock: `-${tablePaddingVertical}px`, marginBlock: `-${tablePaddingVertical}px`,
marginInline: `${tablePaddingHorizontal * 2}px -${tablePaddingHorizontal}px`, marginInline: `${
token.tableExpandColumnWidth - tablePaddingHorizontal
}px -${tablePaddingHorizontal}px`,
[`${componentCls}-tbody > tr:last-child > td`]: { [`${componentCls}-tbody > tr:last-child > td`]: {
borderBottom: 0, borderBottom: 0,
'&:first-child, &:last-child': { '&:first-child, &:last-child': {
@ -245,6 +248,7 @@ export default genComponentStyleHook(
radiusBase, radiusBase,
headerHoverBgColor, headerHoverBgColor,
headerSortActiveBgColor, headerSortActiveBgColor,
controlInteractiveSize: checkboxSize,
} = token; } = token;
const baseColorAction = new TinyColor(colorAction); const baseColorAction = new TinyColor(colorAction);
@ -305,6 +309,8 @@ export default genComponentStyleHook(
tableScrollThumbBg: colorTextPlaceholder, tableScrollThumbBg: colorTextPlaceholder,
tableScrollThumbBgHover: colorTextHeading, tableScrollThumbBgHover: colorTextHeading,
tableScrollBg: colorSplit, tableScrollBg: colorSplit,
tableExpandColumnWidth: checkboxSize + 2 * token.padding,
}); });
return [ return [

View File

@ -27,8 +27,8 @@
// // ========================= Nest Table =========================== // // ========================= Nest Table ===========================
// .@{table-prefix-cls}-wrapper:only-child { // .@{table-prefix-cls}-wrapper:only-child {
// .@{table-prefix-cls} { // .@{table-prefix-cls} {
// margin: -@padding-vertical -@padding-horizontal -@padding-vertical (@padding-horizontal + // margin: -@padding-vertical -@padding-horizontal -@padding-vertical (@table-expand-column-width -
// ceil((@font-size-sm * 1.4))); // @padding-horizontal);
// } // }
// } // }
// } // }

View File

@ -35,7 +35,9 @@ const genSizeStyle: GenerateStyle<TableToken, CSSObject> = token => {
// ========================= Nest Table =========================== // ========================= Nest Table ===========================
[`${componentCls}-wrapper:only-child ${componentCls}`]: { [`${componentCls}-wrapper:only-child ${componentCls}`]: {
marginBlock: `-${paddingVertical}px`, marginBlock: `-${paddingVertical}px`,
marginInline: `${paddingHorizontal * 2}px -${paddingHorizontal}px`, marginInline: `${
token.tableExpandColumnWidth - paddingHorizontal
}px -${paddingHorizontal}px`,
}, },
}, },

View File

@ -47,6 +47,7 @@ export interface TreeSelectProps<
| 'getInputElement' | 'getInputElement'
| 'backfill' | 'backfill'
| 'treeLine' | 'treeLine'
| 'switcherIcon'
> { > {
suffixIcon?: React.ReactNode; suffixIcon?: React.ReactNode;
size?: SizeType; size?: SizeType;
@ -55,7 +56,7 @@ export interface TreeSelectProps<
bordered?: boolean; bordered?: boolean;
treeLine?: TreeProps['showLine']; treeLine?: TreeProps['showLine'];
status?: InputStatus; status?: InputStatus;
switcherIcon?: SwitcherIcon; switcherIcon?: SwitcherIcon | RcTreeSelectProps<ValueType, OptionType>['switcherIcon'];
} }
const InternalTreeSelect = <OptionType extends BaseOptionType | DefaultOptionType = BaseOptionType>( const InternalTreeSelect = <OptionType extends BaseOptionType | DefaultOptionType = BaseOptionType>(

View File

@ -104,7 +104,10 @@ interface DraggableConfig {
} }
export interface TreeProps<T extends BasicDataNode = DataNode> export interface TreeProps<T extends BasicDataNode = DataNode>
extends Omit<RcTreeProps<T>, 'prefixCls' | 'showLine' | 'direction' | 'draggable'> { extends Omit<
RcTreeProps<T>,
'prefixCls' | 'showLine' | 'direction' | 'draggable' | 'icon' | 'switcherIcon'
> {
showLine?: boolean | { showLeafIcon: boolean }; showLine?: boolean | { showLeafIcon: boolean };
className?: string; className?: string;
/** 是否支持多选 */ /** 是否支持多选 */
@ -141,8 +144,11 @@ export interface TreeProps<T extends BasicDataNode = DataNode>
draggable?: DraggableFn | boolean | DraggableConfig; draggable?: DraggableFn | boolean | DraggableConfig;
style?: React.CSSProperties; style?: React.CSSProperties;
showIcon?: boolean; showIcon?: boolean;
icon?: ((nodeProps: AntdTreeNodeAttribute) => React.ReactNode) | React.ReactNode; icon?:
switcherIcon?: SwitcherIcon; | ((nodeProps: AntdTreeNodeAttribute) => React.ReactNode)
| React.ReactNode
| RcTreeProps<T>['icon'];
switcherIcon?: SwitcherIcon | RcTreeProps<T>['switcherIcon'];
prefixCls?: string; prefixCls?: string;
children?: React.ReactNode; children?: React.ReactNode;
blockNode?: boolean; blockNode?: boolean;

View File

@ -411,6 +411,8 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
</div> </div>
); );
const uploadButton = renderUploadButton(children ? undefined : { display: 'none' });
if (listType === 'picture-card') { if (listType === 'picture-card') {
return wrapSSR( return wrapSSR(
<span <span
@ -422,14 +424,14 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
hashId, hashId,
)} )}
> >
{renderUploadList(renderUploadButton(), !!children)} {renderUploadList(uploadButton, !!children)}
</span>, </span>,
); );
} }
return wrapSSR( return wrapSSR(
<span className={classNames(`${prefixCls}-wrapper`, rtlCls, className, hashId)}> <span className={classNames(`${prefixCls}-wrapper`, rtlCls, className, hashId)}>
{renderUploadButton(children ? undefined : { display: 'none' })} {uploadButton}
{renderUploadList()} {renderUploadList()}
</span>, </span>,
); );

View File

@ -937,8 +937,11 @@ describe('Upload', () => {
); );
rerender(<Upload listType="picture-card" />); rerender(<Upload listType="picture-card" />);
expect(container.querySelector('.ant-upload-select')).not.toHaveStyle({ expect(container.querySelector('.ant-upload-select-picture-card')).toHaveClass(
display: 'none', 'ant-upload-animate-inline-leave-start',
);
expect(container.querySelector('.ant-upload-select-picture-card')).toHaveStyle({
pointerEvents: 'none',
}); });
// Motion leave status change: start > active // Motion leave status change: start > active
@ -946,11 +949,10 @@ describe('Upload', () => {
jest.runAllTimers(); jest.runAllTimers();
}); });
fireEvent.animationEnd(container.querySelector('.ant-upload-select')); fireEvent.animationEnd(container.querySelector('.ant-upload-select-picture-card'));
expect(container.querySelector('.ant-upload-select-picture-card')).not.toHaveClass(
expect(container.querySelector('.ant-upload-select')).toHaveStyle({ 'ant-upload-animate-inline-leave-start',
display: 'none', );
});
jest.useRealTimers(); jest.useRealTimers();
}); });

View File

@ -1499,4 +1499,43 @@ describe('Upload List', () => {
}); });
unmount(); unmount();
}); });
describe('should not display upload file-select button when listType is picture-card and children is empty', () => {
it('when showUploadList is true', () => {
const list = [
{
uid: '0',
name: 'xxx.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
},
];
const { container: wrapper, unmount } = render(
<Upload fileList={list} listType="picture-card" />,
);
expect(wrapper.querySelectorAll('.ant-upload-select').length).toBe(1);
expect(wrapper.querySelectorAll('.ant-upload-select')[0].style.display).toBe('none');
unmount();
});
// https://github.com/ant-design/ant-design/issues/36183
it('when showUploadList is false', () => {
const list = [
{
uid: '0',
name: 'xxx.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
},
];
const { container: wrapper, unmount } = render(
<Upload fileList={list} showUploadList={false} listType="picture-card" />,
);
expect(wrapper.querySelectorAll('.ant-upload-select').length).toBe(1);
expect(wrapper.querySelectorAll('.ant-upload-select')[0].style.display).toBe('none');
unmount();
});
});
}); });

View File

@ -11,6 +11,16 @@ Here are the frequently asked questions about Ant Design and antd that you shoul
There is currently no plan to add support for Sass/Stylus(etc.) style files, but using tools on Google you can easily convert the provided Less files to your desired style format. There is currently no plan to add support for Sass/Stylus(etc.) style files, but using tools on Google you can easily convert the provided Less files to your desired style format.
## Is there a difference between undefined and null in the controlled components of antd?
**Yes. antd will treats `undefined` as uncontrolled but `null` as controlled components which means empty value of it.**
As input element, React treats both `undefined` and `null` as uncontrolled. When the `value` is converted from a valid value to `undefined` or `null`, the component is no longer controlled, which causes some unexpected cases.
But in antd, `undefined` is treated as uncontrolled, and `null` is used as an explicit empty value of controlled components. To deal with some cases (e.g. `allowClear`) like clearing the `value` when the `value` is non-primitive. If you need a component controlled with the `value` valid, just set the `value` as `null`.
Note: For `options` in `Select-like` components, it is **strongly recommended not** to use `undefined` and `null` as `value` in `option`. Please use `string | number` as a valid `value` in `option`.
## `Select Dropdown DatePicker TimePicker Popover Popconfirm` disappears when I click another popup component inside it. How do I resolve this? ## `Select Dropdown DatePicker TimePicker Popover Popconfirm` disappears when I click another popup component inside it. How do I resolve this?
This is an old bug that has been fixed since `v3.11.x`. If you're using an older version, you can use `<Select getPopupContainer={trigger => trigger.parentElement}>` to render a component inside Popover. (Or other `getXxxxContainer` props) This is an old bug that has been fixed since `v3.11.x`. If you're using an older version, you can use `<Select getPopupContainer={trigger => trigger.parentElement}>` to render a component inside Popover. (Or other `getXxxxContainer` props)

View File

@ -11,6 +11,16 @@ title: FAQ
暂无计划。事实上你可以使用工具(请自行 Google将 Less 转换成 Sass/Stylus 等。 暂无计划。事实上你可以使用工具(请自行 Google将 Less 转换成 Sass/Stylus 等。
## `undefined``null``antd` 的受控组件中有区别吗?
**有。antd 约定:`undefined` 是非受控的标志,`null` 作为显式的受控空值。**
在输入元素中React 认为 `undefined``null` 都属于非受控的标志。当 `value` 由非空值转化为 `undefined``null` 时,组件不再受控,这通常是一些意外情况发生的原因。
但在 antd 中,我们定义 `undefined` 为非受控的标志,而 `null` 则作为显式的受控空值。为的是处理 `value` 为复杂数据类型时的清空(如 `allowClear`)置 `value` 为空值等场景。如果需要让组件受控且希望将 `value` 置为空值,请将 `value` 设置为 `null`
注意:对于类 `Select` 组件的 `options`,我们**强烈不建议**使用 `undefined``null` 作为 `option` 中的 `value`,请使用 `string | number` 作为 `option``value`
## 当我点击 `Select Dropdown DatePicker TimePicker Popover Popconfirm` 内的另一个 popup 组件时它会消失,如何解决? ## 当我点击 `Select Dropdown DatePicker TimePicker Popover Popconfirm` 内的另一个 popup 组件时它会消失,如何解决?
该问题在 `3.11.0` 后已经解决。如果你仍在使用旧版本,你可以通过 `<Select getPopupContainer={trigger => trigger.parentElement}>` 来在 Popover 中渲染组件,或者使用其他的 `getXxxxContainer` 参数。 该问题在 `3.11.0` 后已经解决。如果你仍在使用旧版本,你可以通过 `<Select getPopupContainer={trigger => trigger.parentElement}>` 来在 Popover 中渲染组件,或者使用其他的 `getXxxxContainer` 参数。
@ -85,9 +95,9 @@ antd 内部会对 props 进行浅比较实现性能优化。当状态变更,
- 2.x: https://ant-design-2x.gitee.io/ - 2.x: https://ant-design-2x.gitee.io/
- 1.x: https://ant-design-1x.gitee.io/ - 1.x: https://ant-design-1x.gitee.io/
## `antd` 会像 `React` 那样提供单文件引入吗? ## `antd` 可以像 `React` 那样使用单文件引入吗?
是的[你可以用 script 标签引入](https://ant.design/docs/react/introduce-cn#%E6%B5%8F%E8%A7%88%E5%99%A8%E5%BC%95%E5%85%A5)。但是我们推荐使用 `npm` 来引入 `antd`,这样维护起来更简单方便。 可以[你可以用 script 标签引入](https://ant.design/docs/react/introduce-cn#%E6%B5%8F%E8%A7%88%E5%99%A8%E5%BC%95%E5%85%A5)。但是我们推荐使用 `npm` 来引入 `antd`,这样维护起来更简单方便。
## 在我的网络环境下没法获取到 `icon` 文件。 ## 在我的网络环境下没法获取到 `icon` 文件。

View File

@ -118,7 +118,7 @@
"@ant-design/cssinjs": "^0.0.0-alpha.34", "@ant-design/cssinjs": "^0.0.0-alpha.34",
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^4.7.0",
"@ant-design/react-slick": "~0.29.1", "@ant-design/react-slick": "~0.29.1",
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.18.3",
"@ctrl/tinycolor": "^3.4.0", "@ctrl/tinycolor": "^3.4.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"copy-to-clipboard": "^3.2.0", "copy-to-clipboard": "^3.2.0",
@ -157,14 +157,14 @@
"rc-tree-select": "~5.4.0", "rc-tree-select": "~5.4.0",
"rc-trigger": "^5.2.10", "rc-trigger": "^5.2.10",
"rc-upload": "~4.3.0", "rc-upload": "~4.3.0",
"rc-util": "^5.22.3", "rc-util": "^5.22.5",
"scroll-into-view-if-needed": "^2.2.25", "scroll-into-view-if-needed": "^2.2.25",
"shallowequal": "^1.1.0" "shallowequal": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/bisheng-plugin": "^3.3.0-alpha.4", "@ant-design/bisheng-plugin": "^3.3.0-alpha.4",
"@ant-design/hitu": "^0.0.0-alpha.13", "@ant-design/hitu": "^0.0.0-alpha.13",
"@ant-design/tools": "^15.0.3", "@ant-design/tools": "^15.0.4",
"@docsearch/css": "^3.0.0", "@docsearch/css": "^3.0.0",
"@qixian.cs/github-contributors-list": "^1.0.3", "@qixian.cs/github-contributors-list": "^1.0.3",
"@stackblitz/sdk": "^1.3.0", "@stackblitz/sdk": "^1.3.0",

View File

@ -15,7 +15,7 @@ export function preLoad(list: string[]) {
} }
} }
export function useSiteData<T>(): [T, boolean] { export function useSiteData<T extends object>(): [T, boolean] {
const [data, setData] = React.useState<T>({} as any); const [data, setData] = React.useState<T>({} as any);
const [loading, setLoading] = React.useState<boolean>(false); const [loading, setLoading] = React.useState<boolean>(false);

View File

@ -1,5 +1,5 @@
import flattenDeep from 'lodash/flattenDeep';
import flatten from 'lodash/flatten'; import flatten from 'lodash/flatten';
import flattenDeep from 'lodash/flattenDeep';
import themeConfig from '../../themeConfig'; import themeConfig from '../../themeConfig';
interface Meta { interface Meta {
@ -212,7 +212,7 @@ export function getMetaDescription(jml?: any[] | null) {
.join(''); .join('');
return [tag, content]; return [tag, content];
}), }),
).find(p => p && typeof p === 'string' && !COMMON_TAGS.includes(p)); ).find(p => p && typeof p === 'string' && !COMMON_TAGS.includes(p)) as string;
return paragraph; return paragraph;
} }