UNPKG

@base-ui/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

229 lines (228 loc) 9.14 kB
'use client'; var _DrawerProviderReport, _DrawerProviderReport2; import * as React from 'react'; import { useControlled } from '@base-ui/utils/useControlled'; import { useStableCallback } from '@base-ui/utils/useStableCallback'; import { ownerWindow } from '@base-ui/utils/owner'; import { isAndroid } from '@base-ui/utils/detectBrowser'; import { DrawerRootContext, useDrawerRootContext } from "./DrawerRootContext.js"; import { Dialog } from "../../dialog/index.js"; import { createChangeEventDetails } from "../../utils/createBaseUIEventDetails.js"; import { REASONS } from "../../utils/reasons.js"; import { useDialogRootContext } from "../../dialog/root/DialogRootContext.js"; import { useDrawerProviderContext } from "../provider/DrawerProviderContext.js"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * Groups all parts of the drawer. * Doesn't render its own HTML element. * * Documentation: [Base UI Drawer](https://base-ui.com/react/components/drawer) */ export function DrawerRoot(props) { const { children, open: openProp, defaultOpen = false, onOpenChange, onOpenChangeComplete, disablePointerDismissal = false, modal = true, actionsRef, handle, triggerId: triggerIdProp, defaultTriggerId: defaultTriggerIdProp = null, swipeDirection = 'down', snapToSequentialPoints = false, snapPoints, snapPoint: snapPointProp, defaultSnapPoint, onSnapPointChange: onSnapPointChangeProp } = props; const onSnapPointChange = useStableCallback(onSnapPointChangeProp); const parentDrawerRootContext = useDrawerRootContext(true); const notifyParentSwipeProgressChange = parentDrawerRootContext?.onNestedSwipeProgressChange; const notifyParentFrontmostHeight = parentDrawerRootContext?.onNestedFrontmostHeightChange; const notifyParentSwipingChange = parentDrawerRootContext?.onNestedSwipingChange; const notifyParentHasNestedDrawer = parentDrawerRootContext?.onNestedDrawerPresenceChange; const [popupHeight, setPopupHeight] = React.useState(0); const [frontmostHeight, setFrontmostHeight] = React.useState(0); const [hasNestedDrawer, setHasNestedDrawer] = React.useState(false); const [nestedSwiping, setNestedSwiping] = React.useState(false); const [nestedSwipeProgressStore] = React.useState(createNestedSwipeProgressStore); const resolvedDefaultSnapPoint = defaultSnapPoint !== undefined ? defaultSnapPoint : snapPoints?.[0] ?? null; const isSnapPointControlled = snapPointProp !== undefined; const [activeSnapPoint, setActiveSnapPointUnwrapped] = useControlled({ controlled: snapPointProp, default: resolvedDefaultSnapPoint, name: 'Drawer', state: 'snapPoint' }); const isNestedDrawerOpenRef = React.useRef(false); const setActiveSnapPoint = useStableCallback((nextSnapPoint, eventDetails) => { const resolvedEventDetails = eventDetails ?? createChangeEventDetails(REASONS.none); onSnapPointChange?.(nextSnapPoint, resolvedEventDetails); if (resolvedEventDetails.isCanceled) { return; } setActiveSnapPointUnwrapped(nextSnapPoint); }); const resolvedActiveSnapPoint = React.useMemo(() => { if (isSnapPointControlled) { return activeSnapPoint; } if (!snapPoints || snapPoints.length === 0) { return activeSnapPoint; } if (activeSnapPoint === null || !snapPoints.some(snapPoint => Object.is(snapPoint, activeSnapPoint))) { return resolvedDefaultSnapPoint; } return activeSnapPoint; }, [activeSnapPoint, isSnapPointControlled, resolvedDefaultSnapPoint, snapPoints]); const onPopupHeightChange = useStableCallback(height => { setPopupHeight(height); if (!isNestedDrawerOpenRef.current && height > 0) { setFrontmostHeight(height); } }); const onNestedFrontmostHeightChange = useStableCallback(height => { if (height > 0) { isNestedDrawerOpenRef.current = true; setFrontmostHeight(height); return; } isNestedDrawerOpenRef.current = false; if (popupHeight > 0) { setFrontmostHeight(popupHeight); } }); const onNestedDrawerPresenceChange = useStableCallback(present => { setHasNestedDrawer(present); }); const onNestedSwipeProgressChange = useStableCallback(progress => { nestedSwipeProgressStore.set(progress); notifyParentSwipeProgressChange?.(progress); }); const onNestedSwipingChange = useStableCallback(swiping => { setNestedSwiping(swiping); notifyParentSwipingChange?.(swiping); }); const handleOpenChange = useStableCallback((nextOpen, eventDetails) => { onOpenChange?.(nextOpen, eventDetails); if (eventDetails.isCanceled) { return; } if (!nextOpen && snapPoints && snapPoints.length > 0) { setActiveSnapPoint(resolvedDefaultSnapPoint, createChangeEventDetails(eventDetails.reason, eventDetails.event, eventDetails.trigger)); } }); const contextValue = React.useMemo(() => ({ swipeDirection, snapToSequentialPoints, snapPoints, activeSnapPoint: resolvedActiveSnapPoint, setActiveSnapPoint, frontmostHeight, popupHeight, hasNestedDrawer, nestedSwiping, nestedSwipeProgressStore, onNestedDrawerPresenceChange, onPopupHeightChange, onNestedFrontmostHeightChange, onNestedSwipingChange, onNestedSwipeProgressChange, notifyParentFrontmostHeight, notifyParentSwipingChange, notifyParentSwipeProgressChange, notifyParentHasNestedDrawer }), [resolvedActiveSnapPoint, frontmostHeight, hasNestedDrawer, nestedSwiping, nestedSwipeProgressStore, notifyParentHasNestedDrawer, notifyParentSwipeProgressChange, notifyParentSwipingChange, notifyParentFrontmostHeight, onNestedDrawerPresenceChange, onNestedFrontmostHeightChange, onNestedSwipeProgressChange, onNestedSwipingChange, onPopupHeightChange, popupHeight, setActiveSnapPoint, snapPoints, snapToSequentialPoints, swipeDirection]); const resolvedChildren = typeof children === 'function' ? payload => /*#__PURE__*/_jsxs(React.Fragment, { children: [_DrawerProviderReport || (_DrawerProviderReport = /*#__PURE__*/_jsx(DrawerProviderReporter, {})), children(payload)] }) : /*#__PURE__*/_jsxs(React.Fragment, { children: [_DrawerProviderReport2 || (_DrawerProviderReport2 = /*#__PURE__*/_jsx(DrawerProviderReporter, {})), children] }); return /*#__PURE__*/_jsx(DrawerRootContext.Provider, { value: contextValue, children: /*#__PURE__*/_jsx(Dialog.Root, { open: openProp, defaultOpen: defaultOpen, onOpenChange: handleOpenChange, onOpenChangeComplete: onOpenChangeComplete, disablePointerDismissal: disablePointerDismissal, modal: modal, actionsRef: actionsRef, handle: handle, triggerId: triggerIdProp, defaultTriggerId: defaultTriggerIdProp, children: resolvedChildren }) }); } function createNestedSwipeProgressStore() { let progress = 0; const listeners = new Set(); return { getSnapshot: () => progress, set(nextProgress) { const resolved = Number.isFinite(nextProgress) ? nextProgress : 0; if (resolved === progress) { return; } progress = resolved; listeners.forEach(listener => { listener(); }); }, subscribe(listener) { listeners.add(listener); return () => { listeners.delete(listener); }; } }; } function DrawerProviderReporter() { const drawerId = React.useId(); const providerContext = useDrawerProviderContext(true); const dialogRootContext = useDialogRootContext(false); const open = dialogRootContext.store.useState('open'); const nestedOpenDialogCount = dialogRootContext.store.useState('nestedOpenDialogCount'); const popupElement = dialogRootContext.store.useState('popupElement'); const isTopmost = nestedOpenDialogCount === 0; React.useEffect(() => { if (!providerContext) { return undefined; } return () => { providerContext.removeDrawer(drawerId); }; }, [drawerId, providerContext]); React.useEffect(() => { providerContext?.setDrawerOpen(drawerId, open); }, [drawerId, open, providerContext]); React.useEffect(() => { // CloseWatcher enables the Android back gesture (Chromium-only). // Keep this Android-only for now to avoid interfering with Escape/nesting semantics on desktop due to `useDismiss`. if (!open || !isTopmost || !isAndroid) { return undefined; } const win = ownerWindow(popupElement); const CloseWatcherCtor = win.CloseWatcher; if (!CloseWatcherCtor) { return undefined; } function handleCloseWatcher(event) { if (!dialogRootContext.store.select('open')) { return; } dialogRootContext.store.setOpen(false, createChangeEventDetails(REASONS.closeWatcher, event)); } const closeWatcher = new CloseWatcherCtor(); closeWatcher.addEventListener('close', handleCloseWatcher); return () => { closeWatcher.removeEventListener('close', handleCloseWatcher); closeWatcher.destroy(); }; }, [dialogRootContext.store, isTopmost, open, popupElement]); return null; }