feat: add ContextIsolator component to Select (#54544)

* fix: add ContextIsolator component to Select

* feat: add ContextIsolator to mergedPopupRender

* feat: useMemo

* test: add tests

* fix: Delete the 'ContextIsolator' at the top level of the component

* feat: add usePopupRender

* test: change tests

---------

Co-authored-by: 刘欢 <lh01217311@antgroup.com>
This commit is contained in:
EmilyyyLiu 2025-08-01 17:27:43 +08:00 committed by GitHub
parent ff5218840f
commit 953a8c0278
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 147 additions and 6 deletions

View file

@ -1,4 +1,5 @@
import React from 'react';
import { Button, Input, Space } from 'antd';
import type { SingleValueType } from 'rc-cascader/lib/Cascader';
import type { DefaultOptionType } from '..';
@ -803,4 +804,41 @@ describe('Cascader', () => {
errSpy.mockRestore();
});
it('Cascader ContextIsolator', () => {
const { container } = render(
<Space.Compact>
<Cascader
open
style={{ width: 120 }}
popupRender={(menu) => {
return (
<div>
{menu}
<Button>123</Button>
<Input style={{ width: 50 }} />
</div>
);
}}
options={[
{ value: 'jack', label: 'Jack' },
{ value: 'lucy', label: 'Lucy' },
]}
/>
<Button className="test-button">test</Button>
</Space.Compact>,
);
const compactButton = container.querySelector('.test-button');
const popupElement = document.querySelector('.ant-select-dropdown');
// selector should have compact
expect(compactButton).toBeInTheDocument();
expect(compactButton!.className.includes('compact')).toBeTruthy();
// popupRender element haven't compact
expect(popupElement).toBeInTheDocument();
const button = popupElement!.querySelector('button');
const input = popupElement!.querySelector('input');
expect(button!.className.includes('compact')).toBeFalsy();
expect(input!.className.includes('compact')).toBeFalsy();
});
});

View file

@ -31,6 +31,7 @@ import useVariant from '../form/hooks/useVariants';
import mergedBuiltinPlacements from '../select/mergedBuiltinPlacements';
import useSelectStyle from '../select/style';
import useIcons from '../select/useIcons';
import usePopupRender from '../select/usePopupRender';
import useShowArrow from '../select/useShowArrow';
import { useCompactItemContext } from '../space/Compact';
import useBase from './hooks/useBase';
@ -292,7 +293,8 @@ const Cascader = React.forwardRef<CascaderRef, CascaderProps<any>>((props, ref)
cssVarCls,
);
const mergedPopupRender = popupRender || dropdownRender;
const mergedPopupRender = usePopupRender(popupRender || dropdownRender);
const mergedPopupMenuColumnStyle = popupMenuColumnStyle || dropdownMenuColumnStyle;
const mergedOnOpenChange = onOpenChange || onDropdownVisibleChange;
const mergedPopupStyle = styles?.popup?.root || contextStyles.popup?.root || dropdownStyle;

View file

@ -1,14 +1,15 @@
import React from 'react';
import { CloseOutlined } from '@ant-design/icons';
import { Button, Input, Space } from 'antd';
import type { SelectProps } from '..';
import Select from '..';
import Form from '../../form';
import { resetWarned } from '../../_util/warning';
import focusTest from '../../../tests/shared/focusTest';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { act, fireEvent, render } from '../../../tests/utils';
import Form from '../../form';
const { Option } = Select;
@ -290,4 +291,42 @@ describe('Select', () => {
errSpy.mockRestore();
});
});
it('Select ContextIsolator', () => {
const { container } = render(
<Space.Compact>
<Select
open
defaultValue="lucy"
style={{ width: 120 }}
popupRender={(menu) => {
return (
<div>
{menu}
<Button>123</Button>
<Input style={{ width: 50 }} />
</div>
);
}}
options={[
{ value: 'jack', label: 'Jack' },
{ value: 'lucy', label: 'Lucy' },
]}
/>
<Button className="test-button">test</Button>
</Space.Compact>,
);
const compactButton = container.querySelector('.test-button');
const popupElement = document.querySelector('.ant-select-dropdown');
// selector should have compact
expect(compactButton).toBeInTheDocument();
expect(compactButton!.className.includes('compact')).toBeTruthy();
// popupRender element haven't compact
expect(popupElement).toBeInTheDocument();
const button = popupElement!.querySelector('button');
const input = popupElement!.querySelector('input');
expect(button!.className.includes('compact')).toBeFalsy();
expect(input!.className.includes('compact')).toBeFalsy();
});
});

View file

