import React, { 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 ImageView from './ImageView'; 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({ backgroundColor: 'rgba(0,0,0,0.5)', position: 'absolute', display: 'flex', flexDirection: 'column', flex: 1, top: 0, left: 0, right: 0, bottom: 0, }); 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({ display: 'flex', flexDirection: 'column', backgroundColor: colors.darkBlue, borderRadius: '11px', padding: '16px', }); const ModalAlertIcon = styled.div({ display: 'flex', justifyContent: 'center', marginTop: '8px', }); const ModalAlertButtonContainer = styled.div({ display: 'flex', flexDirection: 'column', marginTop: '18px', }); interface IModalAlertProps { type?: ModalAlertType; iconColor?: string; message?: string; buttons: React.ReactNode[]; children?: React.ReactNode; close?: () => void; } export function ModalAlert(props: IModalAlertProps) { const activeModalContext = useContext(ActiveModalContext); return ; } class ModalAlertWithContext extends React.Component { private element = document.createElement('div'); private modalRef = React.createRef(); constructor(props: IModalAlertProps & IModalContext) { super(props); if (document.activeElement) { props.previousActiveElement.current = document.activeElement as HTMLElement; } } public componentDidMount() { this.props.setActiveModal(true); // The `true` argument specifies that the event should be dispatched in the capture phase. This // makes sure that this component catches the event before the escape hatch. document.addEventListener('keydown', this.handleKeyPress, true); const modalContainer = document.getElementById(MODAL_CONTAINER_ID); if (modalContainer) { modalContainer.appendChild(this.element); this.modalRef.current?.focus(); } else { log.error('Modal container not found when mounting modal'); } } public componentWillUnmount() { this.props.setActiveModal(false); document.removeEventListener('keydown', this.handleKeyPress, true); 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 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 handleKeyPress = (event: KeyboardEvent) => { if (event.key === 'Escape') { event.stopPropagation(); this.props.close?.(); } }; } export const ModalMessage = styled.span({ fontFamily: 'Open Sans', fontSize: '13px', fontWeight: 500, lineHeight: '20px', color: colors.white80, marginTop: '16px', });