Skip to content

Commit e14ab4b

Browse files
committed
fix: unify z-index logic and ensure it's correct for all isPushed and maskProps combinations
1 parent ce59e1a commit e14ab4b

5 files changed

Lines changed: 136 additions & 24 deletions

File tree

packages/eui/src/components/flyout/_flyout_overlay.tsx

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,29 @@
66
* Side Public License, v 1.
77
*/
88

9-
import React, { PropsWithChildren } from 'react';
9+
import React, { PropsWithChildren, useMemo } from 'react';
1010
import { css, cx } from '@emotion/css';
11-
import type { EuiFlyoutComponentProps } from './flyout.component';
1211
import { EuiOverlayMask } from '../overlay_mask';
1312
import { EuiPortal } from '../portal';
14-
import { useEuiMemoizedStyles, type UseEuiTheme } from '../../services';
13+
import type { EuiFlyoutComponentProps } from './flyout.component';
1514

1615
export interface EuiFlyoutOverlayProps extends PropsWithChildren {
1716
hasOverlayMask: boolean;
1817
maskProps: EuiFlyoutComponentProps['maskProps'];
1918
isPushed: boolean;
19+
maskZIndex: number;
2020
}
2121

22-
const getEuiFlyoutOverlayStyles = ({ euiTheme }: UseEuiTheme) => {
23-
// TODO(tkajtoch): This should likely depend on maskProps.headerZIndexLocation
24-
// in cases where the mask has z-index 6000
25-
const maskLevel = Number(euiTheme.levels.flyout) - 1;
26-
27-
return {
28-
overlayMask: css`
29-
/*
30-
This needs to have !important to override the default EuiOverlayMask
31-
z-index based on the headerZindexLocation prop. Using the style attribute
32-
doesn't work since EuiOverlayMask requires a string style prop that
33-
causes React errors in the test environment.
34-
*/
35-
z-index: ${maskLevel} !important;
36-
`,
37-
};
22+
const getEuiFlyoutOverlayStyles = (zIndex: number) => {
23+
/*
24+
This needs to have !important to override the default EuiOverlayMask
25+
z-index based on the headerZindexLocation prop. Using the style attribute
26+
doesn't work since EuiOverlayMask requires a string style prop that
27+
causes React errors in the test environment.
28+
*/
29+
return css`
30+
z-index: ${zIndex} !important;
31+
`;
3832
};
3933

4034
/**
@@ -50,21 +44,28 @@ export const EuiFlyoutOverlay = ({
5044
isPushed,
5145
maskProps,
5246
hasOverlayMask,
47+
maskZIndex,
5348
}: EuiFlyoutOverlayProps) => {
54-
const styles = useEuiMemoizedStyles(getEuiFlyoutOverlayStyles);
49+
const styles = useMemo(
50+
() => getEuiFlyoutOverlayStyles(maskZIndex),
51+
[maskZIndex]
52+
);
53+
5554
let content = children;
5655

5756
if (!isPushed || hasOverlayMask) {
5857
content = <EuiPortal>{content}</EuiPortal>;
5958
}
6059

60+
const classes = cx(maskProps?.className, styles);
61+
6162
return (
6263
<>
6364
{hasOverlayMask && (
6465
<EuiOverlayMask
6566
headerZindexLocation="below"
6667
{...maskProps}
67-
className={cx(maskProps?.className, styles.overlayMask)}
68+
className={classes}
6869
/>
6970
)}
7071
{content}

packages/eui/src/components/flyout/flyout.component.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import { EuiFlyoutOverlay } from './_flyout_overlay';
7070
import { EuiFlyoutResizeButton } from './_flyout_resize_button';
7171
import { useEuiFlyoutResizable } from './use_flyout_resizable';
7272
import type { EuiFlyoutCloseEvent } from './types';
73+
import { useEuiFlyoutZIndex } from './use_flyout_z_index';
7374

7475
interface _EuiFlyoutComponentProps {
7576
/**
@@ -403,6 +404,11 @@ export const EuiFlyoutComponent = forwardRef(
403404

404405
const siblingFlyoutWidth = useFlyoutWidth(siblingFlyoutId);
405406

407+
const { flyoutZIndex, maskZIndex } = useEuiFlyoutZIndex({
408+
maskProps,
409+
isPushed,
410+
});
411+
406412
/**
407413
* Set inline styles
408414
*/
@@ -412,7 +418,8 @@ export const EuiFlyoutComponent = forwardRef(
412418
layoutMode,
413419
siblingFlyoutId,
414420
siblingFlyoutWidth || null,
415-
maxWidth
421+
maxWidth,
422+
flyoutZIndex
416423
);
417424

418425
return { ...style, ...composedStyles };
@@ -423,6 +430,7 @@ export const EuiFlyoutComponent = forwardRef(
423430
siblingFlyoutId,
424431
siblingFlyoutWidth,
425432
maxWidth,
433+
flyoutZIndex,
426434
]);
427435

