@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.
285 lines (282 loc) • 10.4 kB
JavaScript
'use client';
import * as React from 'react';
import { useIsoLayoutEffect } from '@base-ui/utils/useIsoLayoutEffect';
import { useStableCallback } from '@base-ui/utils/useStableCallback';
import { FloatingFocusManager } from "../../floating-ui-react/index.js";
import { useDialogRootContext } from "../../dialog/root/DialogRootContext.js";
import { useRenderElement } from "../../utils/useRenderElement.js";
import { popupStateMapping as baseMapping } from "../../utils/popupStateMapping.js";
import { transitionStatusMapping } from "../../utils/stateAttributesMapping.js";
import { DrawerBackdropCssVars } from "../backdrop/DrawerBackdropCssVars.js";
import { DrawerPopupCssVars } from "./DrawerPopupCssVars.js";
import { DrawerPopupDataAttributes } from "./DrawerPopupDataAttributes.js";
import { useDialogPortalContext } from "../../dialog/portal/DialogPortalContext.js";
import { useOpenChangeComplete } from "../../utils/useOpenChangeComplete.js";
import { COMPOSITE_KEYS } from "../../composite/composite.js";
import { useDrawerRootContext } from "../root/DrawerRootContext.js";
import { useDrawerSnapPoints } from "../root/useDrawerSnapPoints.js";
import { useDrawerViewportContext } from "../viewport/DrawerViewportContext.js";
import { EMPTY_OBJECT } from "../../utils/constants.js";
import { jsx as _jsx } from "react/jsx-runtime";
const stateAttributesMapping = {
...baseMapping,
...transitionStatusMapping,
expanded(value) {
return value ? {
[DrawerPopupDataAttributes.expanded]: ''
} : null;
},
nestedDrawerOpen(value) {
return value ? {
[DrawerPopupDataAttributes.nestedDrawerOpen]: ''
} : null;
},
nestedDrawerSwiping(value) {
return value ? {
[DrawerPopupDataAttributes.nestedDrawerSwiping]: ''
} : null;
},
swipeDirection(value) {
return value ? {
[DrawerPopupDataAttributes.swipeDirection]: value
} : null;
},
swiping(value) {
return value ? {
[DrawerPopupDataAttributes.swiping]: ''
} : null;
}
};
/**
* A container for the drawer contents.
* Renders a `<div>` element.
*
* Documentation: [Base UI Drawer](https://base-ui.com/react/components/drawer)
*/
export const DrawerPopup = /*#__PURE__*/React.forwardRef(function DrawerPopup(componentProps, forwardedRef) {
const {
className,
finalFocus,
initialFocus,
render,
...elementProps
} = componentProps;
const {
store
} = useDialogRootContext();
const {
swipeDirection,
frontmostHeight,
hasNestedDrawer,
nestedSwiping,
nestedSwipeProgressStore,
onPopupHeightChange,
notifyParentFrontmostHeight,
notifyParentHasNestedDrawer
} = useDrawerRootContext();
const descriptionElementId = store.useState('descriptionElementId');
const disablePointerDismissal = store.useState('disablePointerDismissal');
const floatingRootContext = store.useState('floatingRootContext');
const rootPopupProps = store.useState('popupProps');
const modal = store.useState('modal');
const mounted = store.useState('mounted');
const nested = store.useState('nested');
const nestedOpenDialogCount = store.useState('nestedOpenDialogCount');
const transitionStatus = store.useState('transitionStatus');
const open = store.useState('open');
const openMethod = store.useState('openMethod');
const titleElementId = store.useState('titleElementId');
const role = store.useState('role');
const nestedDrawerOpen = nestedOpenDialogCount > 0;
const swipe = useDrawerViewportContext(true);
const swiping = swipe?.swiping ?? false;
const swipeStrength = swipe?.swipeStrength ?? null;
const {
snapPoints,
activeSnapPoint,
activeSnapPointOffset
} = useDrawerSnapPoints();
useDialogPortalContext();
const [popupHeight, setPopupHeight] = React.useState(0);
const popupHeightRef = React.useRef(0);
const measureHeight = useStableCallback(() => {
const popupElement = store.context.popupRef.current;
if (!popupElement) {
return;
}
const offsetHeight = popupElement.offsetHeight;
// Only skip while the element is still actually stretched beyond its last measured height.
if (popupHeightRef.current > 0 && frontmostHeight > popupHeightRef.current && offsetHeight > popupHeightRef.current) {
return;
}
const keepHeightWhileNested = popupHeightRef.current > 0 && hasNestedDrawer;
if (keepHeightWhileNested) {
const oldHeight = popupHeightRef.current;
setPopupHeight(oldHeight);
onPopupHeightChange(oldHeight);
return;
}
const scrollHeight = popupElement.scrollHeight;
const nextHeight = scrollHeight > 0 ? Math.min(offsetHeight, scrollHeight) : offsetHeight;
if (nextHeight === popupHeightRef.current) {
return;
}
popupHeightRef.current = nextHeight;
setPopupHeight(nextHeight);
onPopupHeightChange(nextHeight);
});
useIsoLayoutEffect(() => {
if (!mounted) {
popupHeightRef.current = 0;
setPopupHeight(0);
onPopupHeightChange(0);
return undefined;
}
const popupElement = store.context.popupRef.current;
if (!popupElement) {
return undefined;
}
measureHeight();
if (typeof ResizeObserver !== 'function') {
return undefined;
}
const resizeObserver = new ResizeObserver(measureHeight);
resizeObserver.observe(popupElement);
return () => {
resizeObserver.disconnect();
};
}, [measureHeight, mounted, nestedDrawerOpen, onPopupHeightChange, store.context.popupRef]);
useIsoLayoutEffect(() => {
const popupRef = store.context.popupRef;
const syncNestedSwipeProgress = () => {
const popupElement = popupRef.current;
if (!popupElement) {
return;
}
const progress = nestedSwipeProgressStore.getSnapshot();
if (progress > 0) {
popupElement.style.setProperty(DrawerBackdropCssVars.swipeProgress, `${progress}`);
} else {
popupElement.style.setProperty(DrawerBackdropCssVars.swipeProgress, '0');
}
};
syncNestedSwipeProgress();
const unsubscribe = nestedSwipeProgressStore.subscribe(syncNestedSwipeProgress);
return () => {
unsubscribe();
const popupElement = popupRef.current;
if (popupElement) {
popupElement.style.setProperty(DrawerBackdropCssVars.swipeProgress, '0');
}
};
}, [nestedSwipeProgressStore, store.context.popupRef]);
React.useEffect(() => {
if (!open) {
return undefined;
}
notifyParentFrontmostHeight?.(frontmostHeight);
return () => {
notifyParentFrontmostHeight?.(0);
};
}, [frontmostHeight, open, notifyParentFrontmostHeight]);
React.useEffect(() => {
if (!notifyParentHasNestedDrawer) {
return undefined;
}
const present = open || transitionStatus === 'ending';
notifyParentHasNestedDrawer(present);
return () => {
notifyParentHasNestedDrawer(false);
};
}, [notifyParentHasNestedDrawer, open, transitionStatus]);
useOpenChangeComplete({
open,
ref: store.context.popupRef,
onComplete() {
if (open) {
store.context.onOpenChangeComplete?.(true);
}
}
});
const resolvedInitialFocus = initialFocus === undefined ? store.context.popupRef : initialFocus;
const state = {
open,
nested,
transitionStatus,
expanded: activeSnapPoint === 1,
nestedDrawerOpen,
nestedDrawerSwiping: nestedSwiping,
swipeDirection,
swiping
};
let popupHeightCssVarValue;
const shouldUseAutoHeight = !hasNestedDrawer && transitionStatus !== 'ending';
if (popupHeight && !shouldUseAutoHeight) {
popupHeightCssVarValue = `${popupHeight}px`;
}
const shouldApplySnapPoints = snapPoints && snapPoints.length > 0 && (swipeDirection === 'down' || swipeDirection === 'up');
let snapPointOffsetValue = null;
if (shouldApplySnapPoints && activeSnapPointOffset !== null) {
snapPointOffsetValue = swipeDirection === 'up' ? -activeSnapPointOffset : activeSnapPointOffset;
}
let dragStyles = swipe ? swipe.getDragStyles() : EMPTY_OBJECT;
if (shouldApplySnapPoints && swipeDirection === 'down') {
const baseOffset = activeSnapPointOffset ?? 0;
const movementValue = Number.parseFloat(String(dragStyles[DrawerPopupCssVars.swipeMovementY] ?? 0));
const nextOffset = Number.isFinite(movementValue) ? baseOffset + movementValue : baseOffset;
const shouldDamp = nextOffset < 0;
if (swiping && shouldDamp && Number.isFinite(movementValue)) {
const overshoot = Math.abs(nextOffset);
const dampedOffset = -Math.sqrt(overshoot);
const dampedMovement = dampedOffset - baseOffset;
dragStyles = {
...dragStyles,
transform: undefined,
[DrawerPopupCssVars.swipeMovementY]: `${dampedMovement}px`
};
} else {
dragStyles = {
...dragStyles,
transform: undefined
};
}
}
const element = useRenderElement('div', componentProps, {
state,
props: [rootPopupProps, {
'aria-labelledby': titleElementId,
'aria-describedby': descriptionElementId,
role,
tabIndex: -1,
hidden: !mounted,
onKeyDown(event) {
if (COMPOSITE_KEYS.has(event.key)) {
event.stopPropagation();
}
},
style: {
...dragStyles,
[DrawerBackdropCssVars.swipeProgress]: '0',
[DrawerPopupCssVars.nestedDrawers]: nestedOpenDialogCount,
[DrawerPopupCssVars.height]: popupHeightCssVarValue,
[DrawerPopupCssVars.snapPointOffset]: typeof snapPointOffsetValue === 'number' ? `${snapPointOffsetValue}px` : '0px',
[DrawerPopupCssVars.frontmostHeight]: frontmostHeight ? `${frontmostHeight}px` : undefined,
[DrawerPopupCssVars.swipeStrength]: typeof swipeStrength === 'number' && Number.isFinite(swipeStrength) && swipeStrength > 0 ? `${swipeStrength}` : '1'
}
}, elementProps],
ref: [forwardedRef, store.context.popupRef, store.useStateSetter('popupElement')],
stateAttributesMapping
});
return /*#__PURE__*/_jsx(FloatingFocusManager, {
context: floatingRootContext,
openInteractionType: openMethod,
disabled: !mounted,
closeOnFocusOut: !disablePointerDismissal,
initialFocus: resolvedInitialFocus,
returnFocus: finalFocus,
modal: modal !== false,
restoreFocus: "popup",
children: element
});
});
if (process.env.NODE_ENV !== "production") DrawerPopup.displayName = "DrawerPopup";