@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
1 lines • 13.3 kB
Source Map (JSON)
{"version":3,"file":"FloatingSheet.mjs","names":[],"sources":["../../../src/base-ui/FloatingSheet/FloatingSheet.tsx"],"sourcesContent":["import { cx } from 'antd-style';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { FloatingSheetHeader } from './FloatingSheetHeader';\nimport { clamp, dampenValue, resolveSize } from './helpers';\nimport { styles } from './style';\nimport type { FloatingSheetProps } from './type';\nimport { useSheetDrag } from './useSheetDrag';\nimport { useSnapPoints } from './useSnapPoints';\n\nconst ANIMATION_MS = 300;\n\nexport function FloatingSheet({\n open: openProp,\n onOpenChange,\n defaultOpen = false,\n snapPoints: snapPointsProp,\n activeSnapPoint: activeSnapPointProp,\n onSnapPointChange,\n minHeight: minHeightProp = 200,\n maxHeight: maxHeightProp = 0.8,\n restingHeight: restingHeightProp,\n mode = 'overlay',\n variant = 'elevated',\n width = '100%',\n title,\n headerActions,\n dismissible = true,\n closeThreshold = 0.25,\n children,\n className,\n}: FloatingSheetProps) {\n const s = styles;\n\n // Controlled / uncontrolled open state\n const isControlled = openProp !== undefined;\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isOpen = isControlled ? openProp : internalOpen;\n\n const setOpen = useCallback(\n (value: boolean) => {\n if (!isControlled) setInternalOpen(value);\n onOpenChange?.(value);\n },\n [isControlled, onOpenChange],\n );\n\n // Container measurement via ResizeObserver\n const containerRef = useRef<HTMLElement | null>(null);\n const sheetRef = useRef<HTMLDivElement>(null);\n const [containerHeight, setContainerHeight] = useState(0);\n\n useEffect(() => {\n const parent = sheetRef.current?.parentElement;\n if (!parent) return;\n containerRef.current = parent;\n\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n setContainerHeight(entry.contentRect.height);\n }\n });\n observer.observe(parent);\n setContainerHeight(parent.getBoundingClientRect().height);\n\n return () => observer.disconnect();\n }, []);\n\n // Resolve min/max to px\n const minHeightPx = useMemo(\n () => resolveSize(minHeightProp, containerHeight),\n [minHeightProp, containerHeight],\n );\n const maxHeightPx = useMemo(\n () => resolveSize(maxHeightProp, containerHeight),\n [maxHeightProp, containerHeight],\n );\n const restingHeightPx = useMemo(\n () =>\n restingHeightProp !== undefined\n ? clamp(resolveSize(restingHeightProp, containerHeight), minHeightPx, maxHeightPx)\n : minHeightPx,\n [restingHeightProp, containerHeight, minHeightPx, maxHeightPx],\n );\n\n // Snap points\n const hasSnapPoints = !!snapPointsProp && snapPointsProp.length > 0;\n const { snapPointHeights, findActiveIndex, getSnapRelease } = useSnapPoints({\n closeThreshold,\n containerHeight,\n containerRef,\n maxHeightPx,\n minHeightPx,\n snapPoints: snapPointsProp ?? [],\n });\n\n // Compute the \"resting\" height for the current open + snap state\n const restingHeight = useMemo(() => {\n if (!containerHeight) return 0;\n if (hasSnapPoints && activeSnapPointProp !== undefined) {\n const resolved = resolveSize(activeSnapPointProp, containerHeight);\n return clamp(resolved, minHeightPx, maxHeightPx);\n }\n if (hasSnapPoints && snapPointHeights.length > 0) {\n return snapPointHeights[0];\n }\n return restingHeightPx;\n }, [\n containerHeight,\n hasSnapPoints,\n activeSnapPointProp,\n snapPointHeights,\n minHeightPx,\n maxHeightPx,\n restingHeightPx,\n ]);\n\n const [height, setHeight] = useState(isOpen ? restingHeight : 0);\n const [isAnimating, setIsAnimating] = useState(false);\n // Keeps sheet visible during close animation (height → 0)\n const [isClosing, setIsClosing] = useState(false);\n const heightBeforeDrag = useRef(0);\n const prevOpenRef = useRef(isOpen);\n\n // Handle open/close transitions\n useEffect(() => {\n const wasOpen = prevOpenRef.current;\n prevOpenRef.current = isOpen;\n\n if (isOpen && !wasOpen) {\n // Opening: animate from 0 → resting height\n setIsClosing(false);\n setIsAnimating(true);\n setHeight(restingHeight);\n const timer = setTimeout(() => setIsAnimating(false), ANIMATION_MS);\n return () => clearTimeout(timer);\n }\n\n if (!isOpen && wasOpen) {\n // Closing: animate from current height → 0, then hide\n setIsClosing(true);\n setIsAnimating(true);\n setHeight(0);\n const timer = setTimeout(() => {\n setIsAnimating(false);\n setIsClosing(false);\n }, ANIMATION_MS);\n return () => clearTimeout(timer);\n }\n }, [isOpen]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Sync height when resting height changes (container resize, snap point change)\n useEffect(() => {\n if (isOpen && !isDragging) {\n setHeight(restingHeight);\n }\n }, [restingHeight]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Drag handlers\n const onDragChange = useCallback(\n (draggedDistance: number) => {\n const newHeight = heightBeforeDrag.current + draggedDistance;\n\n if (hasSnapPoints) {\n const highest = snapPointHeights.at(-1) ?? maxHeightPx;\n const lowest = snapPointHeights[0] ?? minHeightPx;\n\n if (newHeight > highest) {\n const overshoot = newHeight - highest;\n setHeight(highest + dampenValue(overshoot));\n } else if (newHeight < lowest) {\n const undershoot = lowest - newHeight;\n setHeight(Math.max(0, lowest - dampenValue(undershoot)));\n } else {\n setHeight(newHeight);\n }\n } else {\n setHeight(clamp(newHeight, 0, maxHeightPx));\n }\n },\n [hasSnapPoints, snapPointHeights, maxHeightPx, minHeightPx],\n );\n\n const onDragEnd = useCallback(\n (draggedDistance: number, velocity: number) => {\n setIsAnimating(true);\n const currentHeight = heightBeforeDrag.current + draggedDistance;\n\n if (hasSnapPoints) {\n const activeIndex = findActiveIndex(heightBeforeDrag.current);\n const result = getSnapRelease({\n activeIndex,\n currentHeight,\n dismissible,\n draggedDistance,\n velocity,\n });\n\n if (result.type === 'dismiss') {\n setIsClosing(true);\n setHeight(0);\n const timer = setTimeout(() => {\n setOpen(false);\n setIsAnimating(false);\n setIsClosing(false);\n }, ANIMATION_MS);\n return () => clearTimeout(timer);\n }\n\n setHeight(result.height);\n const originalSnapValue = snapPointsProp?.find(\n (sp) =>\n resolveSize(sp, containerHeight) === result.height ||\n clamp(resolveSize(sp, containerHeight), minHeightPx, maxHeightPx) === result.height,\n );\n if (originalSnapValue !== undefined) {\n onSnapPointChange?.(originalSnapValue);\n }\n } else {\n if (dismissible && currentHeight < minHeightPx * closeThreshold) {\n setIsClosing(true);\n setHeight(0);\n const timer = setTimeout(() => {\n setOpen(false);\n setIsAnimating(false);\n setIsClosing(false);\n }, ANIMATION_MS);\n return () => clearTimeout(timer);\n }\n setHeight(clamp(currentHeight, minHeightPx, maxHeightPx));\n }\n\n setTimeout(() => setIsAnimating(false), ANIMATION_MS);\n },\n [\n hasSnapPoints,\n findActiveIndex,\n getSnapRelease,\n dismissible,\n snapPointsProp,\n containerHeight,\n minHeightPx,\n maxHeightPx,\n closeThreshold,\n setOpen,\n onSnapPointChange,\n ],\n );\n\n const { isDragging, handleProps } = useSheetDrag({\n enabled: isOpen ?? false,\n onDragChange,\n onDragEnd,\n });\n\n // Record height at drag start\n useEffect(() => {\n if (isDragging) {\n heightBeforeDrag.current = height;\n }\n }, [isDragging]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const isVisible = isOpen || isClosing || height > 0;\n const shouldAnimate = !isDragging && isAnimating;\n const inlineOverflowUp =\n mode === 'inline' && isVisible ? Math.max(0, height - restingHeightPx) : 0;\n\n return (\n <div\n data-floating-sheet=\"\"\n data-state={isOpen ? 'open' : 'closed'}\n ref={sheetRef}\n className={cx(\n s.root,\n variant === 'embedded' ? s.embedded : s.elevated,\n mode === 'overlay' ? s.overlay : s.inline,\n mode === 'overlay' ? s.overlayRadius : s.inlineRadius,\n shouldAnimate && s.transition,\n !isVisible && s.hidden,\n className,\n )}\n style={{\n height: isVisible ? height : 0,\n marginTop: inlineOverflowUp ? -inlineOverflowUp : undefined,\n width,\n }}\n >\n <FloatingSheetHeader\n handleProps={handleProps}\n headerActions={headerActions}\n isDragging={isDragging}\n title={title}\n />\n <div className={s.content}>{children}</div>\n </div>\n );\n}\n"],"mappings":"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"}