import { Drawer, Popover, type PopoverActions, type PopoverOrigin, type PopoverProps, } from '@mui/material' import { useCallback, useRef, useSyncExternalStore, type CSSProperties, type ReactNode, } from 'react' type ResponsiveDrawerPosition = | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' /** Placement of the panel relative to its anchor (desktop popover only). */ export type ResponsiveDrawerPlacement = 'side' | 'overlay' /** * Popover anchor/transform origins. `'overlay'` pins corner-to-corner on the anchor. */ export function resolveOrigins( position: ResponsiveDrawerPosition, placement: ResponsiveDrawerPlacement, ): { anchorOrigin: PopoverOrigin; transformOrigin: PopoverOrigin } { const isBottom = position.startsWith('bottom') const isRight = position.endsWith('right') const overlay = placement === 'overlay' return { anchorOrigin: { vertical: isBottom ? 'bottom' : 'top', horizontal: overlay ? isRight ? 'right' : 'left' : isRight ? 'left' : 'right', }, transformOrigin: { vertical: isBottom ? 'bottom' : 'top', horizontal: isRight ? 'right' : 'left', }, } } export function ResponsiveDrawer< T extends { collapsed: boolean onChangeCollapsed: (collapsed: boolean) => void position: ResponsiveDrawerPosition slotProps?: PopoverProps['slotProps'] }, >({ ref, children, collapsed, isMobile = false, position, sx, slotProps, container, onChangeCollapsed, modal = true, placement = 'side', }: Required> & Pick & { ref: HTMLElement | null isMobile: boolean children: ReactNode /** * When `false`, the desktop popover is **non-modal**: no backdrop, clicks * pass through to the content behind it (e.g. a map stays interactive), no * focus trap, and neither backdrop-click nor Escape closes it (close it via * `onChangeCollapsed`). Defaults to `true` (standard dismissable popover). * Desktop-only — the mobile drawer is unaffected. */ modal?: boolean /** * Desktop popover placement relative to the anchor: `'side'` (default) opens * beside the trigger; `'overlay'` pins corner-to-corner, sitting on top of * the anchor (e.g. a trigger hidden while the panel is open). Desktop-only. */ placement?: ResponsiveDrawerPlacement }) { // Popover position is frozen on open — re-measure on content resize (callback ref: portal mounts after open). const popoverActions = useRef(null) const observerRef = useRef(null) const observeContent = useCallback((el: HTMLDivElement | null) => { observerRef.current?.disconnect() observerRef.current = null if (el && typeof ResizeObserver !== 'undefined') { const observer = new ResizeObserver(() => popoverActions.current?.updatePosition(), ) observer.observe(el) observerRef.current = observer } }, []) // Popover Paper is auto-height — `%` max-height needs a measured px cap from `container`. const measurableContainer = container && typeof container !== 'function' ? container : null const subscribe = useCallback( (onStoreChange: () => void) => { if (!measurableContainer || typeof ResizeObserver === 'undefined') { return () => undefined } const observer = new ResizeObserver(onStoreChange) observer.observe(measurableContainer) return () => observer.disconnect() }, [measurableContainer], ) const getSnapshot = useCallback( () => measurableContainer?.clientHeight, [measurableContainer], ) const containerHeight = useSyncExternalStore(subscribe, getSnapshot) if (isMobile) { return ( onChangeCollapsed(true)} > {children} ) } const { anchorOrigin, transformOrigin } = resolveOrigins(position, placement) const objectSlotProps = slotProps as | { root?: Record; paper?: { style?: CSSProperties } } | undefined // Capped Paper: disable outer scroll + flex so inner scroll regions can shrink. const paperMaxHeightStyle: CSSProperties | undefined = containerHeight != null && containerHeight > 0 ? { maxHeight: containerHeight * 0.9, overflow: 'hidden', display: 'flex', flexDirection: 'column', } : undefined const contentWrapperStyle: CSSProperties | undefined = paperMaxHeightStyle ? { display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, maxHeight: '100%', overflow: 'hidden', width: '100%', } : undefined const mergedSlotProps: PopoverProps['slotProps'] = modal && !paperMaxHeightStyle ? slotProps : { ...(typeof slotProps === 'object' && slotProps != null ? slotProps : {}), ...(!modal && { root: { style: { pointerEvents: 'none' }, ...objectSlotProps?.root, }, }), paper: { ...objectSlotProps?.paper, style: { ...(!modal && { pointerEvents: 'auto' }), ...paperMaxHeightStyle, ...objectSlotProps?.paper?.style, }, }, } return ( onChangeCollapsed(true) : undefined} hideBackdrop={!modal} disableScrollLock={!modal} disableEscapeKeyDown={!modal} disableAutoFocus={!modal} disableEnforceFocus={!modal} disableRestoreFocus={!modal} anchorOrigin={anchorOrigin} transformOrigin={transformOrigin} slotProps={mergedSlotProps} >
{children}
) }