import React from 'react'; import {cn} from '../../utils/cn'; import {getVisibleRightInset} from './DrawerWidthUtils'; import './Drawer.scss'; const b = cn('ydb-drawer'); export interface DrawerContextType { containerWidth: number; itemContainerRef: React.RefObject | null; rightInset: number; setContainerWidth: React.Dispatch>; setRightInset: React.Dispatch>; } interface DrawerInternalContextType extends DrawerContextType { visibleRightInset: number; } const DrawerContext = React.createContext({ containerWidth: 0, itemContainerRef: null, rightInset: 0, setContainerWidth: () => {}, setRightInset: () => {}, visibleRightInset: 0, }); interface DrawerContextProviderProps { children: React.ReactNode; className?: string; rightInset?: number; onRightInsetChange?: (rightInset: number) => void; } function normalizeRightInset(rightInset: number) { return Number.isFinite(rightInset) ? Math.max(0, rightInset) : 0; } export function isClickInRightInset( event: Pick, itemContainer: HTMLElement | null, rightInset: number, ) { if (rightInset <= 0 || !itemContainer) { return false; } const providerContainer = itemContainer.parentElement; if (!providerContainer) { return false; } const itemRight = itemContainer.getBoundingClientRect().right; const providerRect = providerContainer.getBoundingClientRect(); return ( event.clientX >= itemRight && event.clientX <= providerRect.right && event.clientY >= providerRect.top && event.clientY <= providerRect.bottom ); } export const DrawerContextProvider = ({ children, className, rightInset: controlledRightInset, onRightInsetChange, }: DrawerContextProviderProps) => { const [measuredContainerWidth, setContainerWidth] = React.useState(0); const [rightViewportOverflow, setRightViewportOverflow] = React.useState(0); const [internalRightInset, setInternalRightInset] = React.useState(0); const containerRef = React.useRef(null); const itemContainerRef = React.useRef(null); const rightInset = normalizeRightInset(controlledRightInset ?? internalRightInset); const rightInsetRef = React.useRef(rightInset); rightInsetRef.current = rightInset; const setRightInset = React.useCallback>>( (nextRightInset) => { const nextValue = normalizeRightInset( typeof nextRightInset === 'function' ? nextRightInset(rightInsetRef.current) : nextRightInset, ); rightInsetRef.current = nextValue; if (controlledRightInset === undefined) { setInternalRightInset(nextValue); } onRightInsetChange?.(nextValue); }, [controlledRightInset, onRightInsetChange], ); React.useEffect(() => { if (!containerRef.current) { return undefined; } const updateWidth = () => { if (containerRef.current) { const containerRect = containerRef.current.getBoundingClientRect(); const visibleLeft = Math.max(containerRect.left, 0); const visibleRight = Math.min(containerRect.right, window.innerWidth); const visibleWidth = Math.max(0, visibleRight - visibleLeft); const nextRightViewportOverflow = Math.max(0, containerRect.right - visibleRight); setContainerWidth(visibleWidth || containerRef.current.clientWidth); setRightViewportOverflow(nextRightViewportOverflow); } }; // Set initial width updateWidth(); // Update width on resize const resizeObserver = new ResizeObserver(updateWidth); resizeObserver.observe(containerRef.current); window.addEventListener('resize', updateWidth); return () => { resizeObserver.disconnect(); window.removeEventListener('resize', updateWidth); }; }, []); const visibleRightInset = getVisibleRightInset({rightInset, rightViewportOverflow}); // Memoize the context value to prevent unnecessary re-renders const value = React.useMemo( () => ({ containerWidth: measuredContainerWidth, setContainerWidth, itemContainerRef, rightInset, setRightInset, visibleRightInset, }), [measuredContainerWidth, rightInset, setRightInset, visibleRightInset], ); const itemContainerStyle = React.useMemo(() => { if (rightInset === 0) { return undefined; } return {width: `max(0px, calc(100% - ${rightInset}px))`}; }, [rightInset]); return (
{children} {/* Children styles should not affect drawer container behaviour So we mount it out of children in a separate portal */}
); }; const useDrawerContextValue = () => { const context = React.useContext(DrawerContext); if (context === undefined) { throw Error('useDrawerContext must be used within a DrawerContextProvider'); } return context; }; export const useDrawerContext = (): DrawerContextType => useDrawerContextValue(); export const useDrawerContextInternal = (): DrawerInternalContextType => useDrawerContextValue();