import { ThemeProvider, useTheme } from '@wise/components-theming'; import { clsx } from 'clsx'; import { MouseEvent, ReactNode, useCallback, useEffect, useRef, useState } from 'react'; import { CSSTransition } from 'react-transition-group'; import { addNoScrollClass, removeNoScrollClass, type CommonProps, type PositionBottom, type PositionCenter, type PositionTop, } from '../common'; import { isIosDevice } from '../common/deviceDetection'; import FocusBoundary from '../common/focusBoundary'; import withNextPortal from '../withNextPortal/withNextPortal'; import DimmerManager from './dimmerManager'; export const EXIT_ANIMATION = 350; const dimmerManager = new DimmerManager(); export type DimmerProps = CommonProps & { children?: ReactNode; /** @default false */ disableClickToClose?: boolean; contentPosition?: PositionTop | PositionCenter | PositionBottom; /** @default false */ fadeContentOnEnter?: boolean; /** @default false */ fadeContentOnExit?: boolean; /** @default false */ open?: boolean; /** @default false */ scrollable?: boolean; /** @default false */ transparent?: boolean; onClose?: (event: KeyboardEvent | MouseEvent) => void; onExited?: () => void; }; export const handleTouchMove = (event: Event) => { const isTouchedElementDimmer = (event.target as HTMLDivElement).classList.contains('dimmer'); // disable scroll on iOS devices for Dimmer area // this is because of bug in WebKit https://bugs.webkit.org/show_bug.cgi?id=220908 // note: scrolling still works for children(s) as expected if (isIosDevice() && isTouchedElementDimmer) { event.stopPropagation(); event.preventDefault(); } }; const Dimmer = ({ children, className, disableClickToClose = false, contentPosition, fadeContentOnEnter = false, fadeContentOnExit = false, open = false, scrollable = false, transparent = false, onClose, onExited: handleExited, }: DimmerProps) => { const [hasNotExited, setHasNotExited] = useState(false); const dimmerReference = useRef(null); const closeOnClick = (event: MouseEvent) => { if (event.target === dimmerReference.current) { onClose?.(event); } }; const handleClick = (event: MouseEvent) => { if (disableClickToClose || !onClose) { return; } closeOnClick(event); }; const handleKeyDown = useCallback( (event: KeyboardEvent) => { if (event.key !== 'Escape') { return; } event.stopPropagation(); if (onClose && dimmerReference.current && dimmerManager.isTop(dimmerReference.current)) { onClose(event); } }, [onClose], ); const onEnter = () => { setHasNotExited(true); if (dimmerReference.current) { dimmerManager.add(dimmerReference.current); } }; const onExited = () => { setHasNotExited(false); if (dimmerReference.current) { dimmerManager.remove(dimmerReference.current); } handleExited?.(); }; useEffect(() => { const localReferenceCopy = dimmerReference.current; if (open) { document.addEventListener('keydown', handleKeyDown); localReferenceCopy?.addEventListener('touchmove', handleTouchMove, { passive: true }); } return () => { document.removeEventListener('keydown', handleKeyDown); localReferenceCopy?.removeEventListener('touchmove', handleTouchMove); }; }, [handleKeyDown, open]); return (
{children}
); }; const DimmerWrapper: React.ComponentType<{ open: boolean; hasNotExited: boolean; children: React.ReactNode; }> = ({ open, hasNotExited, children }) => { const { screenMode, theme } = useTheme(); return open || hasNotExited ? ( {children} ) : ( <>{children} ); }; export const DimmerContentWrapper = ({ children, scrollBody, }: { children: React.ReactElement; scrollBody: boolean; }) => { useEffect(() => { if (scrollBody) { addNoScrollClass(); } return () => { if (scrollBody) { removeNoScrollClass(); } }; }, [scrollBody]); return children; }; // Export without the Portal for tests only export { Dimmer }; export default withNextPortal(Dimmer);