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.

873 lines (867 loc) 35.8 kB
"use strict"; 'use client'; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.DrawerViewport = void 0; var React = _interopRequireWildcard(require("react")); var ReactDOM = _interopRequireWildcard(require("react-dom")); var _dom = require("@floating-ui/utils/dom"); var _owner = require("@base-ui/utils/owner"); var _useStableCallback = require("@base-ui/utils/useStableCallback"); var _DialogRootContext = require("../../dialog/root/DialogRootContext"); var _DialogViewport = require("../../dialog/viewport/DialogViewport"); var _mergeProps = require("../../merge-props"); var _DrawerRootContext = require("../root/DrawerRootContext"); var _useDrawerSnapPoints = require("../root/useDrawerSnapPoints"); var _DrawerProviderContext = require("../provider/DrawerProviderContext"); var _clamp = require("../../utils/clamp"); var _useSwipeDismiss = require("../../utils/useSwipeDismiss"); var _DrawerPopupCssVars = require("../popup/DrawerPopupCssVars"); var _DrawerPopupDataAttributes = require("../popup/DrawerPopupDataAttributes"); var _DrawerBackdropCssVars = require("../backdrop/DrawerBackdropCssVars"); var _reasons = require("../../utils/reasons"); var _createBaseUIEventDetails = require("../../utils/createBaseUIEventDetails"); var _utils = require("../../floating-ui-react/utils"); var _DrawerViewportContext = require("./DrawerViewportContext"); var _stateAttributesMapping = require("../../utils/stateAttributesMapping"); var _scrollable = require("../../utils/scrollable"); var _jsxRuntime = require("react/jsx-runtime"); const MIN_SWIPE_THRESHOLD = 10; const FAST_SWIPE_VELOCITY = 0.5; const SNAP_VELOCITY_THRESHOLD = 0.5; const SNAP_VELOCITY_MULTIPLIER = 300; const MAX_SNAP_VELOCITY = 4; const MIN_SWIPE_RELEASE_VELOCITY = 0.2; const MAX_SWIPE_RELEASE_VELOCITY = 4; const MIN_SWIPE_RELEASE_DURATION_MS = 80; const MAX_SWIPE_RELEASE_DURATION_MS = 360; const MIN_SWIPE_RELEASE_SCALAR = 0.1; const MAX_SWIPE_RELEASE_SCALAR = 1; /** * A positioning container for the drawer popup that can be made scrollable. * Renders a `<div>` element. * * Documentation: [Base UI Drawer](https://base-ui.com/react/components/drawer) */ const DrawerViewport = exports.DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(props, forwardedRef) { const { className, render, children, ...elementProps } = props; const { store } = (0, _DialogRootContext.useDialogRootContext)(); const { swipeDirection, notifyParentSwipingChange, notifyParentSwipeProgressChange, frontmostHeight, snapToSequentialPoints } = (0, _DrawerRootContext.useDrawerRootContext)(); const providerContext = (0, _DrawerProviderContext.useDrawerProviderContext)(true); const visualStateStore = providerContext?.visualStateStore; const open = store.useState('open'); const mounted = store.useState('mounted'); const nested = store.useState('nested'); const nestedOpenDialogCount = store.useState('nestedOpenDialogCount'); const viewportElement = store.useState('viewportElement'); const popupElementState = store.useState('popupElement'); const nestedDrawerOpen = nestedOpenDialogCount > 0; const scrollAxis = swipeDirection === 'left' || swipeDirection === 'right' ? 'horizontal' : 'vertical'; const { snapPoints, resolvedSnapPoints, activeSnapPoint, activeSnapPointOffset, setActiveSnapPoint, popupHeight } = (0, _useDrawerSnapPoints.useDrawerSnapPoints)(); const [swipeRelease, setSwipeRelease] = React.useState(null); const pendingSwipeCloseSnapPointRef = React.useRef(undefined); const resetSwipeRef = React.useRef(null); const nestedSwipeActiveRef = React.useRef(false); const lastPointerTypeRef = React.useRef(''); const ignoreNextTouchStartFromPenRef = React.useRef(false); const touchScrollStateRef = React.useRef(null); const snapPointRange = React.useMemo(() => { if (!snapPoints || snapPoints.length < 2) { return null; } if (swipeDirection !== 'down' && swipeDirection !== 'up') { return null; } if (resolvedSnapPoints.length < 2) { return null; } const offsets = resolvedSnapPoints.map(point => point.offset).filter(offset => Number.isFinite(offset)).sort((a, b) => a - b); if (offsets.length < 2) { return null; } const minOffset = offsets[0]; const nextOffset = offsets[1]; const maxOffset = offsets[offsets.length - 1]; let range = nextOffset - minOffset; if (!Number.isFinite(range) || range <= 0) { const fallbackRange = maxOffset - minOffset; if (!Number.isFinite(fallbackRange) || fallbackRange <= 0) { return null; } range = fallbackRange; } return { minOffset, range }; }, [resolvedSnapPoints, snapPoints, swipeDirection]); const snapPointProgress = React.useMemo(() => { if (!snapPointRange || activeSnapPointOffset === null) { return null; } return (0, _clamp.clamp)((activeSnapPointOffset - snapPointRange.minOffset) / snapPointRange.range, 0, 1); }, [activeSnapPointOffset, snapPointRange]); const swipeDirections = React.useMemo(() => { if (snapPoints && snapPoints.length > 0 && (swipeDirection === 'down' || swipeDirection === 'up')) { return swipeDirection === 'down' ? ['down', 'up'] : ['up', 'down']; } return [swipeDirection]; }, [snapPoints, swipeDirection]); const setSwipeDismissed = (0, _useStableCallback.useStableCallback)(dismissed => { setSwipeDismissedElements(store.context.popupRef.current, store.context.backdropRef.current, dismissed); }); const clearSwipeRelease = (0, _useStableCallback.useStableCallback)(() => { setSwipeDismissed(false); store.context.popupRef.current?.removeAttribute(_stateAttributesMapping.TransitionStatusDataAttributes.endingStyle); setSwipeRelease(null); }); const applySwipeProgress = (0, _useStableCallback.useStableCallback)(({ resolvedProgress, shouldTrackProgress, notifyParent }) => { const isActive = open && !nested && shouldTrackProgress; const swipeProgress = isActive ? resolvedProgress : 0; if (notifyParent && notifyParentSwipeProgressChange) { const nestedSwipeProgress = open && shouldTrackProgress ? resolvedProgress : 0; notifyParentSwipeProgressChange(nestedSwipeProgress); } visualStateStore?.set({ swipeProgress, frontmostHeight: swipeProgress > 0 ? frontmostHeight : 0 }); const backdropElement = store.context.backdropRef.current; if (!backdropElement) { return; } if (!isActive || swipeProgress <= 0) { backdropElement.style.setProperty(_DrawerBackdropCssVars.DrawerBackdropCssVars.swipeProgress, '0'); backdropElement.style.removeProperty(_DrawerPopupCssVars.DrawerPopupCssVars.height); return; } backdropElement.style.setProperty(_DrawerBackdropCssVars.DrawerBackdropCssVars.swipeProgress, `${swipeProgress}`); if (frontmostHeight > 0) { backdropElement.style.setProperty(_DrawerPopupCssVars.DrawerPopupCssVars.height, `${frontmostHeight}px`); } else { backdropElement.style.removeProperty(_DrawerPopupCssVars.DrawerPopupCssVars.height); } }); function resolveSwipeRelease({ direction, deltaX, deltaY, velocityX, velocityY, releaseVelocityX, releaseVelocityY }) { if (!direction) { return null; } const popupElement = store.context.popupRef.current; if (!popupElement) { return null; } const size = direction === 'left' || direction === 'right' ? popupElement.offsetWidth : popupElement.offsetHeight; if (!Number.isFinite(size) || size <= 0) { return null; } const axisDelta = direction === 'left' || direction === 'right' ? deltaX : deltaY; const snapPointBaseOffset = snapPoints && snapPoints.length > 0 ? activeSnapPointOffset ?? 0 : 0; let baseOffset = 0; if (direction === 'down') { baseOffset = snapPointBaseOffset; } else if (direction === 'up') { baseOffset = -snapPointBaseOffset; } const translation = baseOffset + axisDelta; const translationAlongDirection = direction === 'left' || direction === 'up' ? -translation : translation; const remainingDistance = Math.max(0, size - translationAlongDirection); if (!Number.isFinite(remainingDistance) || remainingDistance <= 0) { return null; } const axisVelocity = direction === 'left' || direction === 'right' ? releaseVelocityX : releaseVelocityY; const fallbackVelocity = direction === 'left' || direction === 'right' ? velocityX : velocityY; const resolvedVelocity = Math.abs(axisVelocity) > 0 && Number.isFinite(axisVelocity) ? axisVelocity : fallbackVelocity; const directionalVelocity = direction === 'left' || direction === 'up' ? -resolvedVelocity : resolvedVelocity; if (!Number.isFinite(directionalVelocity) || directionalVelocity <= MIN_SWIPE_RELEASE_VELOCITY) { return null; } const clampedVelocity = (0, _clamp.clamp)(directionalVelocity, MIN_SWIPE_RELEASE_VELOCITY, MAX_SWIPE_RELEASE_VELOCITY); const durationMs = (0, _clamp.clamp)(remainingDistance / clampedVelocity, MIN_SWIPE_RELEASE_DURATION_MS, MAX_SWIPE_RELEASE_DURATION_MS); if (!Number.isFinite(durationMs)) { return null; } const normalizedDuration = (durationMs - MIN_SWIPE_RELEASE_DURATION_MS) / (MAX_SWIPE_RELEASE_DURATION_MS - MIN_SWIPE_RELEASE_DURATION_MS); const durationScalar = (0, _clamp.clamp)(MIN_SWIPE_RELEASE_SCALAR + normalizedDuration * (MAX_SWIPE_RELEASE_SCALAR - MIN_SWIPE_RELEASE_SCALAR), MIN_SWIPE_RELEASE_SCALAR, MAX_SWIPE_RELEASE_SCALAR); if (!Number.isFinite(durationScalar) || durationScalar <= 0) { return null; } return durationScalar; } function updateNestedSwipeActive(details) { if (nestedSwipeActiveRef.current || !details) { return; } const direction = details.direction ?? swipeDirection; const delta = direction === 'left' || direction === 'right' ? details.deltaX : details.deltaY; if (!Number.isFinite(delta) || Math.abs(delta) < MIN_SWIPE_THRESHOLD) { return; } nestedSwipeActiveRef.current = true; notifyParentSwipingChange?.(true); } const swipe = (0, _useSwipeDismiss.useSwipeDismiss)({ enabled: mounted && !nestedDrawerOpen, directions: swipeDirections, elementRef: store.context.popupRef, ignoreSelectorWhenTouch: false, ignoreScrollableAncestors: true, movementCssVars: { x: _DrawerPopupCssVars.DrawerPopupCssVars.swipeMovementX, y: _DrawerPopupCssVars.DrawerPopupCssVars.swipeMovementY }, onSwipeStart(event) { if ('touches' in event || 'pointerType' in event && event.pointerType === 'touch') { return; } const popupElement = store.context.popupRef.current; if (!popupElement) { return; } const doc = (0, _owner.ownerDocument)(popupElement); const selection = doc.getSelection?.(); if (!selection || selection.isCollapsed) { return; } const anchorElement = (0, _dom.isElement)(selection.anchorNode) ? selection.anchorNode : selection.anchorNode?.parentElement; const focusElement = (0, _dom.isElement)(selection.focusNode) ? selection.focusNode : selection.focusNode?.parentElement; if (!(0, _utils.contains)(popupElement, anchorElement) && !(0, _utils.contains)(popupElement, focusElement)) { return; } selection.removeAllRanges(); }, onSwipingChange(swiping) { setBackdropSwipingAttribute(store.context.backdropRef.current, swiping); if (!swiping) { nestedSwipeActiveRef.current = false; notifyParentSwipingChange?.(false); } }, swipeThreshold({ element, direction }) { return getBaseSwipeThreshold(element, direction); }, canStart(position) { const popupElement = store.context.popupRef.current; if (!popupElement) { return false; } const doc = popupElement.ownerDocument; const elementAtPoint = typeof doc.elementFromPoint === 'function' ? doc.elementFromPoint(position.x, position.y) : null; return !!(elementAtPoint && (0, _utils.contains)(popupElement, elementAtPoint)); }, onProgress(progress, details) { updateNestedSwipeActive(details); const currentDirection = details?.direction ?? swipe.swipeDirection; const isDismissSwipe = currentDirection === undefined || currentDirection === swipeDirection; const hasSnapPoints = Boolean(snapPoints && snapPoints.length > 0); const isVerticalSwipe = swipeDirection === 'down' || swipeDirection === 'up'; const shouldTrackProgress = hasSnapPoints && isVerticalSwipe || !hasSnapPoints || swipeDirection === 'left' || swipeDirection === 'right' || isDismissSwipe; let resolvedProgress = progress; if (snapPointRange && popupHeight > 0) { if (details && Number.isFinite(details.deltaY)) { const baseOffset = activeSnapPointOffset ?? snapPointRange.minOffset; const nextOffset = (0, _clamp.clamp)(baseOffset + details.deltaY, 0, popupHeight); resolvedProgress = (0, _clamp.clamp)((nextOffset - snapPointRange.minOffset) / snapPointRange.range, 0, 1); } else if (snapPointProgress !== null) { resolvedProgress = snapPointProgress; } else if (currentDirection === 'down' || currentDirection === 'up') { const displacement = progress * popupHeight; const baseOffset = activeSnapPointOffset ?? snapPointRange.minOffset; const nextOffset = currentDirection === 'down' ? baseOffset + displacement : baseOffset - displacement; resolvedProgress = (0, _clamp.clamp)((nextOffset - snapPointRange.minOffset) / snapPointRange.range, 0, 1); } } applySwipeProgress({ resolvedProgress, shouldTrackProgress, notifyParent: true }); }, onRelease({ event, deltaX, deltaY, direction, velocityX, velocityY, releaseVelocityX, releaseVelocityY }) { const swipeReleasePayload = { deltaX, deltaY, velocityX, velocityY, releaseVelocityX, releaseVelocityY }; function startSwipeRelease(resolvedDirection) { // Start ending transition styles earlier and synchronously to prevent a period where // the popup appears stuck on release before the actual closing animation starts. const popupElement = store.context.popupRef.current; if (!popupElement) { return; } notifyParentSwipingChange?.(false); setSwipeDismissed(true); popupElement.style.removeProperty('transition'); popupElement.setAttribute(_stateAttributesMapping.TransitionStatusDataAttributes.endingStyle, ''); ReactDOM.flushSync(() => { setSwipeRelease(resolveSwipeRelease({ direction: resolvedDirection, ...swipeReleasePayload })); }); } if (!snapPoints || snapPoints.length === 0) { if (!direction) { clearSwipeRelease(); return undefined; } const element = store.context.popupRef.current; if (!element) { clearSwipeRelease(); return undefined; } const baseThreshold = getBaseSwipeThreshold(element, direction); const delta = direction === 'left' || direction === 'right' ? deltaX : deltaY; if (!Number.isFinite(delta)) { clearSwipeRelease(); return undefined; } const directionalDelta = direction === 'left' || direction === 'up' ? -delta : delta; if (directionalDelta <= 0) { clearSwipeRelease(); return false; } const velocity = direction === 'left' || direction === 'right' ? velocityX : velocityY; const directionalVelocity = direction === 'left' || direction === 'up' ? -velocity : velocity; if (directionalVelocity >= FAST_SWIPE_VELOCITY && directionalDelta > 0) { startSwipeRelease(direction); return true; } const shouldClose = directionalDelta > baseThreshold; if (shouldClose) { startSwipeRelease(direction); } else { clearSwipeRelease(); } return shouldClose; } if (swipeDirection !== 'down' && swipeDirection !== 'up') { clearSwipeRelease(); return undefined; } if (!popupHeight || resolvedSnapPoints.length === 0) { clearSwipeRelease(); return undefined; } const dragDelta = swipeDirection === 'down' ? deltaY : -deltaY; if (!Number.isFinite(dragDelta)) { clearSwipeRelease(); return undefined; } const dragDirection = Math.sign(dragDelta); const releaseDirectionalVelocity = swipeDirection === 'down' ? releaseVelocityY : -releaseVelocityY; const fallbackDirectionalVelocity = swipeDirection === 'down' ? velocityY : -velocityY; let resolvedDirectionalVelocity = Number.isFinite(releaseDirectionalVelocity) ? releaseDirectionalVelocity : fallbackDirectionalVelocity; if (dragDirection !== 0 && Math.abs(dragDelta) >= MIN_SWIPE_THRESHOLD && Number.isFinite(resolvedDirectionalVelocity)) { const velocityDirection = Math.sign(resolvedDirectionalVelocity); if (velocityDirection !== 0 && velocityDirection !== dragDirection) { // Ignore touch reversals that would otherwise flip the snap decision. resolvedDirectionalVelocity = fallbackDirectionalVelocity; } } const currentOffset = activeSnapPointOffset ?? 0; const dragTargetOffset = (0, _clamp.clamp)(currentOffset + dragDelta, 0, popupHeight); const velocityOffset = Number.isFinite(resolvedDirectionalVelocity) && Math.abs(resolvedDirectionalVelocity) >= SNAP_VELOCITY_THRESHOLD ? (0, _clamp.clamp)(resolvedDirectionalVelocity, -MAX_SNAP_VELOCITY, MAX_SNAP_VELOCITY) * SNAP_VELOCITY_MULTIPLIER : 0; const targetOffset = snapToSequentialPoints ? dragTargetOffset : (0, _clamp.clamp)(dragTargetOffset + velocityOffset, 0, popupHeight); const snapPointEventDetails = (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.swipe, event); const closeFromSnapPoints = () => { pendingSwipeCloseSnapPointRef.current = activeSnapPoint; setActiveSnapPoint?.(null, snapPointEventDetails); startSwipeRelease(swipeDirection); return true; }; if (snapToSequentialPoints) { const orderedSnapPoints = [...resolvedSnapPoints].sort((first, second) => first.offset - second.offset); if (orderedSnapPoints.length === 0) { clearSwipeRelease(); return false; } let currentIndex = 0; let closestDistance = Math.abs(currentOffset - orderedSnapPoints[0].offset); for (let index = 1; index < orderedSnapPoints.length; index += 1) { const distance = Math.abs(currentOffset - orderedSnapPoints[index].offset); if (distance < closestDistance) { closestDistance = distance; currentIndex = index; } } let targetSnapPoint = orderedSnapPoints[0]; closestDistance = Math.abs(targetOffset - targetSnapPoint.offset); for (const snapPoint of orderedSnapPoints) { const distance = Math.abs(targetOffset - snapPoint.offset); if (distance < closestDistance) { closestDistance = distance; targetSnapPoint = snapPoint; } } const velocityDirection = Math.sign(resolvedDirectionalVelocity); const shouldAdvance = dragDirection !== 0 && velocityDirection !== 0 && velocityDirection === dragDirection && Math.abs(resolvedDirectionalVelocity) >= SNAP_VELOCITY_THRESHOLD; let effectiveTargetOffset = targetOffset; if (shouldAdvance) { const adjacentIndex = (0, _clamp.clamp)(currentIndex + dragDirection, 0, orderedSnapPoints.length - 1); if (adjacentIndex !== currentIndex) { const adjacentPoint = orderedSnapPoints[adjacentIndex]; const shouldForceAdjacent = dragDirection > 0 ? targetOffset < adjacentPoint.offset : targetOffset > adjacentPoint.offset; if (shouldForceAdjacent) { targetSnapPoint = adjacentPoint; effectiveTargetOffset = adjacentPoint.offset; } } else if (dragDirection > 0) { return closeFromSnapPoints(); } } const closeOffset = popupHeight; const closeDistance = Math.abs(effectiveTargetOffset - closeOffset); const snapDistance = Math.abs(effectiveTargetOffset - targetSnapPoint.offset); if (closeDistance < snapDistance) { return closeFromSnapPoints(); } setActiveSnapPoint?.(targetSnapPoint.value, snapPointEventDetails); clearSwipeRelease(); return false; } if (resolvedDirectionalVelocity >= FAST_SWIPE_VELOCITY && dragDelta > 0) { return closeFromSnapPoints(); } let closestSnapPoint = resolvedSnapPoints[0]; let closestDistance = Math.abs(targetOffset - closestSnapPoint.offset); for (const snapPoint of resolvedSnapPoints) { const distance = Math.abs(targetOffset - snapPoint.offset); if (distance < closestDistance) { closestDistance = distance; closestSnapPoint = snapPoint; } } const closeOffset = popupHeight; const closeDistance = Math.abs(targetOffset - closeOffset); if (closeDistance < closestDistance) { return closeFromSnapPoints(); } setActiveSnapPoint?.(closestSnapPoint.value, snapPointEventDetails); clearSwipeRelease(); return false; }, onDismiss(event) { visualStateStore?.set({ swipeProgress: 0, frontmostHeight: 0 }); const backdropElement = store.context.backdropRef.current; if (backdropElement) { backdropElement.style.setProperty(_DrawerBackdropCssVars.DrawerBackdropCssVars.swipeProgress, '0'); backdropElement.style.removeProperty(_DrawerPopupCssVars.DrawerPopupCssVars.height); } const dismissEventDetails = (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.swipe, event); store.setOpen(false, dismissEventDetails); if (dismissEventDetails.isCanceled) { const pendingSnapPoint = pendingSwipeCloseSnapPointRef.current; if (pendingSnapPoint !== undefined) { setActiveSnapPoint?.(pendingSnapPoint, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.swipe, event)); } pendingSwipeCloseSnapPointRef.current = undefined; resetSwipeRef.current?.(); clearSwipeRelease(); return; } pendingSwipeCloseSnapPointRef.current = undefined; setSwipeDismissed(true); } }); const swipePointerProps = swipe.getPointerProps(); const swipeTouchProps = swipe.getTouchProps(); const resetSwipe = swipe.reset; resetSwipeRef.current = resetSwipe; React.useEffect(() => { const rootElement = viewportElement ?? popupElementState; if (!rootElement) { return undefined; } const doc = (0, _owner.ownerDocument)(rootElement); const win = (0, _owner.ownerWindow)(doc); function handleNativeTouchMove(event) { const touchState = touchScrollStateRef.current; const touch = event.touches[0]; if (!touch || !touchState) { return; } const target = (0, _dom.isElement)(event.target) ? event.target : null; const updateTouchPosition = () => { touchState.lastX = touch.clientX; touchState.lastY = touch.clientY; }; // Preserve native range interaction by never locking touchmove for range inputs. if (isEventOnRangeInput(event, win)) { touchState.allowSwipe = false; updateTouchPosition(); return; } // Avoid blocking pinch zoom or text selection adjustments on iOS Safari. if (event.touches.length === 2) { updateTouchPosition(); return; } let allowTouchMove = false; // Allow the ability to adjust text selection. if (target) { const selection = target.ownerDocument.defaultView?.getSelection(); if (selection && !selection.isCollapsed && selection.containsNode(target, true)) { allowTouchMove = true; } } // Allow user to drag the selection handles in an input element. if (target instanceof win.HTMLInputElement) { const input = target; if (input.selectionStart != null && input.selectionEnd != null && input.selectionStart < input.selectionEnd && doc.activeElement === input) { allowTouchMove = true; } } if (allowTouchMove || !open || !mounted || nestedDrawerOpen) { updateTouchPosition(); return; } const scrollTarget = touchState.scrollTarget; if (!scrollTarget || scrollTarget === doc.documentElement || scrollTarget === doc.body) { if (event.cancelable) { event.preventDefault(); } updateTouchPosition(); return; } const hasScrollableContent = hasScrollableContentOnAxis(scrollTarget, scrollAxis); if (!hasScrollableContent) { // If the scroll container doesn't overflow on the drawer axis, prevent the window from // scrolling instead. if (event.cancelable) { event.preventDefault(); } updateTouchPosition(); return; } const delta = scrollAxis === 'vertical' ? touch.clientY - touchState.lastY : touch.clientX - touchState.lastX; if (delta !== 0) { const canSwipeFromScrollEdge = canSwipeFromScrollEdgeOnMove(scrollTarget, scrollAxis, swipeDirection, delta); if (touchState.allowSwipe !== true) { if (!event.cancelable) { touchState.allowSwipe = false; } else if (canSwipeFromScrollEdge) { touchState.allowSwipe = true; event.preventDefault(); } else { touchState.allowSwipe = false; } } else if (event.cancelable) { event.preventDefault(); } } updateTouchPosition(); } doc.addEventListener('touchmove', handleNativeTouchMove, { passive: false, capture: true }); return () => { doc.removeEventListener('touchmove', handleNativeTouchMove, { capture: true }); }; }, [mounted, nestedDrawerOpen, open, popupElementState, scrollAxis, swipeDirection, viewportElement]); React.useEffect(() => { if (!snapPointRange || swipe.swiping) { return; } const resolvedProgress = !open || nested ? 0 : snapPointProgress ?? 0; applySwipeProgress({ resolvedProgress, shouldTrackProgress: true, notifyParent: false }); }, [applySwipeProgress, frontmostHeight, nested, notifyParentSwipeProgressChange, open, snapPointProgress, snapPointRange, swipe.swiping, store, visualStateStore]); React.useEffect(() => { if (!notifyParentSwipeProgressChange) { return undefined; } if (!open) { notifyParentSwipeProgressChange(0); } return () => { notifyParentSwipeProgressChange(0); }; }, [notifyParentSwipeProgressChange, open]); React.useEffect(() => { if (open) { resetSwipe(); clearSwipeRelease(); } }, [clearSwipeRelease, open, resetSwipe]); React.useEffect(() => { return () => { visualStateStore?.set({ swipeProgress: 0, frontmostHeight: 0 }); setBackdropSwipingAttribute(store.context.backdropRef.current, false); notifyParentSwipingChange?.(false); }; }, [notifyParentSwipingChange, store, visualStateStore]); const swipeProviderValue = React.useMemo(() => ({ swiping: swipe.swiping, getDragStyles: swipe.getDragStyles, swipeStrength: swipeRelease ?? null, setSwipeDismissed(dismissed) { setSwipeDismissedElements(store.context.popupRef.current, store.context.backdropRef.current, dismissed); } }), [store, swipe.getDragStyles, swipe.swiping, swipeRelease]); function resetTouchTrackingState() { touchScrollStateRef.current = null; lastPointerTypeRef.current = ''; ignoreNextTouchStartFromPenRef.current = false; } return /*#__PURE__*/(0, _jsxRuntime.jsx)(_DialogViewport.DialogViewport, { ref: forwardedRef, className: className, render: render, ...(0, _mergeProps.mergeProps)(elementProps, { onPointerDown(event) { lastPointerTypeRef.current = event.pointerType; ignoreNextTouchStartFromPenRef.current = event.pointerType === 'pen'; if (!open || !mounted || nestedDrawerOpen || event.pointerType === 'touch') { return; } const doc = (0, _owner.ownerDocument)(event.currentTarget); const elementAtPoint = typeof doc.elementFromPoint === 'function' ? doc.elementFromPoint(event.clientX, event.clientY) : null; if (elementAtPoint?.closest('[data-swipe-ignore]')) { return; } swipePointerProps.onPointerDown?.(event); }, onPointerMove(event) { if (event.pointerType === 'touch') { return; } swipePointerProps.onPointerMove?.(event); }, onPointerUp(event) { if (lastPointerTypeRef.current === event.pointerType) { lastPointerTypeRef.current = ''; } if (event.pointerType === 'touch') { return; } swipePointerProps.onPointerUp?.(event); }, onPointerCancel(event) { if (lastPointerTypeRef.current === event.pointerType) { lastPointerTypeRef.current = ''; } if (event.pointerType === 'touch') { return; } swipePointerProps.onPointerCancel?.(event); }, onTouchStart(event) { const startedFromPenPointerDown = lastPointerTypeRef.current === 'pen' && ignoreNextTouchStartFromPenRef.current; if (startedFromPenPointerDown) { ignoreNextTouchStartFromPenRef.current = false; touchScrollStateRef.current = null; return; } if (!open || !mounted || nestedDrawerOpen) { touchScrollStateRef.current = null; return; } const touch = event.touches[0]; if (!touch) { return; } if (isReactTouchEventOnRangeInput(event)) { touchScrollStateRef.current = null; return; } const rootElement = viewportElement ?? popupElementState; const target = (0, _dom.isElement)(event.target) ? event.target : null; const scrollTarget = rootElement && target && (0, _utils.contains)(rootElement, target) ? (0, _scrollable.findScrollableTouchTarget)(target, rootElement, scrollAxis) : null; let allowSwipe = null; if (scrollTarget) { const canSwipeFromEdge = isAtSwipeStartEdge(scrollTarget, scrollAxis, swipeDirection); allowSwipe = canSwipeFromEdge ? null : false; } touchScrollStateRef.current = { lastX: touch.clientX, lastY: touch.clientY, scrollTarget, allowSwipe }; swipeTouchProps.onTouchStart?.(event); }, onTouchMove(event) { if (isReactTouchEventOnRangeInput(event)) { return; } const touchState = touchScrollStateRef.current; if (touchState?.scrollTarget && touchState.allowSwipe !== true) { return; } swipeTouchProps.onTouchMove?.(event); }, onTouchEnd(event) { resetTouchTrackingState(); swipeTouchProps.onTouchEnd?.(event); }, onTouchCancel(event) { resetTouchTrackingState(); swipeTouchProps.onTouchCancel?.(event); } }), children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_DrawerViewportContext.DrawerViewportContext.Provider, { value: swipeProviderValue, children: children }) }); }); if (process.env.NODE_ENV !== "production") DrawerViewport.displayName = "DrawerViewport"; function setSwipeDismissedElements(popupElement, backdropElement, dismissed) { if (dismissed) { popupElement?.setAttribute(_DrawerPopupDataAttributes.DrawerPopupDataAttributes.swipeDismiss, ''); backdropElement?.setAttribute(_DrawerPopupDataAttributes.DrawerPopupDataAttributes.swipeDismiss, ''); return; } popupElement?.removeAttribute(_DrawerPopupDataAttributes.DrawerPopupDataAttributes.swipeDismiss); backdropElement?.removeAttribute(_DrawerPopupDataAttributes.DrawerPopupDataAttributes.swipeDismiss); } function setBackdropSwipingAttribute(backdropElement, swiping) { if (!backdropElement) { return; } if (swiping) { backdropElement.setAttribute(_DrawerPopupDataAttributes.DrawerPopupDataAttributes.swiping, ''); return; } backdropElement.removeAttribute(_DrawerPopupDataAttributes.DrawerPopupDataAttributes.swiping); } function getBaseSwipeThreshold(element, direction) { const size = direction === 'left' || direction === 'right' ? element.offsetWidth : element.offsetHeight; return Math.max(size * 0.5, MIN_SWIPE_THRESHOLD); } function isRangeInput(target, win) { return target instanceof win.HTMLInputElement && target.type === 'range'; } function isEventOnRangeInput(event, win) { const composedPath = event.composedPath(); if (composedPath) { return composedPath.some(pathTarget => isRangeInput(pathTarget, win)); } return isRangeInput(event.target, win); } function isReactTouchEventOnRangeInput(event) { return isEventOnRangeInput(event.nativeEvent, (0, _owner.ownerWindow)(event.currentTarget)); } function hasScrollableContentOnAxis(scrollTarget, axis) { return axis === 'vertical' ? scrollTarget.scrollHeight > scrollTarget.clientHeight : scrollTarget.scrollWidth > scrollTarget.clientWidth; } function getScrollMetrics(scrollTarget, axis) { if (axis === 'vertical') { const max = Math.max(0, scrollTarget.scrollHeight - scrollTarget.clientHeight); return { offset: scrollTarget.scrollTop, max }; } const max = Math.max(0, scrollTarget.scrollWidth - scrollTarget.clientWidth); return { offset: scrollTarget.scrollLeft, max }; } function isAtSwipeStartEdge(scrollTarget, axis, direction) { const { offset, max } = getScrollMetrics(scrollTarget, axis); const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis); if (dismissFromStartEdge === null) { return false; } return dismissFromStartEdge ? offset <= 0 : offset >= max; } function canSwipeFromScrollEdgeOnMove(scrollTarget, axis, direction, delta) { const { offset, max } = getScrollMetrics(scrollTarget, axis); const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis); if (dismissFromStartEdge === null) { return false; } const movingTowardDismiss = dismissFromStartEdge ? delta > 0 : delta < 0; if (!movingTowardDismiss) { return false; } return dismissFromStartEdge ? offset <= 0 : offset >= max; } function shouldDismissFromStartEdge(direction, axis) { if (axis === 'vertical') { if (direction === 'down') { return true; } if (direction === 'up') { return false; } return null; } if (direction === 'right') { return true; } if (direction === 'left') { return false; } return null; }