mirror of
https://gitee.com/ant-design/ant-design.git
synced 2024-12-02 03:59:01 +08:00
feat: FloatButton support dynamic z-index (#50311)
* feat: FloatBtn support dynamic z-index * feat: FloatBtn support dynamic z-index * test: fix test case * fix: fix * test: add test case * test: fix test case * chore: clear
This commit is contained in:
parent
c9fcef725c
commit
4c2a2b0c0d
@ -9,6 +9,7 @@ import {
|
||||
DatePicker,
|
||||
Drawer,
|
||||
Dropdown,
|
||||
FloatButton,
|
||||
Image,
|
||||
Menu,
|
||||
Modal,
|
||||
@ -25,17 +26,16 @@ import type { ZIndexConsumer, ZIndexContainer } from '../hooks/useZIndex';
|
||||
import { consumerBaseZIndexOffset, containerBaseZIndexOffset, useZIndex } from '../hooks/useZIndex';
|
||||
import zIndexContext from '../zindexContext';
|
||||
|
||||
const WrapWithProvider: React.FC<PropsWithChildren<{ containerType: ZIndexContainer }>> = ({
|
||||
const WrapWithProvider: React.FC<PropsWithChildren<{ container: ZIndexContainer }>> = ({
|
||||
children,
|
||||
containerType,
|
||||
container,
|
||||
}) => {
|
||||
const [, contextZIndex] = useZIndex(containerType);
|
||||
const [, contextZIndex] = useZIndex(container);
|
||||
return <zIndexContext.Provider value={contextZIndex}>{children}</zIndexContext.Provider>;
|
||||
};
|
||||
|
||||
const containerComponent: Record<
|
||||
ZIndexContainer,
|
||||
React.FC<PropsWithChildren<{ rootClassName?: string }>>
|
||||
const containerComponent: Partial<
|
||||
Record<ZIndexContainer, React.FC<Readonly<PropsWithChildren<{ rootClassName?: string }>>>>
|
||||
> = {
|
||||
Modal: ({ children, ...restProps }) => (
|
||||
<Modal {...restProps} open>
|
||||
@ -63,16 +63,7 @@ const containerComponent: Record<
|
||||
</Tooltip>
|
||||
),
|
||||
Tour: ({ children, ...restProps }) => (
|
||||
<Tour
|
||||
{...restProps}
|
||||
open
|
||||
steps={[
|
||||
{
|
||||
title: 'cover title',
|
||||
description: children,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Tour {...restProps} open steps={[{ title: 'cover title', description: children }]} />
|
||||
),
|
||||
};
|
||||
|
||||
@ -124,7 +115,9 @@ const items: MenuProps['items'] = [
|
||||
},
|
||||
];
|
||||
|
||||
const consumerComponent: Record<ZIndexConsumer, React.FC<{ rootClassName: string }>> = {
|
||||
const consumerComponent: Partial<
|
||||
Record<ZIndexConsumer, React.FC<Readonly<{ rootClassName: string }>>>
|
||||
> = {
|
||||
SelectLike: ({ rootClassName, ...props }) => (
|
||||
<>
|
||||
<Select
|
||||
@ -200,7 +193,7 @@ const consumerComponent: Record<ZIndexConsumer, React.FC<{ rootClassName: string
|
||||
),
|
||||
};
|
||||
|
||||
function getConsumerSelector(baseSelector: string, consumer: ZIndexConsumer): string {
|
||||
const getConsumerSelector = (baseSelector: string, consumer: ZIndexConsumer): string => {
|
||||
let selector = baseSelector;
|
||||
if (consumer === 'SelectLike') {
|
||||
selector = ['Select', 'Cascader', 'TreeSelect', 'AutoComplete', 'ColorPicker']
|
||||
@ -225,7 +218,7 @@ function getConsumerSelector(baseSelector: string, consumer: ZIndexConsumer): st
|
||||
.join(',');
|
||||
}
|
||||
return selector;
|
||||
}
|
||||
};
|
||||
|
||||
describe('Test useZIndex hooks', () => {
|
||||
beforeEach(() => {
|
||||
@ -234,15 +227,14 @@ describe('Test useZIndex hooks', () => {
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
const containers = Object.keys(containerComponent);
|
||||
const consumers = Object.keys(consumerComponent);
|
||||
|
||||
containers.forEach((containerKey) => {
|
||||
consumers.forEach((key) => {
|
||||
Object.keys(containerComponent).forEach((containerKey) => {
|
||||
Object.keys(consumerComponent).forEach((key) => {
|
||||
const containerZIndexValue = containerBaseZIndexOffset[containerKey as ZIndexContainer];
|
||||
const consumerZIndexValue = consumerBaseZIndexOffset[key as ZIndexConsumer];
|
||||
describe(`Test ${key} zIndex in ${containerKey}`, () => {
|
||||
it('Test hooks', () => {
|
||||
const fn = jest.fn();
|
||||
const Child = () => {
|
||||
const Child: React.FC = () => {
|
||||
const [zIndex] = useZIndex(key as ZIndexConsumer);
|
||||
useEffect(() => {
|
||||
fn(zIndex);
|
||||
@ -250,28 +242,27 @@ describe('Test useZIndex hooks', () => {
|
||||
return <div>Child</div>;
|
||||
};
|
||||
|
||||
const App = () => (
|
||||
<WrapWithProvider containerType={containerKey as ZIndexContainer}>
|
||||
<WrapWithProvider containerType={containerKey as ZIndexContainer}>
|
||||
<WrapWithProvider containerType={containerKey as ZIndexContainer}>
|
||||
const App: React.FC = () => (
|
||||
<WrapWithProvider container={containerKey as ZIndexContainer}>
|
||||
<WrapWithProvider container={containerKey as ZIndexContainer}>
|
||||
<WrapWithProvider container={containerKey as ZIndexContainer}>
|
||||
<Child />
|
||||
</WrapWithProvider>
|
||||
</WrapWithProvider>
|
||||
</WrapWithProvider>
|
||||
);
|
||||
render(<App />);
|
||||
|
||||
expect(fn).toHaveBeenLastCalledWith(
|
||||
1000 +
|
||||
containerBaseZIndexOffset[containerKey as ZIndexContainer] * 3 +
|
||||
consumerBaseZIndexOffset[key as ZIndexConsumer],
|
||||
1000 + containerZIndexValue * 3 + consumerZIndexValue,
|
||||
);
|
||||
});
|
||||
|
||||
it('Test Component', async () => {
|
||||
const Container = containerComponent[containerKey as ZIndexContainer];
|
||||
const Consumer = consumerComponent[key as ZIndexConsumer];
|
||||
const Container = containerComponent[containerKey as ZIndexContainer]!;
|
||||
const Consumer = consumerComponent[key as ZIndexConsumer]!;
|
||||
|
||||
const App = () => (
|
||||
const App: React.FC = () => (
|
||||
<>
|
||||
<Consumer rootClassName="consumer1" />
|
||||
<Container rootClassName="container1">
|
||||
@ -292,75 +283,49 @@ describe('Test useZIndex hooks', () => {
|
||||
const selector3 = getConsumerSelector('.consumer3', key as ZIndexConsumer);
|
||||
|
||||
if (['SelectLike', 'DatePicker', 'ImagePreview'].includes(key)) {
|
||||
let comps = document.querySelectorAll(selector1);
|
||||
let comps = document.querySelectorAll<HTMLElement>(selector1);
|
||||
comps.forEach((comp) => {
|
||||
expect((comp as HTMLDivElement).style.zIndex).toBeFalsy();
|
||||
expect(comp?.style.zIndex).toBeFalsy();
|
||||
});
|
||||
comps = document.querySelectorAll(selector2);
|
||||
comps = document.querySelectorAll<HTMLElement>(selector2);
|
||||
comps.forEach((comp) => {
|
||||
const isColorPicker = (comp as HTMLDivElement).className.includes('comp-ColorPicker');
|
||||
const isColorPicker = comp?.className.includes('comp-ColorPicker');
|
||||
const consumerOffset = isColorPicker
|
||||
? containerBaseZIndexOffset.Popover
|
||||
: consumerBaseZIndexOffset[key as ZIndexConsumer];
|
||||
: consumerZIndexValue;
|
||||
const operOffset = comp.classList.contains('ant-image-preview-operations-wrapper')
|
||||
? 1
|
||||
: 0;
|
||||
expect((comp as HTMLDivElement).style.zIndex).toBe(
|
||||
String(
|
||||
1000 +
|
||||
containerBaseZIndexOffset[containerKey as ZIndexContainer] +
|
||||
consumerOffset +
|
||||
operOffset,
|
||||
),
|
||||
expect(comp?.style.zIndex).toBe(
|
||||
String(1000 + containerZIndexValue + consumerOffset + operOffset),
|
||||
);
|
||||
});
|
||||
|
||||
comps = document.querySelectorAll(selector3);
|
||||
comps = document.querySelectorAll<HTMLElement>(selector3);
|
||||
comps.forEach((comp) => {
|
||||
const isColorPicker = (comp as HTMLDivElement).className.includes('comp-ColorPicker');
|
||||
const isColorPicker = comp?.className.includes('comp-ColorPicker');
|
||||
const consumerOffset = isColorPicker
|
||||
? containerBaseZIndexOffset.Popover
|
||||
: consumerBaseZIndexOffset[key as ZIndexConsumer];
|
||||
: consumerZIndexValue;
|
||||
const operOffset = comp.classList.contains('ant-image-preview-operations-wrapper')
|
||||
? 1
|
||||
: 0;
|
||||
expect((comp as HTMLDivElement).style.zIndex).toBe(
|
||||
String(
|
||||
1000 +
|
||||
containerBaseZIndexOffset[containerKey as ZIndexContainer] * 2 +
|
||||
consumerOffset +
|
||||
operOffset,
|
||||
),
|
||||
expect(comp?.style.zIndex).toBe(
|
||||
String(1000 + containerZIndexValue * 2 + consumerOffset + operOffset),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
if (key === 'Tour') {
|
||||
expect((document.querySelector(selector1) as HTMLDivElement).style.zIndex).toBe(
|
||||
'1001',
|
||||
);
|
||||
} else {
|
||||
expect(
|
||||
(document.querySelector(selector1) as HTMLDivElement).style.zIndex,
|
||||
).toBeFalsy();
|
||||
}
|
||||
|
||||
expect((document.querySelector(selector2) as HTMLDivElement).style.zIndex).toBe(
|
||||
String(
|
||||
1000 +
|
||||
containerBaseZIndexOffset[containerKey as ZIndexContainer] +
|
||||
consumerBaseZIndexOffset[key as ZIndexConsumer],
|
||||
),
|
||||
const element1 = document.querySelector<HTMLElement>(selector1);
|
||||
const element2 = document.querySelector<HTMLElement>(selector2);
|
||||
const element3 = document.querySelector<HTMLElement>(selector3);
|
||||
expect(element1?.style.zIndex).toBe(key === 'Tour' ? '1001' : '');
|
||||
expect(element2?.style.zIndex).toBe(
|
||||
String(1000 + containerZIndexValue + consumerZIndexValue),
|
||||
);
|
||||
|
||||
expect((document.querySelector(selector3) as HTMLDivElement).style.zIndex).toBe(
|
||||
String(
|
||||
1000 +
|
||||
containerBaseZIndexOffset[containerKey as ZIndexContainer] * 2 +
|
||||
consumerBaseZIndexOffset[key as ZIndexConsumer],
|
||||
),
|
||||
expect(element3?.style.zIndex).toBe(
|
||||
String(1000 + containerZIndexValue * 2 + consumerZIndexValue),
|
||||
);
|
||||
}
|
||||
|
||||
unmount();
|
||||
}, 20000);
|
||||
});
|
||||
@ -409,5 +374,25 @@ describe('Test useZIndex hooks', () => {
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Warning: [antd: Tooltip] `zIndex` is over design token `zIndexPopupBase` too much. It may cause unexpected override.',
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('FloatButton support zIndex', () => {
|
||||
const { container, rerender } = render(
|
||||
<WrapWithProvider container="FloatButton">
|
||||
<FloatButton />
|
||||
</WrapWithProvider>,
|
||||
);
|
||||
expect(container.querySelector<HTMLElement>('.ant-float-btn')?.style.zIndex).toBe(
|
||||
// parentZIndex + containerBaseZIndexOffset["FloatButton"]
|
||||
String(1100 + containerBaseZIndexOffset.FloatButton),
|
||||
);
|
||||
rerender(
|
||||
<WrapWithProvider container="FloatButton">
|
||||
<FloatButton style={{ zIndex: 666 }} />
|
||||
</WrapWithProvider>,
|
||||
);
|
||||
expect(container.querySelector<HTMLElement>('.ant-float-btn')?.style.zIndex).toBe(String(666));
|
||||
});
|
||||
});
|
||||
|
@ -4,7 +4,14 @@ import useToken from '../../theme/useToken';
|
||||
import { devUseWarning } from '../warning';
|
||||
import zIndexContext from '../zindexContext';
|
||||
|
||||
export type ZIndexContainer = 'Modal' | 'Drawer' | 'Popover' | 'Popconfirm' | 'Tooltip' | 'Tour';
|
||||
export type ZIndexContainer =
|
||||
| 'Modal'
|
||||
| 'Drawer'
|
||||
| 'Popover'
|
||||
| 'Popconfirm'
|
||||
| 'Tooltip'
|
||||
| 'Tour'
|
||||
| 'FloatButton';
|
||||
|
||||
export type ZIndexConsumer = 'SelectLike' | 'Dropdown' | 'DatePicker' | 'Menu' | 'ImagePreview';
|
||||
|
||||
@ -25,7 +32,9 @@ export const containerBaseZIndexOffset: Record<ZIndexContainer, number> = {
|
||||
Popconfirm: CONTAINER_OFFSET,
|
||||
Tooltip: CONTAINER_OFFSET,
|
||||
Tour: CONTAINER_OFFSET,
|
||||
FloatButton: CONTAINER_OFFSET,
|
||||
};
|
||||
|
||||
export const consumerBaseZIndexOffset: Record<ZIndexConsumer, number> = {
|
||||
SelectLike: 50,
|
||||
Dropdown: 50,
|
||||
@ -40,10 +49,10 @@ function isContainerType(type: ZIndexContainer | ZIndexConsumer): type is ZIndex
|
||||
|
||||
type ReturnResult = [zIndex: number | undefined, contextZIndex: number];
|
||||
|
||||
export function useZIndex(
|
||||
export const useZIndex = (
|
||||
componentType: ZIndexContainer | ZIndexConsumer,
|
||||
customZIndex?: number,
|
||||
): ReturnResult {
|
||||
): ReturnResult => {
|
||||
const [, token] = useToken();
|
||||
const parentZIndex = React.useContext(zIndexContext);
|
||||
const isContainer = isContainerType(componentType);
|
||||
@ -81,4 +90,4 @@ export function useZIndex(
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import React, { useContext, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import omit from 'rc-util/lib/omit';
|
||||
|
||||
import { useZIndex } from '../_util/hooks/useZIndex';
|
||||
import { devUseWarning } from '../_util/warning';
|
||||
import Badge from '../badge';
|
||||
import type { ConfigConsumerProps } from '../config-provider';
|
||||
@ -13,7 +14,6 @@ import FloatButtonGroupContext from './context';
|
||||
import Content from './FloatButtonContent';
|
||||
import type FloatButtonGroup from './FloatButtonGroup';
|
||||
import type {
|
||||
FloatButtonBadgeProps,
|
||||
FloatButtonContentProps,
|
||||
FloatButtonElement,
|
||||
FloatButtonProps,
|
||||
@ -29,6 +29,7 @@ const InternalFloatButton = React.forwardRef<FloatButtonElement, FloatButtonProp
|
||||
prefixCls: customizePrefixCls,
|
||||
className,
|
||||
rootClassName,
|
||||
style,
|
||||
type = 'default',
|
||||
shape = 'circle',
|
||||
icon,
|
||||
@ -59,11 +60,13 @@ const InternalFloatButton = React.forwardRef<FloatButtonElement, FloatButtonProp
|
||||
},
|
||||
);
|
||||
|
||||
// ============================ zIndex ============================
|
||||
const [zIndex] = useZIndex('FloatButton', style?.zIndex as number);
|
||||
|
||||
const mergedStyle: React.CSSProperties = { ...style, zIndex };
|
||||
|
||||
// 虽然在 ts 中已经 omit 过了,但是为了防止多余的属性被透传进来,这里再 omit 一遍,以防万一
|
||||
const badgeProps = useMemo<FloatButtonBadgeProps>(
|
||||
() => omit(badge, ['title', 'children', 'status', 'text'] as any[]),
|
||||
[badge],
|
||||
);
|
||||
const badgeProps = omit(badge, ['title', 'children', 'status', 'text'] as any[]);
|
||||
|
||||
const contentProps = useMemo<FloatButtonContentProps>(
|
||||
() => ({ prefixCls, description, icon, type }),
|
||||
@ -100,11 +103,11 @@ const InternalFloatButton = React.forwardRef<FloatButtonElement, FloatButtonProp
|
||||
|
||||
return wrapCSSVar(
|
||||
props.href ? (
|
||||
<a ref={ref} {...restProps} className={classString}>
|
||||
<a ref={ref} {...restProps} className={classString} style={mergedStyle}>
|
||||
{buttonNode}
|
||||
</a>
|
||||
) : (
|
||||
<button ref={ref} {...restProps} className={classString} type="button">
|
||||
<button ref={ref} {...restProps} className={classString} style={mergedStyle} type="button">
|
||||
{buttonNode}
|
||||
</button>
|
||||
),
|
||||
|
@ -5,6 +5,7 @@ import classNames from 'classnames';
|
||||
import CSSMotion from 'rc-motion';
|
||||
import useMergedState from 'rc-util/lib/hooks/useMergedState';
|
||||
|
||||
import { useZIndex } from '../_util/hooks/useZIndex';
|
||||
import { devUseWarning } from '../_util/warning';
|
||||
import type { ConfigConsumerProps } from '../config-provider';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
@ -47,6 +48,11 @@ const FloatButtonGroup: React.FC<FloatButtonGroupProps> = (props) => {
|
||||
[`${groupPrefixCls}-${shape}-shadow`]: !trigger,
|
||||
});
|
||||
|
||||
// ============================ zIndex ============================
|
||||
const [zIndex] = useZIndex('FloatButton', style?.zIndex as number);
|
||||
|
||||
const mergedStyle: React.CSSProperties = { ...style, zIndex };
|
||||
|
||||
const wrapperCls = classNames(hashId, `${groupPrefixCls}-wrap`);
|
||||
|
||||
const [open, setOpen] = useMergedState(false, { value: customOpen });
|
||||
@ -112,7 +118,7 @@ const FloatButtonGroup: React.FC<FloatButtonGroupProps> = (props) => {
|
||||
|
||||
return wrapCSSVar(
|
||||
<FloatButtonGroupProvider value={shape}>
|
||||
<div ref={floatButtonGroupRef} className={groupCls} style={style} {...hoverAction}>
|
||||
<div ref={floatButtonGroupRef} className={groupCls} style={mergedStyle} {...hoverAction}>
|
||||
{trigger && ['click', 'hover'].includes(trigger) ? (
|
||||
<>
|
||||
<CSSMotion visible={open} motionName={`${groupPrefixCls}-wrap`}>
|
||||
|
@ -101,13 +101,14 @@ const floatButtonGroupStyle: GenerateStyle<FloatButtonToken, CSSObject> = (token
|
||||
borderRadiusSM,
|
||||
badgeOffset,
|
||||
floatButtonBodyPadding,
|
||||
zIndexPopupBase,
|
||||
calc,
|
||||
} = token;
|
||||
const groupPrefixCls = `${componentCls}-group`;
|
||||
return {
|
||||
[groupPrefixCls]: {
|
||||
...resetComponent(token),
|
||||
zIndex: 99,
|
||||
zIndex: zIndexPopupBase,
|
||||
display: 'block',
|
||||
border: 'none',
|
||||
position: 'fixed',
|
||||
@ -221,6 +222,7 @@ const sharedFloatButtonStyle: GenerateStyle<FloatButtonToken, CSSObject> = (toke
|
||||
badgeOffset,
|
||||
dotOffsetInSquare,
|
||||
dotOffsetInCircle,
|
||||
zIndexPopupBase,
|
||||
calc,
|
||||
} = token;
|
||||
return {
|
||||
@ -229,7 +231,7 @@ const sharedFloatButtonStyle: GenerateStyle<FloatButtonToken, CSSObject> = (toke
|
||||
border: 'none',
|
||||
position: 'fixed',
|
||||
cursor: 'pointer',
|
||||
zIndex: 99,
|
||||
zIndex: zIndexPopupBase,
|
||||
// Do not remove the 'display: block' here.
|
||||
// Deleting it will cause marginBottom to become ineffective.
|
||||
// Ref: https://github.com/ant-design/ant-design/issues/44700
|
||||
|
Loading…
Reference in New Issue
Block a user