diff --git a/components/dropdown/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/dropdown/__tests__/__snapshots__/demo-extend.test.ts.snap
index 91b94d2bdd..74e30e6b48 100644
--- a/components/dropdown/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/dropdown/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -8194,6 +8194,232 @@ exports[`renders ./components/dropdown/demo/placement.md extend context correctl
`;
+exports[`renders ./components/dropdown/demo/selectable.md extend context correctly 1`] = `
+Array [
+
+
+ ,
+
,
+]
+`;
+
exports[`renders ./components/dropdown/demo/sub-menu.md extend context correctly 1`] = `
Array [
`;
+exports[`renders ./components/dropdown/demo/selectable.md correctly 1`] = `
+
+
+
+`;
+
exports[`renders ./components/dropdown/demo/sub-menu.md correctly 1`] = `
+);
+
+const App: React.FC = () => (
+
+
+
+ Selectable
+
+
+
+
+);
+
+export default App;
+```
diff --git a/components/dropdown/dropdown.tsx b/components/dropdown/dropdown.tsx
index e28a3d67fd..0f4bf4797e 100644
--- a/components/dropdown/dropdown.tsx
+++ b/components/dropdown/dropdown.tsx
@@ -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(
- () => ({
- prefixCls: `${prefixCls}-menu`,
- expandIcon: (
-
-
-
- ),
- 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 (
- {overlayNode}
+
+
+
+ }
+ 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}
+
);
};
diff --git a/components/menu/OverrideContext.ts b/components/menu/OverrideContext.ts
deleted file mode 100644
index c6357adfb4..0000000000
--- a/components/menu/OverrideContext.ts
+++ /dev/null
@@ -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) => void;
-}
-
-/** @private Internal Usage. Only used for Dropdown component. Do not use this in your production. */
-const OverrideContext = React.createContext(null);
-
-export default OverrideContext;
diff --git a/components/menu/OverrideContext.tsx b/components/menu/OverrideContext.tsx
new file mode 100644
index 0000000000..6910f560fb
--- /dev/null
+++ b/components/menu/OverrideContext.tsx
@@ -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) => void;
+}
+
+/** @private Internal Usage. Only used for Dropdown component. Do not use this in your production. */
+const OverrideContext = React.createContext(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 {children};
+};
+
+export default OverrideContext;
diff --git a/components/table/__tests__/Table.filter.test.js b/components/table/__tests__/Table.filter.test.js
index 40555d3053..b92cf89dde 100644
--- a/components/table/__tests__/Table.filter.test.js
+++ b/components/table/__tests__/Table.filter.test.js
@@ -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: (
+
+
+
+ ),
+ },
+ ],
+ }),
+ );
+
+ // 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();
diff --git a/components/table/hooks/useFilter/FilterDropdown.tsx b/components/table/hooks/useFilter/FilterDropdown.tsx
index 6a824f6b0d..8a7ba4fe2a 100644
--- a/components/table/hooks/useFilter/FilterDropdown.tsx
+++ b/components/table/hooks/useFilter/FilterDropdown.tsx
@@ -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 * as React from 'react';
+import type { FilterState } from '.';
+import { flattenKeys } from '.';
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 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 {
- ColumnType,
ColumnFilterItem,
+ ColumnType,
+ FilterSearchType,
+ GetPopupContainer,
Key,
TableLocale,
- GetPopupContainer,
- FilterSearchType,
} from '../../interface';
-import FilterDropdownMenuWrapper from './FilterWrapper';
import FilterSearch from './FilterSearch';
-import type { FilterState } from '.';
-import { flattenKeys } from '.';
-import useSyncState from '../../../_util/hooks/useSyncState';
-import { ConfigContext } from '../../../config-provider/context';
+import FilterDropdownMenuWrapper from './FilterWrapper';
type FilterTreeDataNode = FieldDataNode<{ title: React.ReactNode; key: React.Key }>;
@@ -303,6 +304,7 @@ function FilterDropdown(props: FilterDropdownProps) {
});
let dropdownContent: React.ReactNode;
+
if (typeof column.filterDropdown === 'function') {
dropdownContent = column.filterDropdown({
prefixCls: `${dropdownPrefixCls}-custom`,
@@ -441,6 +443,11 @@ function FilterDropdown(props: FilterDropdownProps) {
);
}
+ // We should not block customize Menu with additional props
+ if (column.filterDropdown) {
+ dropdownContent = {dropdownContent};
+ }
+
const menu = (
{dropdownContent}