@ -29,6 +29,7 @@ import { useToken } from '../theme/internal';
import mergedBuiltinPlacements from './mergedBuiltinPlacements';
import useStyle from './style';
import useIcons from './useIcons';
import usePopupRender from './usePopupRender';
import useShowArrow from './useShowArrow';
type RawValue = string | number;
@ -203,7 +204,9 @@ const InternalSelect = <
popupMatchSelectWidth ?? dropdownMatchSelectWidth ?? contextPopupMatchSelectWidth;
const mergedPopupStyle = styles?.popup?.root || contextStyles.popup?.root || dropdownStyle;
const mergedPopupRender = popupRender || dropdownRender;
const mergedPopupRender = usePopupRender(popupRender || dropdownRender);
const mergedOnOpenChange = onOpenChange || onDropdownVisibleChange;
// ===================== Form Status =====================

View file

@ -0,0 +1,18 @@
import React from 'react';
import ContextIsolator from '../_util/ContextIsolator';
type RenderFunction<T extends any[]> = (...args: T) => React.ReactNode;
function usePopupRender<T extends [React.ReactElement, ...any[]]>(
renderFn?: RenderFunction<T>,
): ((...args: T) => React.ReactElement) | undefined {
return React.useMemo(() => {
if (!renderFn) {
return undefined;
}
return (...args: T) => <ContextIsolator space>{renderFn(...args)}</ContextIsolator>;
}, [renderFn]);
}
export default usePopupRender;

View file

@ -1,11 +1,12 @@
import React from 'react';
import { Button, Input, Space } from 'antd';
import TreeSelect, { TreeNode } from '..';
import { resetWarned } from '../../_util/warning';
import focusTest from '../../../tests/shared/focusTest';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { render, fireEvent } from '../../../tests/utils';
import { fireEvent, render } from '../../../tests/utils';
describe('TreeSelect', () => {
focusTest(TreeSelect, { refFocus: true });
@ -162,4 +163,41 @@ describe('TreeSelect', () => {
errSpy.mockRestore();
});
it('TreeSelect ContextIsolator', () => {
const { container } = render(
<Space.Compact>
<TreeSelect
open
defaultValue="lucy"
style={{ width: 120 }}
popupRender={(menu) => {
return (
<div>
{menu}
<Button>123</Button>
<Input style={{ width: 50 }} />
</div>
);
}}
treeData={[
{ value: 'jack', title: 'Jack', children: [{ value: 'Emily', title: 'Emily' }] },
{ value: 'lucy', title: 'Lucy' },
]}
/>
<Button className="test-button">test</Button>
</Space.Compact>,
);
const compactButton = container.querySelector('.test-button');
const popupElement = document.querySelector('.ant-select-dropdown');
// selector should have compact
expect(compactButton).toBeInTheDocument();
expect(compactButton!.className.includes('compact')).toBeTruthy();
// popupRender element haven't compact
expect(popupElement).toBeInTheDocument();
const button = popupElement!.querySelector('button');
const input = popupElement!.querySelector('input');
expect(button!.className.includes('compact')).toBeFalsy();
expect(input!.className.includes('compact')).toBeFalsy();
});
});

View file

@ -16,6 +16,7 @@ import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import { devUseWarning } from '../_util/warning';
import { ConfigContext } from '../config-provider';
import type { Variant } from '../config-provider';
import { useComponentConfig } from '../config-provider/context';
import DefaultRenderEmpty from '../config-provider/defaultRenderEmpty';
import DisabledContext from '../config-provider/DisabledContext';
import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
@ -26,6 +27,7 @@ import useVariant from '../form/hooks/useVariants';
import mergedBuiltinPlacements from '../select/mergedBuiltinPlacements';
import useSelectStyle from '../select/style';
import useIcons from '../select/useIcons';
import usePopupRender from '../select/usePopupRender';
import useShowArrow from '../select/useShowArrow';
import { useCompactItemContext } from '../space/Compact';
import { useToken } from '../theme/internal';
@ -33,7 +35,6 @@ import type { AntTreeNodeProps, TreeProps } from '../tree';
import type { SwitcherIcon } from '../tree/Tree';
import SwitcherIconCom from '../tree/utils/iconUtil';
import useStyle from './style';
import { useComponentConfig } from '../config-provider/context';
type RawValue = string | number;
@ -226,7 +227,9 @@ const InternalTreeSelect = <ValueType = any, OptionType extends DataNode = DataN
);
const mergedPopupStyle = styles?.popup?.root || contextStyles?.popup?.root || dropdownStyle;
const mergedPopupRender = popupRender || dropdownRender;
const mergedPopupRender = usePopupRender(popupRender || dropdownRender);
const mergedOnOpenChange = onOpenChange || onDropdownVisibleChange;
const isMultiple = !!(treeCheckable || multiple);