insomnia/packages/insomnia-app/app/ui/components/base/modal.tsx
2021-12-15 23:58:00 -05:00

195 lines
4.5 KiB
TypeScript

import { autoBindMethodsForReact } from 'class-autobind-decorator';
import classnames from 'classnames';
import React, { CSSProperties, PureComponent, ReactNode } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import { hotKeyRefs } from '../../../common/hotkeys';
import { pressedHotKey } from '../../../common/hotkeys-listener';
import { KeydownBinder } from '../keydown-binder';
// Keep global z-index reference so that every modal will
// appear over top of an existing one.
let globalZIndex = 1000;
export interface ModalProps {
centered?: boolean;
tall?: boolean;
wide?: boolean;
skinny?: boolean;
noEscape?: boolean;
dontFocus?: boolean;
closeOnKeyCodes?: any[];
onShow?: Function;
onHide?: Function;
onCancel?: Function;
onKeyDown?: Function;
freshState?: boolean;
children?: ReactNode;
className?: string;
}
interface State {
open: boolean;
forceRefreshCounter: number;
zIndex: number;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
export class Modal extends PureComponent<ModalProps, State> {
onHide: Function | null = null;
_node: HTMLDivElement | null = null;
state: State = {
open: false,
forceRefreshCounter: 0,
zIndex: globalZIndex,
};
async _handleKeyDown(event: KeyboardEvent) {
if (!this.state.open) {
return;
}
this.props.onKeyDown?.(event);
// Don't check for close keys if we don't want them
if (this.props.noEscape) {
return;
}
const closeOnKeyCodes = this.props.closeOnKeyCodes || [];
const pressedEscape = await pressedHotKey(event, hotKeyRefs.CLOSE_MODAL);
const pressedCloseButton = closeOnKeyCodes.find(c => c === event.keyCode);
// Pressed escape
if (pressedEscape || pressedCloseButton) {
event.preventDefault();
this.hide();
this.props.onCancel?.();
}
}
_handleClick(e) {
// Don't check for close keys if we don't want them
if (this.props.noEscape) {
return;
}
// Did we click a close button. Let's check a few parent nodes up as well
// because some buttons might have nested elements. Maybe there is a better
// way to check this?
let target = e.target;
let shouldHide = false;
for (let i = 0; i < 5; i++) {
if (target instanceof HTMLElement && target.hasAttribute('data-close-modal')) {
shouldHide = true;
break;
}
target = target.parentNode;
}
if (shouldHide) {
this.hide();
this.props.onCancel?.();
}
}
_setModalRef(n: HTMLDivElement) {
this._node = n;
}
show(options?: ModalProps) {
const { freshState } = this.props;
const { forceRefreshCounter } = this.state;
this.setState({
open: true,
zIndex: globalZIndex++,
forceRefreshCounter: forceRefreshCounter + (freshState ? 1 : 0),
});
this.props.onShow?.();
if (this.props.dontFocus) {
return;
}
// Allow instance-based onHide method
this.onHide = options?.onHide ?? null;
setTimeout(() => this._node?.focus());
}
toggle() {
if (this.state.open) {
this.hide();
} else {
this.show();
}
}
isOpen() {
return this.state.open;
}
hide() {
this.setState({
open: false,
});
this.props.onHide?.();
this.onHide?.();
}
render() {
const { tall, wide, skinny, noEscape, className, children, centered } = this.props;
const { open, zIndex, forceRefreshCounter } = this.state;
if (!open) {
return null;
}
const classes = classnames(
'modal',
'theme--dialog',
className,
{
'modal--fixed-height': tall,
},
{
'modal--noescape': noEscape,
},
{
'modal--wide': wide,
},
{
'modal--skinny': skinny,
},
);
const styles: CSSProperties = {};
if (open) {
styles.zIndex = zIndex;
}
return (
<KeydownBinder stopMetaPropagation scoped onKeydown={this._handleKeyDown}>
<div
ref={this._setModalRef}
tabIndex={-1}
className={classes}
style={styles}
aria-hidden={!open}
onClick={this._handleClick}
>
<div className="modal__backdrop overlay theme--transparent-overlay" data-close-modal />
<div className={classnames('modal__content__wrapper', { 'modal--centered': centered })}>
<div className="modal__content" key={forceRefreshCounter}>
{children}
</div>
</div>
</div>
</KeydownBinder>
);
}
}