Skip to content

Commit

Permalink
[EuiOverlayMask] Prevent duplicate render and allow use in strict mode (
Browse files Browse the repository at this point in the history
#3555)

* convert to functional component; use lifecycle events for DOM element creation

* expose prop types

* fewer updates; implicit event type

* preserve docgeninfo with fragment workaround

* CL

* remove null ref
  • Loading branch information
thompsongl authored Jun 5, 2020
1 parent 3e0ebf2 commit 1bc2b29
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 42 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
## [`master`](https://github.com/elastic/eui/tree/master)

- Added `useEuiTextDiff` react hook utility ([#3288](https://github.com/elastic/eui/pull/3288))
- Converted `EuiOverlayMask` to be a React functional component ([#3555](https://github.com/elastic/eui/pull/3555))

**Bug fixes**

- Fixed `EuiCodeBlockImpl` testenv mock pass-through of `data-test-subj` attribute ([#3560](https://github.com/elastic/eui/pull/3560))
- Fixed DOM element creation issues in `EuiOverlayMask` by using lifecycle methods ([#3555](https://github.com/elastic/eui/pull/3555))

## [`25.0.0`](https://github.com/elastic/eui/tree/v25.0.0)

Expand Down
2 changes: 1 addition & 1 deletion src/components/overlay_mask/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
* under the License.
*/

export { EuiOverlayMask } from './overlay_mask';
export { EuiOverlayMask, EuiOverlayMaskProps } from './overlay_mask';
105 changes: 64 additions & 41 deletions src/components/overlay_mask/overlay_mask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,70 +22,93 @@
* into portals.
*/

import { Component, HTMLAttributes, ReactNode } from 'react';
import React, {
FunctionComponent,
HTMLAttributes,
ReactNode,
useEffect,
useRef,
useState,
} from 'react';
import { createPortal } from 'react-dom';
import classNames from 'classnames';
import { CommonProps, keysOf } from '../common';

export interface EuiOverlayMaskProps {
export interface EuiOverlayMaskInterface {
onClick?: () => void;
children?: ReactNode;
}

export type Props = CommonProps &
export type EuiOverlayMaskProps = CommonProps &
Omit<
Partial<Record<keyof HTMLAttributes<HTMLDivElement>, string>>,
keyof EuiOverlayMaskProps
keyof EuiOverlayMaskInterface
> &
EuiOverlayMaskProps;
EuiOverlayMaskInterface;

export class EuiOverlayMask extends Component<Props> {
private overlayMaskNode?: HTMLDivElement;
export const EuiOverlayMask: FunctionComponent<EuiOverlayMaskProps> = ({
className,
children,
onClick,
...rest
}) => {
const overlayMaskNode = useRef<HTMLDivElement>(document.createElement('div'));
const [isPortalTargetReady, setIsPortalTargetReady] = useState(false);

constructor(props: Props) {
super(props);
useEffect(() => {
document.body.classList.add('euiBody-hasOverlayMask');

return () => {
document.body.classList.remove('euiBody-hasOverlayMask');
};
}, []);

const { className, children, onClick, ...rest } = this.props;
useEffect(() => {
const portalTarget = overlayMaskNode.current;
document.body.appendChild(overlayMaskNode.current);
setIsPortalTargetReady(true);

this.overlayMaskNode = document.createElement('div');
this.overlayMaskNode.className = classNames('euiOverlayMask', className);
if (onClick) {
this.overlayMaskNode.addEventListener(
'click',
(e: React.MouseEvent | MouseEvent) => {
if (e.target === this.overlayMaskNode) {
onClick();
}
}
);
}
return () => {
if (portalTarget) {
document.body.removeChild(portalTarget);
}
};
}, []);

useEffect(() => {
if (!overlayMaskNode.current) return;
keysOf(rest).forEach(key => {
if (typeof rest[key] !== 'string') {
throw new Error(
`Unhandled property type. EuiOverlayMask property ${key} is not a string.`
);
}
this.overlayMaskNode!.setAttribute(key, rest[key]!);
overlayMaskNode.current.setAttribute(key, rest[key]!);
});
}, []); // eslint-disable-line react-hooks/exhaustive-deps

document.body.appendChild(this.overlayMaskNode);
}

componentDidMount() {
document.body.classList.add('euiBody-hasOverlayMask');
}
useEffect(() => {
if (!overlayMaskNode.current) return;
overlayMaskNode.current.className = classNames('euiOverlayMask', className);
}, [className]);

componentWillUnmount() {
document.body.classList.remove('euiBody-hasOverlayMask');
useEffect(() => {
if (!overlayMaskNode.current || !onClick) return;
const portalTarget = overlayMaskNode.current;
overlayMaskNode.current.addEventListener('click', e => {
if (e.target === overlayMaskNode.current) {
onClick();
}
});

if (this.props.onClick) {
this.overlayMaskNode!.removeEventListener('click', this.props.onClick);
}
document.body.removeChild(this.overlayMaskNode!);
this.overlayMaskNode = undefined;
}
return () => {
if (portalTarget && onClick) {
portalTarget.removeEventListener('click', onClick);
}
};
}, [onClick]);

render() {
return createPortal(this.props.children, this.overlayMaskNode!);
}
}
return isPortalTargetReady ? (
<>{createPortal(children, overlayMaskNode.current!)}</>
) : null;
};

0 comments on commit 1bc2b29

Please sign in to comment.