diff --git a/src-docs/src/views/overlay_mask/overlay_mask.js b/src-docs/src/views/overlay_mask/overlay_mask.js index 60a502bebc3..94981dd1ef5 100644 --- a/src-docs/src/views/overlay_mask/overlay_mask.js +++ b/src-docs/src/views/overlay_mask/overlay_mask.js @@ -5,6 +5,7 @@ import { EuiButton, EuiSpacer, EuiTitle, + EuiFocusTrap, } from '../../../../src/components'; export default () => { @@ -13,20 +14,22 @@ export default () => { const modal = ( - { - changeMask(false); - }} - > - -

Click anywhere to close overlay.

-
+ + { + changeMask(false); + }} + > + +

Click anywhere to close overlay.

+
+
); const maskWithClick = ( - + { changeMaskWithClick(false); @@ -39,16 +42,16 @@ export default () => { return ( + changeMaskWithClick(true)}> + Overlay with button + + { changeMask(true); }} > - Overlay with onClick - - - changeMaskWithClick(true)}> - Overlay with button + Overlay with EuiFocusTrap click-to-close {maskOpen ? modal : undefined} {maskWithClickOpen ? maskWithClick : undefined} diff --git a/src-docs/src/views/overlay_mask/overlay_mask_example.js b/src-docs/src/views/overlay_mask/overlay_mask_example.js index ef6a91a4db5..48bd4220216 100644 --- a/src-docs/src/views/overlay_mask/overlay_mask_example.js +++ b/src-docs/src/views/overlay_mask/overlay_mask_example.js @@ -39,8 +39,11 @@ export const OverlayMaskExample = { {' '} to make before choosing to use an overlay. At the very least, you must provide a visible button to close the overlay. You can also - pass an onClick handler to handle closing the - overlay. + nest an{' '} + + EuiFocusTrap + {' '} + to handle closing the overlay.

), diff --git a/src-docs/src/views/overlay_mask/overlay_mask_header.js b/src-docs/src/views/overlay_mask/overlay_mask_header.js index 8cc6053ef44..bb171176fbd 100644 --- a/src-docs/src/views/overlay_mask/overlay_mask_header.js +++ b/src-docs/src/views/overlay_mask/overlay_mask_header.js @@ -19,7 +19,7 @@ export default () => { if (flyOut) { flyout = ( - + diff --git a/src-docs/src/views/overlay_mask/props.tsx b/src-docs/src/views/overlay_mask/props.tsx index 06aaead948b..527de3f4dab 100644 --- a/src-docs/src/views/overlay_mask/props.tsx +++ b/src-docs/src/views/overlay_mask/props.tsx @@ -1,21 +1,7 @@ -import React, { FunctionComponent, ReactNode } from 'react'; +import React, { FunctionComponent } from 'react'; import { CommonProps } from '../../../../src/components/common'; +import { EuiOverlayMaskInterface } from '../../../../src/components/overlay_mask/overlay_mask'; -interface EuiOverlayMaskInterface extends CommonProps { - /** - * Function that applies to clicking the mask itself and not the children - */ - onClick?: () => void; - /** - * ReactNode to render as this component's children - */ - children?: ReactNode; - /** - * Should the mask visually sit above or below the EuiHeader (controlled by z-index) - */ - headerZindexLocation?: 'above' | 'below'; -} - -export const EuiOverlayMaskProps: FunctionComponent = () => ( -
-); +export const EuiOverlayMaskProps: FunctionComponent< + EuiOverlayMaskInterface & CommonProps +> = () =>
; diff --git a/src/components/code/__snapshots__/code_block.test.tsx.snap b/src/components/code/__snapshots__/code_block.test.tsx.snap index 1e6ffd75302..6fe263cbd09 100644 --- a/src/components/code/__snapshots__/code_block.test.tsx.snap +++ b/src/components/code/__snapshots__/code_block.test.tsx.snap @@ -108,6 +108,7 @@ exports[`EuiCodeBlock fullscreen displays content in fullscreen mode 1`] = ` Object { "insert": [Function], "inserted": Object { + "animation-ox4vyo": true, "dsw53p-empty-text-hoverStyles-EuiButtonIcon": true, }, "key": "css", @@ -118,6 +119,27 @@ exports[`EuiCodeBlock fullscreen displays content in fullscreen mode 1`] = ` "_insertTag": [Function], "before": null, "container": + + + + + , - "ctr": 3, + "ctr": 6, "insertionPoint": undefined, "isSpeedy": false, "key": "css", @@ -177,6 +220,27 @@ exports[`EuiCodeBlock fullscreen displays content in fullscreen mode 1`] = ` .css-dsw53p-empty-text-hoverStyles-EuiButtonIcon:hover{background-color:rgba(211,218,230,0.2);} , + , + , + , ], }, } diff --git a/src/components/image/image.test.tsx b/src/components/image/image.test.tsx index 5853d05bb4d..d006f715b37 100644 --- a/src/components/image/image.test.tsx +++ b/src/components/image/image.test.tsx @@ -191,7 +191,11 @@ describe('EuiImage', () => { expect(overlayMask).toBeFalsy(); }); - test('close using overlay mask', () => { + // Clicking the mask to close is now handled by EuiFocusTrap + // and we can't use Enzyme to test this behavior. + // A Cypress test exists in the EuiFocusTrap suite that + // sufficiently covers this behavior + test.skip('close using overlay mask', () => { let overlayMask = document.querySelectorAll( '[data-test-subj=fullScreenOverlayMask]' ); diff --git a/src/components/image/image_fullscreen_wrapper.tsx b/src/components/image/image_fullscreen_wrapper.tsx index 7d2887aa063..e31ed0d3960 100644 --- a/src/components/image/image_fullscreen_wrapper.tsx +++ b/src/components/image/image_fullscreen_wrapper.tsx @@ -65,11 +65,8 @@ export const EuiImageFullScreenWrapper: FunctionComponent ]; return ( - - + + <>
50) { - $backgroundColor: $euiColorLightShade; - } - - background: transparentize($backgroundColor, .2); -} - -.euiBody-hasOverlayMask { - overflow: hidden; -} - -// Handling the z-index based on whether it should be displayed above or below the header -.euiOverlayMask--aboveHeader { - z-index: $euiZMask; -} - -.euiOverlayMask--belowHeader { - z-index: $euiZMaskBelowHeader; -} diff --git a/src/components/overlay_mask/overlay_mask.styles.ts b/src/components/overlay_mask/overlay_mask.styles.ts new file mode 100644 index 00000000000..2590a4ac22a --- /dev/null +++ b/src/components/overlay_mask/overlay_mask.styles.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { css } from '@emotion/react'; +import { logicalCSS, euiAnimFadeIn } from '../../global_styling'; +import { transparentize, UseEuiTheme } from '../../services'; + +export const euiOverlayMaskStyles = ({ euiTheme }: UseEuiTheme) => ({ + euiOverlayMask: css` + .euiOverlayMask { + position: fixed; + ${logicalCSS('top', 0)} + ${logicalCSS('left', 0)} + ${logicalCSS('right', 0)} + ${logicalCSS('bottom', 0)} + display: flex; + align-items: center; + justify-content: center; + ${logicalCSS('padding-bottom', '10vh')}; + animation: ${euiAnimFadeIn} ${euiTheme.animation.fast} ease-in; + background: ${transparentize(euiTheme.colors.ink, 0.5)}; + } + `, + aboveHeader: css` + .euiOverlayMask { + z-index: ${euiTheme.levels.mask}; + } + `, + belowHeader: css` + .euiOverlayMask { + z-index: ${euiTheme.levels.maskBelowHeader}; + // TODO: use size variable when EuiHeader is converted + ${logicalCSS('top', `${euiTheme.base * 3}px`)}; + } + `, +}); + +export const euiOverlayMaskBodyStyles = css` + body { + overflow: hidden; + } +`; diff --git a/src/components/overlay_mask/overlay_mask.tsx b/src/components/overlay_mask/overlay_mask.tsx index 282b69b10f0..2f6fe007ca7 100644 --- a/src/components/overlay_mask/overlay_mask.tsx +++ b/src/components/overlay_mask/overlay_mask.tsx @@ -18,19 +18,19 @@ import React, { ReactNode, Ref, useEffect, - useRef, useState, } from 'react'; -import { createPortal } from 'react-dom'; import classNames from 'classnames'; +import { Global } from '@emotion/react'; import { CommonProps, keysOf } from '../common'; -import { useCombinedRefs } from '../../services'; +import { useCombinedRefs, useEuiTheme } from '../../services'; +import { EuiPortal } from '../portal'; +import { + euiOverlayMaskStyles, + euiOverlayMaskBodyStyles, +} from './overlay_mask.styles'; export interface EuiOverlayMaskInterface { - /** - * Function that applies to clicking the mask itself and not the children - */ - onClick?: () => void; /** * ReactNode to render as this component's content */ @@ -55,86 +55,49 @@ export type EuiOverlayMaskProps = CommonProps & export const EuiOverlayMask: FunctionComponent = ({ className, children, - onClick, headerZindexLocation = 'above', maskRef, css, // TODO: apply custom CSS-in-JS as a className ...rest }) => { - const overlayMaskNode = useRef(); - const combinedMaskRef = useCombinedRefs([overlayMaskNode, maskRef]); - const [isPortalTargetReady, setIsPortalTargetReady] = useState(false); - - useEffect(() => { - document.body.classList.add('euiBody-hasOverlayMask'); - - return () => { - document.body.classList.remove('euiBody-hasOverlayMask'); - }; - }, []); + const [overlayMaskNode, setOverlayMaskNode] = useState( + null + ); + const combinedMaskRef = useCombinedRefs([ + setOverlayMaskNode, + maskRef, + ]); + const euiTheme = useEuiTheme(); + const styles = euiOverlayMaskStyles(euiTheme); + const cssStyles = [ + styles.euiOverlayMask, + styles[`${headerZindexLocation}Header`], + ]; useEffect(() => { - if (typeof document !== 'undefined') { - combinedMaskRef(document.createElement('div')); - } - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - const portalTarget = overlayMaskNode.current; - - if (portalTarget) { - document.body.appendChild(portalTarget); - } - - setIsPortalTargetReady(true); - - return () => { - if (portalTarget) { - document.body.removeChild(portalTarget); - } - }; - }, []); - - useEffect(() => { - if (!overlayMaskNode.current) return; + if (!overlayMaskNode) return; keysOf(rest).forEach((key) => { if (typeof rest[key] !== 'string') { throw new Error( `Unhandled property type. EuiOverlayMask property ${key} is not a string.` ); } - if (overlayMaskNode.current) { - overlayMaskNode.current.setAttribute(key, rest[key]!); + if (overlayMaskNode) { + overlayMaskNode.setAttribute(key, rest[key]!); } }); - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - if (!overlayMaskNode.current) return; - overlayMaskNode.current.className = classNames( - 'euiOverlayMask', - `euiOverlayMask--${headerZindexLocation}Header`, - className - ); - }, [className, headerZindexLocation]); + }, [overlayMaskNode]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { - const portalTarget = overlayMaskNode.current; - if (!portalTarget || !onClick) return; - - const listener = (e: Event) => { - if (e.target === portalTarget) { - onClick(); - } - }; - portalTarget.addEventListener('click', listener); - - return () => { - portalTarget.removeEventListener('click', listener); - }; - }, [onClick]); - - return isPortalTargetReady ? ( - <>{createPortal(children, overlayMaskNode.current!)} - ) : null; + if (!overlayMaskNode) return; + overlayMaskNode.className = classNames('euiOverlayMask', className); + }, [overlayMaskNode, className]); + + return ( + + + + {children} + + ); }; diff --git a/src/global_styling/index.ts b/src/global_styling/index.ts index 5fc347eaf52..fbd28be2dff 100644 --- a/src/global_styling/index.ts +++ b/src/global_styling/index.ts @@ -10,3 +10,4 @@ export * from './reset/global_styles'; export * from './functions'; export * from './variables'; export * from './mixins'; +export * from './utility/animations'; diff --git a/src/global_styling/utility/animations.ts b/src/global_styling/utility/animations.ts new file mode 100644 index 00000000000..bedd2330ecc --- /dev/null +++ b/src/global_styling/utility/animations.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { keyframes } from '@emotion/react'; + +export const euiAnimFadeIn = keyframes` + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +`; diff --git a/src/themes/amsterdam/overrides/_index.scss b/src/themes/amsterdam/overrides/_index.scss index 38444a01edb..8566e195866 100644 --- a/src/themes/amsterdam/overrides/_index.scss +++ b/src/themes/amsterdam/overrides/_index.scss @@ -16,7 +16,6 @@ @import 'markdown_editor'; @import 'modal'; @import 'notification_badge'; -@import 'overlay_mask'; @import 'range'; @import 'range_draggable'; @import 'range_highlight'; diff --git a/src/themes/amsterdam/overrides/_overlay_mask.scss b/src/themes/amsterdam/overrides/_overlay_mask.scss deleted file mode 100644 index 022a18082f6..00000000000 --- a/src/themes/amsterdam/overrides/_overlay_mask.scss +++ /dev/null @@ -1,3 +0,0 @@ -.euiOverlayMask { - background: transparentize($euiColorInk, .5); -} \ No newline at end of file diff --git a/upcoming_changelogs/6090.md b/upcoming_changelogs/6090.md new file mode 100644 index 00000000000..a1c959ae7cb --- /dev/null +++ b/upcoming_changelogs/6090.md @@ -0,0 +1,9 @@ +- Updated `EuiOverlayMask` to use `EuiPortal` + +**Breaking changes** + +- Removed `onClick` prop from `EuiOverlayMask`. Use a nested `EuiFocusTrap` instead. + +**CSS-in-JS conversions** + +- Converted `EuiOverlayMask` to Emotion