1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
lens/src/renderer/components/dialog/dialog.tsx
Sebastian Malton 33de7cf7e9 Ensure that all mentioned items in extension API are exported
Signed-off-by: Sebastian Malton <sebastian@malton.name>
2022-03-02 09:12:14 -05:00

172 lines
3.8 KiB
TypeScript

/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./dialog.scss";
import React from "react";
import { createPortal } from "react-dom";
import { disposeOnUnmount, observer } from "mobx-react";
import { reaction } from "mobx";
import { Animate } from "../animate";
import { cssNames, noop, stopPropagation } from "../../utils";
import { navigation } from "../../navigation";
// todo: refactor + handle animation-end in props.onClose()?
export interface DialogProps {
className?: string;
isOpen?: boolean;
open?: () => void;
close?: () => void;
onOpen?: () => void;
onClose?: () => void;
modal?: boolean;
pinned?: boolean;
animated?: boolean;
"data-testid"?: string;
}
export interface DialogState {
isOpen: boolean;
}
@observer
export class Dialog extends React.Component<DialogProps, DialogState> {
private contentElem: HTMLElement;
ref = React.createRef<HTMLDivElement>();
static defaultProps: DialogProps = {
isOpen: false,
open: noop,
close: noop,
onOpen: noop,
onClose: noop,
modal: true,
animated: true,
pinned: false,
};
/**
* @internal
*/
public state: DialogState = {
isOpen: this.props.isOpen,
};
get elem(): HTMLElement {
return this.ref.current;
}
get isOpen() {
return this.state.isOpen;
}
componentDidMount() {
if (this.isOpen) {
this.onOpen();
}
disposeOnUnmount(this, [
reaction(() => navigation.toString(), () => this.close()),
]);
}
componentDidUpdate(prevProps: DialogProps) {
const { isOpen } = this.props;
if (isOpen !== prevProps.isOpen) {
this.toggle(isOpen);
}
}
componentWillUnmount() {
if (this.isOpen) this.onClose();
}
toggle(isOpen: boolean) {
if (isOpen) this.open();
else this.close();
}
open() {
requestAnimationFrame(this.onOpen); // wait for render(), bind close-event to this.elem
this.setState({ isOpen: true });
this.props.open();
}
close() {
this.onClose(); // must be first to get access to dialog's content from outside
this.setState({ isOpen: false });
this.props.close();
}
onOpen = () => {
this.props.onOpen();
if (!this.props.pinned) {
if (this.elem) this.elem.addEventListener("click", this.onClickOutside);
// Using document.body target to handle keydown event before Drawer does
document.body.addEventListener("keydown", this.onEscapeKey);
}
};
onClose = () => {
this.props.onClose();
if (!this.props.pinned) {
if (this.elem) this.elem.removeEventListener("click", this.onClickOutside);
document.body.removeEventListener("keydown", this.onEscapeKey);
}
};
onEscapeKey = (evt: KeyboardEvent) => {
const escapeKey = evt.code === "Escape";
if (escapeKey) {
this.close();
evt.stopPropagation();
}
};
onClickOutside = (evt: MouseEvent) => {
const target = evt.target as HTMLElement;
if (!this.contentElem.contains(target)) {
this.close();
evt.stopPropagation();
}
};
render() {
const { modal, animated, pinned, "data-testid": testId } = this.props;
let { className } = this.props;
className = cssNames("Dialog flex center", className, { modal, pinned });
let dialog = (
<div
className={className}
onClick={stopPropagation}
ref={this.ref}
data-testid={testId}
>
<div className="box" ref={e => this.contentElem = e}>
{this.props.children}
</div>
</div>
);
if (animated) {
dialog = (
<Animate enter={this.isOpen} name="opacity-scale">
{dialog}
</Animate>
);
} else if (!this.isOpen) {
return null;
}
return createPortal(dialog, document.body) as React.ReactPortal;
}
}