mirror of
https://gitee.com/ant-design/ant-design.git
synced 2024-11-30 02:59:04 +08:00
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:
parent
98a8d30439
commit
dde36511e6
49
components/_util/__tests__/useZIndex.test.tsx
Normal file
49
components/_util/__tests__/useZIndex.test.tsx
Normal 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],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
56
components/_util/hooks/useZIndex.tsx
Normal file
56
components/_util/hooks/useZIndex.tsx
Normal 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];
|
||||
}
|
5
components/_util/zindexContext.ts
Normal file
5
components/_util/zindexContext.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
const zIndexContext = React.createContext<number | undefined>(undefined);
|
||||
|
||||
export default zIndexContext;
|
@ -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,13 +115,18 @@ 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>
|
||||
<zIndexContext.Provider value={contextZIndex}>
|
||||
<Dialog
|
||||
width={width}
|
||||
{...restProps}
|
||||
zIndex={zIndex}
|
||||
getContainer={getContainer === undefined ? getContextPopupContainer : getContainer}
|
||||
prefixCls={prefixCls}
|
||||
rootClassName={classNames(hashId, rootClassName)}
|
||||
@ -145,6 +152,7 @@ const Modal: React.FC<ModalProps> = (props) => {
|
||||
}}
|
||||
panelRef={panelRef}
|
||||
/>
|
||||
</zIndexContext.Provider>
|
||||
</NoFormStyle>
|
||||
</NoCompactStyle>,
|
||||
);
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
7
components/modal/demo/nested.md
Normal file
7
components/modal/demo/nested.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
嵌套弹框
|
||||
|
||||
## en-US
|
||||
|
||||
Nested modal.
|
89
components/modal/demo/nested.tsx
Normal file
89
components/modal/demo/nested.tsx
Normal 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;
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
@ -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": {
|
||||
|
Loading…
Reference in New Issue
Block a user