diff --git a/src/index.tsx b/src/index.tsx index 691e95b..5973fbf 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -29,8 +29,9 @@ export interface WithFadeFromProps { * Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up. * Should go from least visible. Example `[0.2, 0.5, 0.8]`. * You can also use px values, which doesn't take screen height into account. + * Should be used, calculated points will be included in the callback. */ - snapPoints: (number | string)[]; + snapPoints: (number | string)[] | ((embeddedSnapPoints: (number | string)[]) => (number | string)[]); /** * Index of a `snapPoint` from which the overlay fade should be applied. Defaults to the last snap point. */ @@ -42,8 +43,9 @@ export interface WithoutFadeFromProps { * Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up. * Should go from least visible. Example `[0.2, 0.5, 0.8]`. * You can also use px values, which doesn't take screen height into account. + * Should be used, calculated points will be included in the callback. */ - snapPoints?: (number | string)[]; + snapPoints?: (number | string)[] | ((embeddedSnapPoints: (number | string)[]) => (number | string)[]); fadeFromIndex?: never; } @@ -142,14 +144,14 @@ export function Root({ children, onDrag: onDragProp, onRelease: onReleaseProp, - snapPoints, + snapPoints: initialSnapPoints, shouldScaleBackground = false, setBackgroundColorOnScale = true, closeThreshold = CLOSE_THRESHOLD, scrollLockTimeout = SCROLL_LOCK_TIMEOUT, dismissible = true, handleOnly = false, - fadeFromIndex = snapPoints && snapPoints.length - 1, + fadeFromIndex: initialFadeFromIndex, activeSnapPoint: activeSnapPointProp, setActiveSnapPoint: setActiveSnapPointProp, fixed, @@ -167,6 +169,12 @@ export function Root({ container, autoFocus = false, }: DialogProps) { + const [snapPoints, setSnapPoints] = React.useState<(string | number)[] | undefined>( + // If no snap points are provided, default to [0, 1] until we can ensure no snap points are present in DOM + (typeof initialSnapPoints === 'function' ? initialSnapPoints([]) : initialSnapPoints) ?? [0, 1], + ); + const fadeFromIndex = initialFadeFromIndex ?? (snapPoints && snapPoints.length - 1); + const [isOpen = false, setIsOpen] = useControllableState({ defaultProp: defaultOpen, prop: openProp, @@ -214,10 +222,13 @@ export function Root({ const drawerWidthRef = React.useRef(drawerRef.current?.getBoundingClientRect().width || 0); const initialDrawerHeight = React.useRef(0); - const onSnapPointChange = React.useCallback((activeSnapPointIndex: number) => { - // Change openTime ref when we reach the last snap point to prevent dragging for 500ms incase it's scrollable. - if (snapPoints && activeSnapPointIndex === snapPointsOffset.length - 1) openTime.current = new Date(); - }, []); + const onSnapPointChange = React.useCallback( + (activeSnapPointIndex: number) => { + // Change openTime ref when we reach the last snap point to prevent dragging for 500ms incase it's scrollable. + if (snapPoints && activeSnapPointIndex === snapPointsOffset.length - 1) openTime.current = new Date(); + }, + [snapPoints], + ); const { activeSnapPoint, @@ -477,6 +488,8 @@ export function Root({ function onVisualViewportChange() { if (!drawerRef.current || !repositionInputs) return; + updateSnapPoints(); + const focusedElement = document.activeElement as HTMLElement; if (isInput(focusedElement) || keyboardIsOpen.current) { const visualViewportHeight = window.visualViewport?.height || 0; @@ -662,6 +675,36 @@ export function Root({ resetDrawer(); } + function updateSnapPoints() { + if (!drawerRef.current || (initialSnapPoints && typeof initialSnapPoints !== 'function')) return; + const drawerPosition = drawerRef.current?.getBoundingClientRect().y ?? 0; + const snapPointsNodes = document.querySelectorAll('[data-vaul-snap-point]'); + if (snapPointsNodes.length === 0) return; + const embeddedSnapPoints = Array.from(snapPointsNodes).map((snapPoint) => { + const snapPointOffset = Number(snapPoint.getAttribute('data-vaul-offset')) ?? 0; + const snapPointVerticalPosition = snapPoint.getBoundingClientRect().y; + return `${snapPointVerticalPosition + WINDOW_TOP_OFFSET + snapPointOffset - drawerPosition}px`; + }); + const newSnapPoints = initialSnapPoints?.(embeddedSnapPoints) ?? embeddedSnapPoints; + setSnapPoints(newSnapPoints); + + return newSnapPoints; + } + + React.useEffect(() => { + if (isOpen) { + // Detect if any snapPoints are present in DOM, otherwise set snapPoints to undefined + window.requestAnimationFrame(() => { + const newSnapPoints = updateSnapPoints(); + if (newSnapPoints) { + setActiveSnapPoint(newSnapPoints[0]); + } else if (!initialSnapPoints) { + setSnapPoints(undefined); + } + }); + } + }, [isOpen]); + React.useEffect(() => { // Trigger enter animation without using CSS animation if (isOpen) { @@ -1095,6 +1138,16 @@ export const Handle = React.forwardRef(function ( Handle.displayName = 'Drawer.Handle'; +export type SnapPointProps = { + offset?: number; +}; + +export const SnapPoint = React.forwardRef(function ({ offset = 0 }, ref) { + return ; +}); + +SnapPoint.displayName = 'Drawer.SnapPoint'; + export function NestedRoot({ onDrag, onOpenChange, open: nestedIsOpen, ...rest }: DialogProps) { const { onNestedDrag, onNestedOpenChange, onNestedRelease } = useDrawerContext(); @@ -1145,4 +1198,5 @@ export const Drawer = { Close: DialogPrimitive.Close, Title: DialogPrimitive.Title, Description: DialogPrimitive.Description, + SnapPoint, }; diff --git a/test/src/app/page.tsx b/test/src/app/page.tsx index 0e270a0..cfba5e8 100644 --- a/test/src/app/page.tsx +++ b/test/src/app/page.tsx @@ -8,6 +8,7 @@ export default function Page() { With scaled background Without scaled background With snap points + With embedded snap points With modal false Scrollable with inputs Nested drawers diff --git a/test/src/app/with-embedded-snap-points/page.tsx b/test/src/app/with-embedded-snap-points/page.tsx new file mode 100644 index 0000000..a133fee --- /dev/null +++ b/test/src/app/with-embedded-snap-points/page.tsx @@ -0,0 +1,152 @@ +'use client'; + +import { clsx } from 'clsx'; +import { useState } from 'react'; +import { Drawer } from 'vaul'; + +export default function Page() { + const [snap, setSnap] = useState(); + + return ( +
+ [...snapPoints, 1]} activeSnapPoint={snap} setActiveSnapPoint={setSnap}> + + + + + + +
+
+ + + + + +
{' '} +

The Hidden Details

+

2 modules, 27 hours of video

+ +

+ The world of user interface design is an intricate landscape filled with hidden details and nuance. In + this course, you will learn something cool. To the untrained eye, a beautifully designed UI. +

+ + +
+

Module 01. The Details

+
+
+ Layers of UI + A basic introduction to Layers of Design. +
+
+ Typography + The fundamentals of type. +
+
+ UI Animations + Going through the right easings and durations. +
+
+
+
+
+
+ “I especially loved the hidden details video. That was so useful, learned a lot by just reading it. + Can’t wait for more course content!” +
+
+ Yvonne Ray, Frontend Developer +
+
+
+
+

Module 02. The Process

+
+
+ Build + Create cool components to practice. +
+
+ User Insight + Find out what users think and fine-tune. +
+
+ Putting it all together + Let's build an app together and apply everything. +
+
+
+
+
+
+
+
+ ); +}