fix: Table customize filterDropdown with Menu should not block default selectable (#36098)

* fix: Table customize Menu should be selectable

* test: Add test case

* test: Update snapshow
This commit is contained in:
二货机器人 2022-06-17 14:37:18 +08:00 committed by GitHub
parent f19cf66c88
commit 63fc5055f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 463 additions and 91 deletions

View File

@ -8194,6 +8194,232 @@ exports[`renders ./components/dropdown/demo/placement.md extend context correctl
</div>
`;
exports[`renders ./components/dropdown/demo/selectable.md extend context correctly 1`] = `
Array [
<a
class="ant-typography ant-dropdown-trigger"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
Selectable
</div>
<div
class="ant-space-item"
>
<span
aria-label="down"
class="anticon anticon-down"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</div>
</div>
</a>,
<div>
<div
class="ant-dropdown"
style="opacity:0"
>
<ul
class="ant-dropdown-menu ant-dropdown-menu-root ant-dropdown-menu-vertical ant-dropdown-menu-light"
data-menu-list="true"
role="menu"
tabindex="0"
>
<li
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child"
role="menuitem"
tabindex="-1"
>
<span
class="ant-dropdown-menu-title-content"
>
Item 1
</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 ant-dropdown-menu-item-only-child"
role="menuitem"
tabindex="-1"
>
<span
class="ant-dropdown-menu-title-content"
>
Item 2
</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 ant-dropdown-menu-item-selected ant-dropdown-menu-item-only-child"
role="menuitem"
tabindex="-1"
>
<span
class="ant-dropdown-menu-title-content"
>
Item 3
</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>,
]
`;
exports[`renders ./components/dropdown/demo/sub-menu.md extend context correctly 1`] = `
Array [
<a

View File

@ -858,6 +858,46 @@ exports[`renders ./components/dropdown/demo/placement.md correctly 1`] = `
</div>
`;
exports[`renders ./components/dropdown/demo/selectable.md correctly 1`] = `
<a
class="ant-typography ant-dropdown-trigger"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
Selectable
</div>
<div
class="ant-space-item"
>
<span
aria-label="down"
class="anticon anticon-down"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</div>
</div>
</a>
`;
exports[`renders ./components/dropdown/demo/sub-menu.md correctly 1`] = `
<a
class="ant-dropdown-trigger"

View File

@ -0,0 +1,54 @@
---
order: 10
title:
zh-CN: 菜单可选选择
en-US: Selectable Menu
---
## zh-CN
为 Menu 添加 `selectable` 属性可以开启选择能力。
## en-US
Config Menu `selectable` prop to enable selectable ability.
```tsx
import { DownOutlined } from '@ant-design/icons';
import { Dropdown, Menu, Space, Typography } from 'antd';
import React from 'react';
const menu = (
<Menu
selectable
defaultSelectedKeys={['3']}
items={[
{
key: '1',
label: 'Item 1',
},
{
key: '2',
label: 'Item 2',
},
{
key: '3',
label: 'Item 3',
},
]}
/>
);
const App: React.FC = () => (
<Dropdown overlay={menu}>
<Typography.Link>
<Space>
Selectable
<DownOutlined />
</Space>
</Typography.Link>
</Dropdown>
);
export default App;
```

View File

@ -3,8 +3,7 @@ import classNames from 'classnames';
import RcDropdown from 'rc-dropdown';
import * as React from 'react';
import { ConfigContext } from '../config-provider';
import type { OverrideContextProps } from '../menu/OverrideContext';
import OverrideContext from '../menu/OverrideContext';
import { OverrideProvider } from '../menu/OverrideContext';
import getPlacements from '../_util/placements';
import { cloneElement } from '../_util/reactNode';
import { tuple } from '../_util/type';
@ -148,28 +147,6 @@ const Dropdown: DropdownInterface = props => {
autoAdjustOverflow: true,
});
const overlayContext = React.useMemo<OverrideContextProps>(
() => ({
prefixCls: `${prefixCls}-menu`,
expandIcon: (
<span className={`${prefixCls}-menu-submenu-arrow`}>
<RightOutlined className={`${prefixCls}-menu-submenu-arrow-icon`} />
</span>
),
mode: 'vertical',
selectable: false,
validator: ({ mode }) => {
// Warning if use other mode
warning(
!mode || mode === 'vertical',
'Dropdown',
`mode="${mode}" is not supported for Dropdown's Menu.`,
);
},
}),
[prefixCls],
);
const renderOverlay = () => {
// rc-dropdown already can process the function of overlay, but we have check logic here.
// So we need render the element to check and pass back to rc-dropdown.
@ -186,7 +163,26 @@ const Dropdown: DropdownInterface = props => {
);
return (
<OverrideContext.Provider value={overlayContext}>{overlayNode}</OverrideContext.Provider>
<OverrideProvider
prefixCls={`${prefixCls}-menu`}
expandIcon={
<span className={`${prefixCls}-menu-submenu-arrow`}>
<RightOutlined className={`${prefixCls}-menu-submenu-arrow-icon`} />
</span>
}
mode="vertical"
selectable={false}
validator={({ mode }) => {
// Warning if use other mode
warning(
!mode || mode === 'vertical',
'Dropdown',
`mode="${mode}" is not supported for Dropdown's Menu.`,
);
}}
>
{overlayNode}
</OverrideProvider>
);
};

View File

@ -1,16 +0,0 @@
import * as React from 'react';
import type { MenuProps } from '.';
// Used for Dropdown only
export interface OverrideContextProps {
prefixCls?: string;
expandIcon?: React.ReactNode;
mode?: MenuProps['mode'];
selectable?: boolean;
validator?: (menuProps: Pick<MenuProps, 'mode'>) => void;
}
/** @private Internal Usage. Only used for Dropdown component. Do not use this in your production. */
const OverrideContext = React.createContext<OverrideContextProps | null>(null);
export default OverrideContext;

View File

@ -0,0 +1,41 @@
import * as React from 'react';
import type { MenuProps } from '.';
// Used for Dropdown only
export interface OverrideContextProps {
prefixCls?: string;
expandIcon?: React.ReactNode;
mode?: MenuProps['mode'];
selectable?: boolean;
validator?: (menuProps: Pick<MenuProps, 'mode'>) => void;
}
/** @private Internal Usage. Only used for Dropdown component. Do not use this in your production. */
const OverrideContext = React.createContext<OverrideContextProps | null>(null);
/** @private Internal Usage. Only used for Dropdown component. Do not use this in your production. */
export const OverrideProvider = ({
children,
...restProps
}: OverrideContextProps & { children: React.ReactNode }) => {
const override = React.useContext(OverrideContext);
const context = React.useMemo(
() => ({
...override,
...restProps,
}),
[
override,
restProps.prefixCls,
// restProps.expandIcon, Not mark as deps since this is a ReactNode
restProps.mode,
restProps.selectable,
// restProps.validator, Not mark as deps since this is a function
],
);
return <OverrideContext.Provider value={context}>{children}</OverrideContext.Provider>;
};
export default OverrideContext;

View File

@ -1,13 +1,14 @@
/* eslint-disable react/no-multi-comp */
import React from 'react';
import { act } from 'react-dom/test-utils';
import { render, fireEvent, waitFor } from '../../../tests/utils';
import Table from '..';
import Input from '../../input';
import Tooltip from '../../tooltip';
import { fireEvent, render, waitFor } from '../../../tests/utils';
import Button from '../../button';
import Select from '../../select';
import ConfigProvider from '../../config-provider';
import Input from '../../input';
import Menu from '../../menu';
import Select from '../../select';
import Tooltip from '../../tooltip';
// https://github.com/Semantic-Org/Semantic-UI-React/blob/72c45080e4f20b531fda2e3e430e384083d6766b/test/specs/modules/Dropdown/Dropdown-test.js#L73
const nativeEvent = { nativeEvent: { stopImmediatePropagation: () => {} } };
@ -65,6 +66,14 @@ describe('Table.filter', () => {
return namesList;
}
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('not show filter icon when undefined', () => {
const noFilterColumn = { ...column, filters: undefined };
delete noFilterColumn.onFilter;
@ -106,8 +115,6 @@ describe('Table.filter', () => {
});
it('renders empty menu correctly', () => {
jest.useFakeTimers();
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render(
createTable({
@ -129,8 +136,6 @@ describe('Table.filter', () => {
expect(container.querySelector('.ant-empty')).toBeTruthy();
expect(errorSpy).not.toHaveBeenCalled();
errorSpy.mockRestore();
jest.useRealTimers();
});
it('renders radio filter correctly', async () => {
@ -613,7 +618,6 @@ describe('Table.filter', () => {
onChange,
}),
);
jest.useFakeTimers();
expect(renderedNames(container)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']);
@ -665,8 +669,6 @@ describe('Table.filter', () => {
// What's this? Is that a coverage case? Or check a crash?
const latestItems = getFilterMenu().querySelectorAll('li.ant-dropdown-menu-item');
fireEvent.click(latestItems[latestItems.length - 1]);
jest.useRealTimers();
});
describe('should support value types', () => {
@ -676,7 +678,6 @@ describe('Table.filter', () => {
['Bamboo', false],
].forEach(([text, value]) => {
it(`${typeof value} type`, async () => {
jest.useFakeTimers();
const onChange = jest.fn();
const filters = [{ text, value }];
const { container } = render(
@ -698,8 +699,6 @@ describe('Table.filter', () => {
fireEvent.click(container.querySelector('.ant-dropdown-trigger'));
jest.useFakeTimers();
fireEvent.click(container.querySelectorAll('.ant-dropdown-menu-item')[0]);
// This test can be remove if refactor
@ -733,7 +732,6 @@ describe('Table.filter', () => {
.querySelector('.ant-table-filter-dropdown')
.querySelectorAll('.ant-checkbox-input')[0].checked,
).toEqual(false);
jest.useRealTimers();
});
});
});
@ -1831,7 +1829,6 @@ describe('Table.filter', () => {
describe('filter tree mode', () => {
it('supports filter tree', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render(
createTable({
@ -1852,7 +1849,6 @@ describe('Table.filter', () => {
});
it('supports search input in filter tree', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render(
createTable({
@ -1875,7 +1871,6 @@ describe('Table.filter', () => {
});
it('supports search input in filter menu', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render(
createTable({
@ -1897,7 +1892,6 @@ describe('Table.filter', () => {
});
it('should skip search when filters[0].text is ReactNode', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render(
createTable({
@ -1936,7 +1930,6 @@ describe('Table.filter', () => {
});
it('should supports filterSearch has type of function', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render(
createTable({
@ -1974,7 +1967,6 @@ describe('Table.filter', () => {
});
it('supports check all items', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render(
createTable({
@ -2007,7 +1999,6 @@ describe('Table.filter', () => {
});
it('supports check item by selecting it', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render(
createTable({
@ -2043,7 +2034,6 @@ describe('Table.filter', () => {
});
it('select-all checkbox should change when all items are selected', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render(
createTable({
@ -2075,7 +2065,6 @@ describe('Table.filter', () => {
});
it('filterMultiple is false - check item', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render(
createTable({
@ -2123,7 +2112,6 @@ describe('Table.filter', () => {
});
it('filterMultiple is false - select item', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render(
createTable({
@ -2170,7 +2158,6 @@ describe('Table.filter', () => {
});
it('should select children when select parent', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render(
createTable({
@ -2299,7 +2286,6 @@ describe('Table.filter', () => {
});
it('filterDropdown should support filterResetToDefaultFilteredValue', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined);
const columnFilter = {
@ -2347,6 +2333,44 @@ describe('Table.filter', () => {
expect(container.querySelector('.ant-tree-checkbox-checked+span').textContent).toBe('Girl');
});
it('filterDropdown should not override customize Menu selectable', () => {
const onSelect = jest.fn();
const { container } = render(
createTable({
columns: [
{
...column,
filterDropdown: (
<div className="custom-filter-dropdown">
<Menu
onSelect={onSelect}
items={[
{
key: '1',
label: 'Item 1',
},
]}
/>
</div>
),
},
],
}),
);
// Open Filter
fireEvent.click(container.querySelector('span.ant-dropdown-trigger'));
act(() => {
jest.runAllTimers();
});
// Click Item
fireEvent.click(container.querySelector('.ant-table-filter-dropdown .ant-dropdown-menu-item'));
expect(onSelect).toHaveBeenCalled();
});
it('filteredKeys should all be controlled or not controlled', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
errorSpy.mockReset();

View File

@ -1,32 +1,33 @@
import * as React from 'react';
import FilterFilled from '@ant-design/icons/FilterFilled';
import classNames from 'classnames';
import isEqual from 'lodash/isEqual';
import type { FieldDataNode } from 'rc-tree';
import FilterFilled from '@ant-design/icons/FilterFilled';
import Button from '../../../button';
import Menu from '../../../menu';
import type { MenuProps } from '../../../menu';
import Tree from '../../../tree';
import type { EventDataNode } from '../../../tree';
import Checkbox from '../../../checkbox';
import type { CheckboxChangeEvent } from '../../../checkbox';
import Radio from '../../../radio';
import Dropdown from '../../../dropdown';
import Empty from '../../../empty';
import type {
ColumnType,
ColumnFilterItem,
Key,
TableLocale,
GetPopupContainer,
FilterSearchType,
} from '../../interface';
import FilterDropdownMenuWrapper from './FilterWrapper';
import FilterSearch from './FilterSearch';
import * as React from 'react';
import type { FilterState } from '.';
import { flattenKeys } from '.';
import useSyncState from '../../../_util/hooks/useSyncState';
import Button from '../../../button';
import type { CheckboxChangeEvent } from '../../../checkbox';
import Checkbox from '../../../checkbox';
import { ConfigContext } from '../../../config-provider/context';
import Dropdown from '../../../dropdown';
import Empty from '../../../empty';
import type { MenuProps } from '../../../menu';
import Menu from '../../../menu';
import { OverrideProvider } from '../../../menu/OverrideContext';
import Radio from '../../../radio';
import type { EventDataNode } from '../../../tree';
import Tree from '../../../tree';
import useSyncState from '../../../_util/hooks/useSyncState';
import type {
ColumnFilterItem,
ColumnType,
FilterSearchType,
GetPopupContainer,
Key,
TableLocale,
} from '../../interface';
import FilterSearch from './FilterSearch';
import FilterDropdownMenuWrapper from './FilterWrapper';
type FilterTreeDataNode = FieldDataNode<{ title: React.ReactNode; key: React.Key }>;
@ -303,6 +304,7 @@ function FilterDropdown<RecordType>(props: FilterDropdownProps<RecordType>) {
});
let dropdownContent: React.ReactNode;
if (typeof column.filterDropdown === 'function') {
dropdownContent = column.filterDropdown({
prefixCls: `${dropdownPrefixCls}-custom`,
@ -441,6 +443,11 @@ function FilterDropdown<RecordType>(props: FilterDropdownProps<RecordType>) {
);
}
// We should not block customize Menu with additional props
if (column.filterDropdown) {
dropdownContent = <OverrideProvider selectable={undefined}>{dropdownContent}</OverrideProvider>;
}
const menu = (
<FilterDropdownMenuWrapper className={`${prefixCls}-dropdown`}>
{dropdownContent}