feat(Table): column filterDropdownProps support (#51297)

Co-authored-by: afc163 <afc163@users.noreply.github.com>
This commit is contained in:
𝑾𝒖𝒙𝒉 2024-10-22 17:50:17 +08:00 committed by GitHub
parent 9f77bc576b
commit badbd80eb4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 281 additions and 61 deletions

View File

@ -360,11 +360,15 @@ describe('Table.filter', () => {
const onFilterDropdownOpenChange = jest.fn();
const onFilterDropdownVisibleChange = jest.fn();
const onOpenChange = jest.fn();
const { container } = render(
createTable({
columns: [
{
...column,
filterDropdownProps: {
onOpenChange,
},
onFilterDropdownOpenChange,
onFilterDropdownVisibleChange,
},
@ -372,11 +376,12 @@ describe('Table.filter', () => {
}),
);
fireEvent.click(container.querySelector('.ant-dropdown-trigger')!);
expect(onOpenChange).toHaveBeenCalledWith(true);
expect(onFilterDropdownOpenChange).toHaveBeenCalledWith(true);
expect(onFilterDropdownVisibleChange).toHaveBeenCalledWith(true);
expect(errSpy).toHaveBeenCalledWith(
'Warning: [antd: Table] `onFilterDropdownVisibleChange` is deprecated. Please use `onFilterDropdownOpenChange` instead.',
'Warning: [antd: Table] `onFilterDropdownVisibleChange` is deprecated. Please use `filterDropdownProps.onOpenChange` instead.',
);
errSpy.mockRestore();
@ -3094,4 +3099,53 @@ describe('Table.filter', () => {
expect(container.querySelector('.ant-table-filter-dropdown')!.childNodes).toHaveLength(1);
});
});
// https://github.com/ant-design/ant-design/issues/51151#issuecomment-2419116749
describe('should support filterDropdownProps', () => {
it('dropdownRender', () => {
const dropdownRender = jest.fn((node) => (
<>
{node}
<span>Foo</span>
</>
));
const { container, getByText } = render(
createTable({
columns: [
{
...column,
filterDropdownProps: {
dropdownRender,
},
},
],
}),
);
fireEvent.click(container.querySelector('.ant-dropdown-trigger')!);
expect(dropdownRender).toHaveBeenCalled();
expect(dropdownRender.mock.calls[0][0]).toMatchSnapshot();
expect(getByText('Foo')).toBeTruthy();
});
// https://github.com/ant-design/ant-design/issues/51151
it('placement', () => {
const { container } = render(
createTable({
columns: [
{
...column,
filterDropdownProps: {
placement: 'topLeft',
},
},
],
}),
);
fireEvent.click(container.querySelector('.ant-dropdown-trigger')!);
expect(container.querySelector('.ant-dropdown-placement-topLeft')).toBeTruthy();
});
});
});

View File

@ -838,3 +838,126 @@ exports[`Table.filter renders radio filter correctly 1`] = `
</div>
</div>
`;
exports[`Table.filter should support filterDropdownProps dropdownRender 1`] = `
<FilterDropdownMenuWrapper
className="ant-table-filter-dropdown"
>
<React.Fragment>
<React.Fragment>
<FilterSearch
filterSearch={false}
locale={
{
"cancelSort": "Click to cancel sorting",
"collapse": "Collapse row",
"emptyText": "No data",
"expand": "Expand row",
"filterCheckall": "Select all items",
"filterConfirm": "OK",
"filterEmptyText": "No filters",
"filterReset": "Reset",
"filterSearchPlaceholder": "Search in filters",
"filterTitle": "Filter menu",
"selectAll": "Select current page",
"selectInvert": "Invert current page",
"selectNone": "Clear all data",
"selectionAll": "Select all data",
"sortTitle": "Sort",
"triggerAsc": "Click to sort ascending",
"triggerDesc": "Click to sort descending",
}
}
onChange={[Function]}
tablePrefixCls="ant-table"
value=""
/>
<Menu
className=""
items={
[
{
"key": "boy",
"label": <React.Fragment>
<Checkbox
checked={false}
/>
<span>
Boy
</span>
</React.Fragment>,
},
{
"key": "girl",
"label": <React.Fragment>
<Checkbox
checked={false}
/>
<span>
Girl
</span>
</React.Fragment>,
},
{
"children": [
{
"key": "designer",
"label": <React.Fragment>
<Checkbox
checked={false}
/>
<span>
Designer
</span>
</React.Fragment>,
},
{
"key": "coder",
"label": <React.Fragment>
<Checkbox
checked={false}
/>
<span>
Coder
</span>
</React.Fragment>,
},
],
"key": "title",
"label": "Title",
"popupClassName": "ant-table-filter-dropdown-submenu",
},
]
}
multiple={true}
onDeselect={[Function]}
onOpenChange={[Function]}
onSelect={[Function]}
openKeys={[]}
prefixCls="ant-dropdown-menu"
selectable={true}
selectedKeys={[]}
/>
</React.Fragment>
<div
className="ant-table-filter-dropdown-btns"
>
<Button
disabled={true}
onClick={[Function]}
size="small"
type="link"
>
Reset
</Button>
<Button
onClick={[Function]}
size="small"
type="primary"
>
OK
</Button>
</div>
</React.Fragment>
</FilterDropdownMenuWrapper>
`;

View File

@ -120,10 +120,12 @@ const App: React.FC = () => {
.toString()
.toLowerCase()
.includes((value as string).toLowerCase()),
onFilterDropdownOpenChange: (visible) => {
if (visible) {
setTimeout(() => searchInput.current?.select(), 100);
}
filterDropdownProps: {
onOpenChange(open) {
if (open) {
setTimeout(() => searchInput.current?.select(), 100);
}
},
},
render: (text) =>
searchedColumn === dataIndex ? (

View File

@ -165,14 +165,14 @@ const FilterDropdown = <RecordType extends AnyObject = AnyObject>(
} = props;
const {
filterDropdownOpen,
onFilterDropdownOpenChange,
filterResetToDefaultFilteredValue,
defaultFilteredValue,
filterDropdownProps = {},
// Deprecated
filterDropdownOpen,
filterDropdownVisible,
onFilterDropdownVisibleChange,
onFilterDropdownOpenChange,
} = column;
const [visible, setVisible] = React.useState(false);
@ -182,30 +182,33 @@ const FilterDropdown = <RecordType extends AnyObject = AnyObject>(
);
const triggerVisible = (newVisible: boolean) => {
setVisible(newVisible);
filterDropdownProps.onOpenChange?.(newVisible);
// deprecated
onFilterDropdownOpenChange?.(newVisible);
onFilterDropdownVisibleChange?.(newVisible);
};
// =================Warning===================
if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Table');
[
['filterDropdownVisible', 'filterDropdownOpen', filterDropdownVisible],
[
'onFilterDropdownVisibleChange',
'onFilterDropdownOpenChange',
onFilterDropdownVisibleChange,
],
].forEach(([deprecatedName, newName, prop]) => {
warning.deprecated(
prop === undefined || prop === null,
deprecatedName as string,
newName as string,
);
const deprecatedList: [keyof typeof column, string][] = [
['filterDropdownOpen', 'filterDropdownProps.open'],
['filterDropdownVisible', 'filterDropdownProps.open'],
['onFilterDropdownOpenChange', 'filterDropdownProps.onOpenChange'],
['onFilterDropdownVisibleChange', 'filterDropdownProps.onOpenChange'],
];
deprecatedList.forEach(([deprecatedName, newName]) => {
warning.deprecated(!(deprecatedName in column), deprecatedName, newName);
});
}
const mergedVisible = filterDropdownOpen ?? filterDropdownVisible ?? visible;
const mergedVisible =
filterDropdownProps.open ??
filterDropdownOpen ?? // deprecated
filterDropdownVisible ?? // deprecated
visible; // inner state
// ===================== Select Keys =====================
const propFilteredKeys = filterState?.filteredKeys;
@ -514,46 +517,59 @@ const FilterDropdown = <RecordType extends AnyObject = AnyObject>(
dropdownContent = <OverrideProvider selectable={undefined}>{dropdownContent}</OverrideProvider>;
}
const menu = () => (
dropdownContent = (
<FilterDropdownMenuWrapper className={`${prefixCls}-dropdown`}>
{dropdownContent}
</FilterDropdownMenuWrapper>
);
let filterIcon: React.ReactNode;
if (typeof column.filterIcon === 'function') {
filterIcon = column.filterIcon(filtered);
} else if (column.filterIcon) {
filterIcon = column.filterIcon;
} else {
filterIcon = <FilterFilled />;
}
const getDropdownTrigger = () => {
let filterIcon: React.ReactNode;
if (typeof column.filterIcon === 'function') {
filterIcon = column.filterIcon(filtered);
} else if (column.filterIcon) {
filterIcon = column.filterIcon;
} else {
filterIcon = <FilterFilled />;
}
return (
<span
role="button"
tabIndex={-1}
className={classNames(`${prefixCls}-trigger`, {
active: filtered,
})}
onClick={(e) => {
e.stopPropagation();
}}
>
{filterIcon}
</span>
);
};
const mergedDropdownProps: DropdownProps = {
trigger: ['click'],
placement: direction === 'rtl' ? 'bottomLeft' : 'bottomRight',
children: getDropdownTrigger(),
getPopupContainer,
...filterDropdownProps,
rootClassName: classNames(rootClassName, filterDropdownProps.rootClassName),
open: mergedVisible,
onOpenChange: onVisibleChange,
dropdownRender: () => {
if (typeof filterDropdownProps?.dropdownRender === 'function') {
return filterDropdownProps.dropdownRender(dropdownContent);
}
return dropdownContent;
},
};
return (
<div className={`${prefixCls}-column`}>
<span className={`${tablePrefixCls}-column-title`}>{children}</span>
<Dropdown
dropdownRender={menu}
trigger={['click']}
open={mergedVisible}
onOpenChange={onVisibleChange}
getPopupContainer={getPopupContainer}
placement={direction === 'rtl' ? 'bottomLeft' : 'bottomRight'}
rootClassName={rootClassName}
>
<span
role="button"
tabIndex={-1}
className={classNames(`${prefixCls}-trigger`, {
active: filtered,
})}
onClick={(e) => {
e.stopPropagation();
}}
>
{filterIcon}
</span>
</Dropdown>
<Dropdown {...mergedDropdownProps} />
</div>
);
};

View File

@ -192,7 +192,6 @@ One of the Table `columns` prop for describing the table's columns, Column has t
| defaultSortOrder | Default order of sorted values | `ascend` \| `descend` | - | |
| ellipsis | The ellipsis cell content, not working with sorter and filters for now.<br />tableLayout would be `fixed` when `ellipsis` is `true` or `{ showTitle?: boolean }` | boolean \| {showTitle?: boolean } | false | showTitle: 4.3.0 |
| filterDropdown | Customized filter overlay | ReactNode \| (props: [FilterDropdownProps](https://github.com/ant-design/ant-design/blob/ecc54dda839619e921c0ace530408871f0281c2a/components/table/interface.tsx#L79)) => ReactNode | - | |
| filterDropdownOpen | Whether `filterDropdown` is visible | boolean | - | |
| filtered | Whether the `dataSource` is filtered | boolean | false | |
| filteredValue | Controlled filtered value, filter icon will highlight | string\[] | - | |
| filterIcon | Customized filter icon | ReactNode \| (filtered: boolean) => ReactNode | - | |
@ -201,6 +200,7 @@ One of the Table `columns` prop for describing the table's columns, Column has t
| filterMode | To specify the filter interface | 'menu' \| 'tree' | 'menu' | 4.17.0 |
| filterSearch | Whether to be searchable for filter menu | boolean \| function(input, record):boolean | false | boolean:4.17.0 function:4.19.0 |
| filters | Filter menu config | object\[] | - | |
| filterDropdownProps | Customized dropdown props, `filterDropdownOpen` and `onFilterDropdownOpenChange` were available before `<5.22.0` | [DropdownProps](/components/dropdown#api) | - | 5.22.0 |
| fixed | (IE not support) Set column to be fixed: `true`(same as left) `'left'` `'right'` | boolean \| string | false | |
| key | Unique key of this column, you can ignore this prop if you've set a unique `dataIndex` | string | - | |
| render | Renderer of the table cell. The return value should be a ReactNode | function(text, record, index) {} | - | |
@ -218,7 +218,6 @@ One of the Table `columns` prop for describing the table's columns, Column has t
| hidden | Hidden this column | boolean | false | 5.13.0 |
| onCell | Set props on per cell | function(record, rowIndex) | - | |
| onFilter | Function that determines if the row is displayed when filtered | function(value, record) => boolean | - | |
| onFilterDropdownOpenChange | Callback executed when `filterDropdownOpen` is changed | function(visible) {} | - | |
| onHeaderCell | Set props on per header cell | function(column) | - | |
### ColumnGroup

View File

@ -194,7 +194,6 @@ const columns = [
| defaultSortOrder | 默认排序顺序 | `ascend` \| `descend` | - | |
| ellipsis | 超过宽度将自动省略,暂不支持和排序筛选一起使用。<br />设置为 `true``{ showTitle?: boolean }` 时,表格布局将变成 `tableLayout="fixed"`。 | boolean \| { showTitle?: boolean } | false | showTitle: 4.3.0 |
| filterDropdown | 可以自定义筛选菜单,此函数只负责渲染图层,需要自行编写各种交互 | ReactNode \| (props: [FilterDropdownProps](https://github.com/ant-design/ant-design/blob/ecc54dda839619e921c0ace530408871f0281c2a/components/table/interface.tsx#L79)) => ReactNode | - | |
| filterDropdownOpen | 用于控制自定义筛选菜单是否可见 | boolean | - | |
| filtered | 标识数据是否经过过滤,筛选图标会高亮 | boolean | false | |
| filteredValue | 筛选的受控属性,外界可用此控制列的筛选状态,值为已筛选的 value 数组 | string\[] | - | |
| filterIcon | 自定义 filter 图标。 | ReactNode \| (filtered: boolean) => ReactNode | false | |
@ -203,6 +202,7 @@ const columns = [
| filterMode | 指定筛选菜单的用户界面 | 'menu' \| 'tree' | 'menu' | 4.17.0 |
| filterSearch | 筛选菜单项是否可搜索 | boolean \| function(input, record):boolean | false | boolean:4.17.0 function:4.19.0 |
| filters | 表头的筛选菜单项 | object\[] | - | |
| filterDropdownProps | 自定义下拉属性,在 `<5.22.0` 之前可用 `filterDropdownOpen``onFilterDropdownOpenChange` | [DropdownProps](/components/dropdown#api) | - | 5.22.0 |
| fixed | IE 下无效)列是否固定,可选 `true` (等效于 `left`) `left` `right` | boolean \| string | false | |
| key | React 需要的 key如果已经设置了唯一的 `dataIndex`,可以忽略这个属性 | string | - | |
| render | 生成复杂数据的渲染函数,参数分别为当前行的值,当前行数据,行索引 | function(text, record, index) {} | - | |
@ -220,7 +220,6 @@ const columns = [
| hidden | 隐藏列 | boolean | false | 5.13.0 |
| onCell | 设置单元格属性 | function(record, rowIndex) | - | |
| onFilter | 本地模式下,确定筛选的运行函数 | function | - | |
| onFilterDropdownOpenChange | 自定义筛选菜单可见变化时调用 | function(visible) {} | - | |
| onHeaderCell | 设置头部单元格属性 | function(column) | - | |
### ColumnGroup

View File

@ -11,6 +11,7 @@ import { ExpandableConfig, GetRowKey } from 'rc-table/lib/interface';
import type { Breakpoint } from '../_util/responsiveObserver';
import type { AnyObject } from '../_util/type';
import type { CheckboxProps } from '../checkbox';
import type { DropdownProps } from '../dropdown';
import type { PaginationProps } from '../pagination';
import type { TooltipProps } from '../tooltip';
import type { INTERNAL_SELECTION_ITEM } from './hooks/useSelection';
@ -114,6 +115,19 @@ export interface FilterDropdownProps {
visible: boolean;
}
// 非必要请勿导出
interface CoverableDropdownProps
extends Omit<
DropdownProps,
| 'onOpenChange'
// === deprecated ===
| 'overlay'
| 'visible'
| 'onVisibleChange'
> {
onOpenChange?: (open: boolean) => void;
}
export interface ColumnType<RecordType = AnyObject>
extends Omit<RcColumnType<RecordType>, 'title'> {
title?: ColumnTitle<RecordType>;
@ -144,17 +158,30 @@ export interface ColumnType<RecordType = AnyObject>
filterMode?: 'menu' | 'tree';
filterSearch?: FilterSearchType<ColumnFilterItem>;
onFilter?: (value: React.Key | boolean, record: RecordType) => boolean;
filterDropdownOpen?: boolean;
onFilterDropdownOpenChange?: (visible: boolean) => void;
/**
* Can cover `<Dropdown>` props
* @since 5.22.0
*/
filterDropdownProps?: CoverableDropdownProps;
filterResetToDefaultFilteredValue?: boolean;
// Responsive
responsive?: Breakpoint[];
// Deprecated
/** @deprecated Please use `filterDropdownOpen` instead */
/**
* @deprecated Please use `filterDropdownProps.open` instead.
* @since 4.23.0
*/
filterDropdownOpen?: boolean;
/**
* @deprecated Please use `filterDropdownProps.onOpenChange` instead.
* @since 4.23.0
*/
onFilterDropdownOpenChange?: (visible: boolean) => void;
/** @deprecated Please use `filterDropdownProps.open` instead. */
filterDropdownVisible?: boolean;
/** @deprecated Please use `onFilterDropdownOpenChange` instead */
/** @deprecated Please use `filterDropdownProps.onOpenChange` instead */
onFilterDropdownVisibleChange?: (visible: boolean) => void;
}