428436
const styles = useEuiMemoizedStyles(euiFlyoutStyles);
@@ -579,6 +587,7 @@ export const EuiFlyoutComponent = forwardRef(
579587
<EuiFlyoutOverlay
580588
hasOverlayMask={hasOverlayMask}
581589
isPushed={isPushed}
590+
maskZIndex={maskZIndex}
582591
maskProps={{
583592
...maskProps,
584593
maskRef: maskCombinedRefs,

packages/eui/src/components/flyout/flyout.styles.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ export const euiFlyoutStyles = (euiThemeContext: UseEuiTheme) => {
8080
${logicalCSS('bottom', 0)}
8181
${logicalCSS('top', 'var(--euiFixedHeadersOffset, 0)')}
8282
${logicalCSS('height', 'inherit')}
83-
z-index: ${euiTheme.levels.flyout};
8483
background: ${euiTheme.colors.backgroundBasePlain};
8584
display: flex;
8685
flex-direction: column;
@@ -361,7 +360,8 @@ export const composeFlyoutInlineStyles = (
361360
layoutMode: 'side-by-side' | 'stacked',
362361
siblingFlyoutId: string | null,
363362
siblingFlyoutWidth: number | null,
364-
maxWidth: boolean | number | string | undefined
363+
maxWidth: boolean | number | string | undefined,
364+
zIndex: number
365365
): React.CSSProperties => {
366366
// Handle custom width values (non-named sizes)
367367
const customWidthStyles = !isEuiFlyoutSizeNamed(size)
@@ -433,5 +433,6 @@ export const composeFlyoutInlineStyles = (
433433
...dynamicStyles,
434434
...minWidthOverride,
435435
...(finalMaxWidth ? { maxWidth: finalMaxWidth } : {}),
436+
zIndex,
436437
});
437438
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import { renderHook } from '../../test/rtl/render_hook';
10+
import { UseEuiFlyoutZIndex, useEuiFlyoutZIndex } from './use_flyout_z_index';
11+
12+
describe('useEuiFlyoutZIndex', () => {
13+
const render = (initialProps: UseEuiFlyoutZIndex) =>
14+
renderHook((props: UseEuiFlyoutZIndex) => useEuiFlyoutZIndex(props), {
15+
initialProps,
16+
});
17+
18+
it('returns flyout level based z-index values when isPushed = true', () => {
19+
const { result, rerender } = render({ isPushed: true });
20+
expect(result.current.flyoutZIndex).toEqual(1000);
21+
expect(result.current.maskZIndex).toEqual(999);
22+
23+
rerender({ isPushed: true, maskProps: { headerZindexLocation: 'above' } });
24+
expect(result.current.flyoutZIndex).toEqual(1000);
25+
expect(result.current.maskZIndex).toEqual(999);
26+
27+
rerender({ isPushed: true, maskProps: { headerZindexLocation: 'below' } });
28+
expect(result.current.flyoutZIndex).toEqual(1000);
29+
expect(result.current.maskZIndex).toEqual(999);
30+
});
31+
32+
it('returns flyout level based z-index values when maskProps.headerZindexLocation != "above"', () => {
33+
const { result, rerender } = render({ isPushed: false, maskProps: {} });
34+
expect(result.current.flyoutZIndex).toEqual(1000);
35+
expect(result.current.maskZIndex).toEqual(999);
36+
37+
rerender({ isPushed: false, maskProps: { headerZindexLocation: 'below' } });
38+
expect(result.current.flyoutZIndex).toEqual(1000);
39+
expect(result.current.maskZIndex).toEqual(999);
40+
});
41+
42+
it('returns mask level based z-index values when maskProps.headerZindexLocation = "above"', () => {
43+
const { result } = render({
44+
isPushed: false,
45+
maskProps: { headerZindexLocation: 'above' },
46+
});
47+
expect(result.current.flyoutZIndex).toEqual(6000);
48+
expect(result.current.maskZIndex).toEqual(5999);
49+
});
50+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import type { CSSProperties } from 'react';
10+
import { useEuiTheme } from '../../services';
11+
import type { EuiOverlayMaskProps } from '../overlay_mask';
12+
13+
/**
14+
* @internal
15+
*/
16+
export interface UseEuiFlyoutZIndex {
17+
maskProps?: EuiOverlayMaskProps;
18+
isPushed: boolean;
19+
}
20+
21+
const calculateZIndex = (initialValue: CSSProperties['zIndex']) => {
22+
const valueAsNumber = Number(initialValue);
23+
24+
return {
25+
flyoutZIndex: valueAsNumber,
26+
maskZIndex: valueAsNumber - 1,
27+
};
28+
};
29+
30+
/**
31+
* TODO: Calculate z-index values so that the latest flyout is always on top
32+
* https://github.com/elastic/eui/issues/9160
33+
* @internal
34+
*/
35+
export const useEuiFlyoutZIndex = ({
36+
maskProps,
37+
isPushed,
38+
}: UseEuiFlyoutZIndex) => {
39+
const { euiTheme } = useEuiTheme();
40+
41+
// The default headerZindexLocation for EuiFlyout is "below"
42+
// which is different from what EuiOverlayMask fallbacks to - see
43+
// _flyout_overlay.tsx.
44+
// We set z-index to mask level only when explicitly overridden
45+
// via the maskProps prop
46+
if (!isPushed && maskProps?.headerZindexLocation === 'above') {
47+
return calculateZIndex(euiTheme.levels.mask);
48+
}
49+
50+
return calculateZIndex(euiTheme.levels.flyout);
51+
};

0 commit comments

Comments
 (0)