feat: Modal & Select support z-index context to manage z-index (#45346)

* feat: z-index manager

* feat: z-index manager

* feat: update snap

* chore: update site-limit

* feat: optimize code

* feat: optimize code

* feat: add test case

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code
This commit is contained in:
kiner-tang(文辉) 2023-10-19 02:03:20 -05:00 committed by GitHub
parent 98a8d30439
commit dde36511e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 347 additions and 31 deletions

View File

@ -0,0 +1,49 @@
import type { PropsWithChildren } from 'react';
import React, { useEffect } from 'react';
import { render } from '@testing-library/react';
import zIndexContext from '../zindexContext';
import type { ZIndexConsumer, ZIndexContainer } from '../hooks/useZIndex';
import { consumerBaseZIndexOffset, containerBaseZIndexOffset, useZIndex } from '../hooks/useZIndex';
const WrapWithProvider: React.FC<PropsWithChildren<{ containerType: ZIndexContainer }>> = ({
children,
containerType,
}) => {
const [, contextZIndex] = useZIndex(containerType);
return <zIndexContext.Provider value={contextZIndex}>{children}</zIndexContext.Provider>;
};
describe('Test useZIndex hooks', () => {
Object.keys(containerBaseZIndexOffset).forEach((containerKey) => {
Object.keys(consumerBaseZIndexOffset).forEach((key) => {
describe(`Test ${key} zIndex in ${containerKey}`, () => {
it('parentZIndex should be parent zIndex', () => {
const fn = jest.fn();
const Child = () => {
const [zIndex] = useZIndex(key as ZIndexConsumer);
useEffect(() => {
fn(zIndex);
}, [zIndex]);
return <div>Child</div>;
};
const App = () => (
<WrapWithProvider containerType={containerKey as ZIndexContainer}>
<WrapWithProvider containerType={containerKey as ZIndexContainer}>
<WrapWithProvider containerType={containerKey as ZIndexContainer}>
<Child />
</WrapWithProvider>
</WrapWithProvider>
</WrapWithProvider>
);
render(<App />);
expect(fn).toHaveBeenLastCalledWith(
(1000 + containerBaseZIndexOffset[containerKey as ZIndexContainer]) * 3 +
consumerBaseZIndexOffset[key as ZIndexConsumer],
);
});
});
});
});
});

View File

@ -0,0 +1,56 @@
import React from 'react';
import useToken from '../../theme/useToken';
import zIndexContext from '../zindexContext';
export type ZIndexContainer = 'Modal' | 'Drawer' | 'Popover' | 'Popconfirm' | 'Tooltip' | 'Tour';
export type ZIndexConsumer =
| 'Select'
| 'Dropdown'
| 'Cascader'
| 'TreeSelect'
| 'AutoComplete'
| 'ColorPicker'
| 'DatePicker'
| 'TimePicker'
| 'Menu';
export const containerBaseZIndexOffset: Record<ZIndexContainer, number> = {
Modal: 0,
Drawer: 0,
Popover: 30,
Popconfirm: 60,
Tooltip: 70,
Tour: 70,
};
export const consumerBaseZIndexOffset: Record<ZIndexConsumer, number> = {
Select: 50,
Dropdown: 50,
Cascader: 50,
TreeSelect: 50,
AutoComplete: 50,
ColorPicker: 30,
DatePicker: 50,
TimePicker: 50,
Menu: 50,
};
function isContainerType(type: ZIndexContainer | ZIndexConsumer): type is ZIndexContainer {
return type in containerBaseZIndexOffset;
}
export function useZIndex(
componentType: ZIndexContainer | ZIndexConsumer,
customZIndex?: number,
): [zIndex: number | undefined, contextZIndex: number] {
const [, token] = useToken();
const parentZIndex = React.useContext(zIndexContext);
const isContainer = isContainerType(componentType);
let zIndex = parentZIndex ?? 0;
if (isContainer) {
zIndex += token.zIndexPopupBase + containerBaseZIndexOffset[componentType];
} else {
zIndex += consumerBaseZIndexOffset[componentType];
}
return [parentZIndex === undefined ? customZIndex : zIndex, zIndex];
}

View File

@ -0,0 +1,5 @@
import React from 'react';
const zIndexContext = React.createContext<number | undefined>(undefined);
export default zIndexContext;

View File

@ -7,6 +7,7 @@ import useClosable from '../_util/hooks/useClosable';
import { getTransitionName } from '../_util/motion';
import { canUseDocElement } from '../_util/styleChecker';
import { devUseWarning } from '../_util/warning';
import zIndexContext from '../_util/zindexContext';
import { ConfigContext } from '../config-provider';
import { NoFormStyle } from '../form/context';
import { NoCompactStyle } from '../space/Compact';
@ -14,6 +15,7 @@ import { usePanelRef } from '../watermark/context';
import type { ModalProps, MousePosition } from './interface';
import { Footer, renderCloseIcon } from './shared';
import useStyle from './style';
import { useZIndex } from '../_util/hooks/useZIndex';
let mousePosition: MousePosition;
@ -113,38 +115,44 @@ const Modal: React.FC<ModalProps> = (props) => {
// Select `ant-modal-content` by `panelRef`
const panelRef = usePanelRef(`.${prefixCls}-content`);
// ============================ zIndex ============================
const [zIndex, contextZIndex] = useZIndex('Modal', restProps.zIndex);
// =========================== Render ===========================
return wrapSSR(
<NoCompactStyle>
<NoFormStyle status override>
<Dialog
width={width}
{...restProps}
getContainer={getContainer === undefined ? getContextPopupContainer : getContainer}
prefixCls={prefixCls}
rootClassName={classNames(hashId, rootClassName)}
footer={dialogFooter}
visible={open ?? visible}
mousePosition={restProps.mousePosition ?? mousePosition}
onClose={handleCancel}
closable={mergedClosable}
closeIcon={mergedCloseIcon}
focusTriggerAfterClose={focusTriggerAfterClose}
transitionName={getTransitionName(rootPrefixCls, 'zoom', props.transitionName)}
maskTransitionName={getTransitionName(rootPrefixCls, 'fade', props.maskTransitionName)}
className={classNames(hashId, className, modal?.className)}
style={{ ...modal?.style, ...style }}
classNames={{
wrapper: wrapClassNameExtended,
...modal?.classNames,
...modalClassNames,
}}
styles={{
...modal?.styles,
...modalStyles,
}}
panelRef={panelRef}
/>
<zIndexContext.Provider value={contextZIndex}>
<Dialog
width={width}
{...restProps}
zIndex={zIndex}
getContainer={getContainer === undefined ? getContextPopupContainer : getContainer}
prefixCls={prefixCls}
rootClassName={classNames(hashId, rootClassName)}
footer={dialogFooter}
visible={open ?? visible}
mousePosition={restProps.mousePosition ?? mousePosition}
onClose={handleCancel}
closable={mergedClosable}
closeIcon={mergedCloseIcon}
focusTriggerAfterClose={focusTriggerAfterClose}
transitionName={getTransitionName(rootPrefixCls, 'zoom', props.transitionName)}
maskTransitionName={getTransitionName(rootPrefixCls, 'fade', props.maskTransitionName)}
className={classNames(hashId, className, modal?.className)}
style={{ ...modal?.style, ...style }}
classNames={{
wrapper: wrapClassNameExtended,
...modal?.classNames,
...modalClassNames,
}}
styles={{
...modal?.styles,
...modalStyles,
}}
panelRef={panelRef}
/>
</zIndexContext.Provider>
</NoFormStyle>
</NoCompactStyle>,
);

View File

@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
import { Select } from 'antd';
import type { ModalProps } from '..';
import Modal from '..';
@ -182,4 +183,37 @@ describe('Modal', () => {
expect(document.querySelector('.first-origin')).toMatchSnapshot();
expect(document.querySelector('.second-props-origin')).toMatchSnapshot();
});
it('z-index should be accumulated in nested Modal', () => {
const options = [
{
label: 'Option 1',
value: '1',
},
{
label: 'Option 2',
value: '2',
},
];
render(
<>
<Select open options={options} popupClassName="select0" />
<Modal open>
<Select open options={options} popupClassName="select1" />
<Modal open>
<Select open options={options} popupClassName="select2" />
</Modal>
</Modal>
</>,
);
expect(
(document.querySelectorAll('.ant-modal-wrap')[0] as HTMLDivElement)!.style.zIndex,
).toBeFalsy();
expect((document.querySelectorAll('.ant-modal-wrap')[1] as HTMLDivElement)!.style.zIndex).toBe(
'2000',
);
expect((document.querySelector('.select0') as HTMLDivElement)!.style.zIndex).toBeFalsy();
expect((document.querySelector('.select1') as HTMLDivElement)!.style.zIndex).toBe('1050');
expect((document.querySelector('.select2') as HTMLDivElement)!.style.zIndex).toBe('2050');
});
});

