import { CssProps, RefProps, VNode, callUnload, mountInnerComponent } from 'lupine.web'; import { backActionHelper, stopPropagation } from '../lib'; import { MediaQueryMaxWidth, MediaQueryRange } from '../styles'; import { isSafeAreaSupported } from '../lib/support-safe-area'; export type SliderHelperDirection = 'right' | 'left' | 'bottom' | 'top'; export type SliderHelperCloseProps = () => void; export type SliderHelperShowProps = { children: VNode; direction?: SliderHelperDirection; closeEvent?: () => void; closeWhenClickOutside?: boolean; zIndex?: string; maxWidth?: string; className?: string; contentClassName?: string; mountTarget?: HTMLElement; }; export class SliderHelper { static async show({ children, direction = 'right', closeEvent, closeWhenClickOutside = true, zIndex, maxWidth, className = '', contentClassName = '', mountTarget, }: SliderHelperShowProps): Promise { const base = document.createElement('div'); const isHorizontal = direction === 'left' || direction === 'right'; const isFromEnd = direction === 'right' || direction === 'bottom'; const closeClassName = isFromEnd ? 'close-to-end' : 'close-to-start'; let closed = false; const handleClose = () => { if (closed) return; closed = true; closeEvent?.(); ref.current?.classList.remove('show'); ref.current?.classList.add(closeClassName); setTimeout(async () => { await callUnload(base); base.remove(); }, 400); }; const onClickContainer = (event: MouseEvent) => { if (closeWhenClickOutside !== false && event.target === ref.current) { handleClose(); } }; const onClickContent = (event: MouseEvent) => { stopPropagation(event); }; const ref: RefProps = { onLoad: async () => { // Make sure the browser has painted the initial transform state before adding show, // otherwise rapid close/open can skip the transition and display directly. ref.current?.classList.remove('show', 'close-to-end', 'close-to-start'); ref.current?.getBoundingClientRect(); requestAnimationFrame(() => { requestAnimationFrame(() => { ref.current?.classList.add('show'); }); }); }, }; const startTransform = isHorizontal ? direction === 'right' ? 'translateX(100%)' : 'translateX(-100%)' : direction === 'bottom' ? 'translateY(100%)' : 'translateY(-100%)'; const showTransform = 'translate(0, 0)'; const endTransform = isHorizontal ? direction === 'right' ? 'translateX(100%)' : 'translateX(-100%)' : direction === 'bottom' ? 'translateY(100%)' : 'translateY(-100%)'; const oppositeTransform = isHorizontal ? direction === 'right' ? 'translateX(-100%)' : 'translateX(100%)' : direction === 'bottom' ? 'translateY(-100%)' : 'translateY(100%)'; const contentCss: CssProps = isHorizontal ? { top: '0', bottom: '0', width: '100%', maxWidth, [direction]: '0', } : { top: direction === 'top' ? '0' : 'auto', bottom: direction === 'bottom' ? '0' : 'auto', left: 'auto', right: '0', width: '100%', maxWidth, height: '100%', }; const cssContainer: CssProps = { display: 'flex', position: 'fixed', top: '0', left: '0', right: '0', bottom: '0', zIndex: zIndex || 'var(--layer-slider)', overflow: 'hidden', backgroundColor: '#00000000', transition: 'background-color 0.4s ease-in-out', pointerEvents: 'auto', '&.show': { [MediaQueryRange.MobileAbove]: { backgroundColor: 'var(--cover-mask-bg-color)', }, '.slider-helper-content': { transform: showTransform, }, }, '&.close-to-end': { '.slider-helper-content': { transform: endTransform, }, }, '&.close-to-start': { '.slider-helper-content': { transform: oppositeTransform, }, }, '.slider-helper-content': { display: 'flex', flexDirection: 'column', position: 'fixed', boxSizing: 'border-box', overflowX: 'hidden', overflowY: 'hidden', scrollbarWidth: 'none', color: 'var(--primary-color)', backgroundColor: 'var(--primary-bg-color)', boxShadow: 'var(--cover-box-shadow)', transform: startTransform, transition: 'transform 0.4s ease-in-out', // trick: to put two padding-top properties 'padding-top ': isSafeAreaSupported() ? 'constant(safe-area-inset-top)' : '0', 'padding-top': isSafeAreaSupported() ? 'env(safe-area-inset-top)' : '0', ...contentCss, '&::-webkit-scrollbar': { display: 'none', }, }, }; const component = (
{children}
); base.style.position = 'fixed'; base.style.inset = '0'; base.style.zIndex = zIndex || 'var(--layer-slider)'; const host = mountTarget || document.querySelector('.responsive-frame') || document.querySelector('.lupine-root') || document.body; host.appendChild(base); await mountInnerComponent(base, component); return handleClose; } }