import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import styled from 'styled-components'; import { colors } from '../../config.json'; import log from '../../shared/logging'; import { tinyText } from './common-styles'; import CustomScrollbars from './CustomScrollbars'; import ImageView from './ImageView'; import { BackAction } from './KeyboardNavigation'; const MODAL_CONTAINER_ID = 'modal-container'; const ModalContent = styled.div({ position: 'absolute', display: 'flex', flexDirection: 'column', flex: 1, top: 0, left: 0, right: 0, bottom: 0, }); const ModalBackground = styled.div({}, (props: { visible: boolean }) => ({ backgroundColor: props.visible ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0)', backdropFilter: props.visible ? 'blur(1.5px)' : '', position: 'absolute', display: 'flex', flexDirection: 'column', flex: 1, top: 0, left: 0, right: 0, bottom: 0, transition: 'all 150ms ease-out', pointerEvents: props.visible ? 'auto' : 'none', zIndex: 2, })); export const StyledModalContainer = styled.div({ position: 'relative', flex: 1, }); interface IModalContainerProps { children?: React.ReactNode; } interface IModalContext { activeModal: boolean; setActiveModal: (value: boolean) => void; previousActiveElement: React.MutableRefObject; } const noActiveModalContextError = new Error('ActiveModalContext.Provider missing'); const ActiveModalContext = React.createContext({ get activeModal(): boolean { throw noActiveModalContextError; }, setActiveModal(_value) { throw noActiveModalContextError; }, get previousActiveElement(): React.MutableRefObject { throw noActiveModalContextError; }, }); export function ModalContainer(props: IModalContainerProps) { const [activeModal, setActiveModal] = useState(false); const previousActiveElement = useRef(); const contextValue = useMemo( () => ({ activeModal, setActiveModal, previousActiveElement, }), [activeModal], ); useEffect(() => { if (!activeModal) { previousActiveElement.current?.focus(); } }, [activeModal]); return ( {props.children} ); } export enum ModalAlertType { info = 1, caution, warning, } const ModalAlertContainer = styled.div({ display: 'flex', flexDirection: 'column', flex: 1, justifyContent: 'center', padding: '26px 14px 14px', }); const StyledModalAlert = styled.div({}, (props: { visible: boolean; closing: boolean }) => { let transform = ''; if (props.visible && props.closing) { transform = 'scale(80%)'; } else if (!props.visible) { transform = 'translateY(10px) scale(98%)'; } return { display: 'flex', flexDirection: 'column', backgroundColor: colors.darkBlue, borderRadius: '11px', padding: '16px 0 16px 16px', maxHeight: '80vh', opacity: props.visible && !props.closing ? 1 : 0, transform, boxShadow: ' 0px 15px 35px 5px rgba(0,0,0,0.5)', transition: 'all 150ms ease-out', }; }); const StyledCustomScrollbars = styled(CustomScrollbars)({ paddingRight: '16px', }); const ModalAlertIcon = styled.div({ display: 'flex', justifyContent: 'center', marginTop: '8px', }); const ModalAlertButtonContainer = styled.div({ display: 'flex', flexDirection: 'column', marginTop: '18px', marginRight: '16px', }); interface IModalAlertProps { type?: ModalAlertType; iconColor?: string; message?: string; buttons: React.ReactNode[]; children?: React.ReactNode; close?: () => void; } export function ModalAlert(props: IModalAlertProps & { isOpen: boolean }) { const { isOpen, ...otherProps } = props; const activeModalContext = useContext(ActiveModalContext); const [closing, setClosing] = useState(false); const prevIsOpen = useRef(isOpen); const onTransitionEnd = useCallback(() => setClosing(false), []); useEffect(() => { setClosing((closing) => closing || (prevIsOpen.current && !isOpen)); prevIsOpen.current = isOpen; }, [isOpen]); if (!prevIsOpen.current && !isOpen && !closing) { return null; } return ( ); } interface IModalAlertState { visible: boolean; } interface IModalAlertImplProps extends IModalAlertProps, IModalContext { closing: boolean; onTransitionEnd: () => void; } class ModalAlertImpl extends React.Component { public state = { visible: false }; private element = document.createElement('div'); private modalRef = React.createRef(); constructor(props: IModalAlertImplProps) { super(props); if (document.activeElement) { props.previousActiveElement.current = document.activeElement as HTMLElement; } } public componentDidMount() { this.props.setActiveModal(true); const modalContainer = document.getElementById(MODAL_CONTAINER_ID); if (modalContainer) { modalContainer.appendChild(this.element); this.modalRef.current?.focus(); this.setState({ visible: true }); } else { log.error('Modal container not found when mounting modal'); } } public componentWillUnmount() { this.props.setActiveModal(false); const modalContainer = document.getElementById(MODAL_CONTAINER_ID); modalContainer?.removeChild(this.element); } public render() { return ReactDOM.createPortal(this.renderModal(), this.element); } private renderModal() { return ( {this.props.type && ( {this.renderTypeIcon(this.props.type)} )} {this.props.message && {this.props.message}} {this.props.children} {this.props.buttons.map((button, index) => ( {button} ))} ); } private close = () => { this.props.close?.(); }; private renderTypeIcon(type: ModalAlertType) { let source = ''; let color = ''; switch (type) { case ModalAlertType.info: source = 'icon-info'; color = colors.white; break; case ModalAlertType.caution: source = 'icon-alert'; color = colors.white; break; case ModalAlertType.warning: source = 'icon-alert'; color = colors.red; break; } return ( ); } private onTransitionEnd = (event: React.TransitionEvent) => { if (event.target === this.modalRef.current) { this.props.onTransitionEnd(); } }; } export const ModalMessage = styled.span(tinyText, { color: colors.white80, marginTop: '16px', });