View File

@ -669,6 +669,36 @@ exports[`renders components/modal/demo/modal-render.tsx extend context correctly
exports[`renders components/modal/demo/modal-render.tsx extend context correctly 2`] = `[]`;
exports[`renders components/modal/demo/nested.tsx extend context correctly 1`] = `
<button
aria-checked="false"
class="ant-switch"
role="switch"
style="position: relative; z-index: 4000;"
type="button"
>
<div
class="ant-switch-handle"
/>
<span
class="ant-switch-inner"
>
<span
class="ant-switch-inner-checked"
>
Open
</span>
<span
class="ant-switch-inner-unchecked"
>
Close
</span>
</span>
</button>
`;
exports[`renders components/modal/demo/nested.tsx extend context correctly 2`] = `[]`;
exports[`renders components/modal/demo/position.tsx extend context correctly 1`] = `
Array [
<button

View File

@ -639,6 +639,34 @@ exports[`renders components/modal/demo/modal-render.tsx correctly 1`] = `
</button>
`;
exports[`renders components/modal/demo/nested.tsx correctly 1`] = `
<button
aria-checked="false"
class="ant-switch"
role="switch"
style="position:relative;z-index:4000"
type="button"
>
<div
class="ant-switch-handle"
/>
<span
class="ant-switch-inner"
>
<span
class="ant-switch-inner-checked"
>
Open
</span>
<span
class="ant-switch-inner-unchecked"
>
Close
</span>
</span>
</button>
`;
exports[`renders components/modal/demo/position.tsx correctly 1`] = `
Array [
<button

View File

@ -0,0 +1,7 @@
## zh-CN
嵌套弹框
## en-US
Nested modal.

View File

@ -0,0 +1,89 @@
import React, { useState } from 'react';
import { Modal, Select, Switch } from 'antd';
const options = [
{
label: 'Option 1',
value: '1',
},
{
label: 'Option 2',
value: '2',
},
];
const App: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
return (
<>
<Switch
style={{ position: 'relative', zIndex: 4000 }}
checkedChildren="Open"
unCheckedChildren="Close"
onChange={(open) => setIsModalOpen(open)}
/>
<Modal
title="Basic Modal"
open={isModalOpen}
footer={null}
destroyOnClose
onCancel={() => setIsModalOpen(false)}
maskClosable={false}
closable={false}
styles={{
content: {
marginBlockStart: 100,
},
}}
>
<Select open value="1" options={options} />
<Modal
title="Nested Modal"
open={isModalOpen}
footer={null}
destroyOnClose
mask={false}
onCancel={() => setIsModalOpen(false)}
maskClosable={false}
closable={false}
styles={{
content: {
marginBlockStart: 250,
},
body: {
display: 'flex',
justifyContent: 'center',
},
}}
>
<Select open value="1" options={options} />
<Modal
title="Nested Modal"
open={isModalOpen}
footer={null}
destroyOnClose
mask={false}
maskClosable={false}
onCancel={() => setIsModalOpen(false)}
closable={false}
styles={{
content: {
marginBlockStart: 400,
},
body: {
display: 'flex',
justifyContent: 'flex-end',
},
}}
>
<Select open value="1" options={options} />
</Modal>
</Modal>
</Modal>
</>
);
};
export default App;

View File

@ -35,6 +35,7 @@ Additionally, if you need show a simple confirmation dialog, you can use [`App.u
<code src="./demo/confirm.tsx">Static confirmation</code>
<code src="./demo/classNames.tsx">Customize className for build-in module</code>
<code src="./demo/confirm-router.tsx">destroy confirmation modal dialog</code>
<code src="./demo/nested.tsx" debug>Nested Modal</code>
<code src="./demo/render-panel.tsx" debug>\_InternalPanelDoNotUseOrYouWillBeFired</code>
<code src="./demo/custom-mouse-position.tsx" debug>Control modal's animation origin position</code>
<code src="./demo/wireframe.tsx" debug>Wireframe</code>

View File

@ -36,6 +36,7 @@ demo:
<code src="./demo/confirm.tsx">静态确认对话框</code>
<code src="./demo/classNames.tsx">自定义内部模块 className</code>
<code src="./demo/confirm-router.tsx">销毁确认对话框</code>
<code src="./demo/nested.tsx" debug>嵌套弹框</code>
<code src="./demo/render-panel.tsx" debug>\_InternalPanelDoNotUseOrYouWillBeFired</code>
<code src="./demo/custom-mouse-position.tsx" debug>控制弹框动画原点</code>
<code src="./demo/wireframe.tsx" debug>线框风格</code>

View File

@ -7,6 +7,7 @@ import type { OptionProps } from 'rc-select/lib/Option';
import type { BaseOptionType, DefaultOptionType } from 'rc-select/lib/Select';
import omit from 'rc-util/lib/omit';
import { useZIndex } from '../_util/hooks/useZIndex';
import type { SelectCommonPlacement } from '../_util/motion';
import { getTransitionName } from '../_util/motion';
import genPurePanel from '../_util/PurePanel';
@ -22,8 +23,8 @@ import { FormItemInputContext } from '../form/context';
import { useCompactItemContext } from '../space/Compact';
import useStyle from './style';
import useBuiltinPlacements from './useBuiltinPlacements';
import useShowArrow from './useShowArrow';
import useIcons from './useIcons';
import useShowArrow from './useShowArrow';
type RawValue = string | number;
@ -240,6 +241,9 @@ const InternalSelect = <
);
}
// ====================== zIndex =========================
const [zIndex] = useZIndex('Select', props.dropdownStyle?.zIndex as number);
// ====================== Render =======================
return wrapSSR(
<RcSelect<ValueType, OptionType>
@ -266,6 +270,10 @@ const InternalSelect = <
getPopupContainer={getPopupContainer || getContextPopupContainer}
dropdownClassName={rcSelectRtlDropdownClassName}
disabled={mergedDisabled}
dropdownStyle={{
...props?.dropdownStyle,
zIndex: props.dropdownStyle?.zIndex ?? zIndex,
}}
/>,
);
};

View File

@ -319,11 +319,11 @@
"size-limit": [
{
"path": "./dist/antd.min.js",
"limit": "399 KiB"
"limit": "400 KiB"
},
{
"path": "./dist/antd-with-locales.min.js",
"limit": "458 KiB"
"limit": "459 KiB"
}
],
"tnpm": {