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:
lijianan 2024-08-09 22:22:45 +08:00 committed by GitHub
parent c9fcef725c
commit 4c2a2b0c0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 100 additions and 95 deletions

View File

@ -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));
});
});

View File

@ -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;
}
};

View File

@ -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>
),

View File

@ -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`}>

View File

@ -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