Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiOverlayMask] Prevent duplicate render and allow use in strict mode #3555

Merged
merged 9 commits into from
Jun 5, 2020
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';
108 changes: 68 additions & 40 deletions src/components/overlay_mask/overlay_mask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,70 +22,98 @@
* into portals.
*/

import { Component, HTMLAttributes, ReactNode } from 'react';
import {
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 | null>(
chandlerprall marked this conversation as resolved.
Show resolved Hide resolved
document.createElement('div')
);
const [isPortalTargetReady, setIsPortalTargetReady] = useState(false);

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

const { className, children, onClick, ...rest } = this.props;
return () => {
document.body.classList.remove('euiBody-hasOverlayMask');
};
}, []);

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();
}
}
);
useEffect(() => {
if (overlayMaskNode.current) {
document.body.appendChild(overlayMaskNode.current);
setIsPortalTargetReady(true);
}

return () => {
if (overlayMaskNode.current) {
document.body.removeChild(overlayMaskNode.current);
overlayMaskNode.current = null;
}
};
}, []);

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]!);
if (overlayMaskNode.current) {
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;
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 (overlayMaskNode.current && onClick) {
overlayMaskNode.current.removeEventListener('click', onClick);
}
};
}, [onClick]);

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