From 4e79a3c7a75e64f8bf6907f1974786ecd08bd856 Mon Sep 17 00:00:00 2001 From: Sam Fletcher Date: Wed, 12 Feb 2025 01:15:25 -0600 Subject: [PATCH 1/3] Add support for new `` --- src/index.tsx | 65 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 691e95b..9268f30 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -29,6 +29,7 @@ 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, this prop will be ignored. */ snapPoints: (number | string)[]; /** @@ -42,6 +43,7 @@ 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, this prop will be ignored. */ snapPoints?: (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 + 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,35 @@ export function Root({ resetDrawer(); } + function updateSnapPoints() { + if (!drawerRef.current) return; + const drawerPosition = drawerRef.current?.getBoundingClientRect().y ?? 0; + const snapPointsNodes = document.querySelectorAll('[data-vaul-snap-point]'); + if (snapPointsNodes.length === 0) return; + const newSnapPoints = 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`; + }); + 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 || initialSnapPoints.length === 0) { + setSnapPoints(undefined); + } + }); + } + }, [isOpen]); + React.useEffect(() => { // Trigger enter animation without using CSS animation if (isOpen) { @@ -1095,6 +1137,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 +1197,5 @@ export const Drawer = { Close: DialogPrimitive.Close, Title: DialogPrimitive.Title, Description: DialogPrimitive.Description, + SnapPoint, }; From eb098cdf32f9d9531957085ae35f52d14abd74af Mon Sep 17 00:00:00 2001 From: Sam Fletcher Date: Wed, 12 Feb 2025 11:29:05 -0600 Subject: [PATCH 2/3] Add optional callback for `snapPoints` prop --- src/index.tsx | 17 +- test/src/app/page.tsx | 1 + .../app/with-embedded-snap-points/page.tsx | 152 ++++++++++++++++++ 3 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 test/src/app/with-embedded-snap-points/page.tsx diff --git a/src/index.tsx b/src/index.tsx index 9268f30..5973fbf 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -29,9 +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, this prop will be ignored. + * 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. */ @@ -43,9 +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, this prop will be ignored. + * Should be used, calculated points will be included in the callback. */ - snapPoints?: (number | string)[]; + snapPoints?: (number | string)[] | ((embeddedSnapPoints: (number | string)[]) => (number | string)[]); fadeFromIndex?: never; } @@ -171,7 +171,7 @@ export function Root({ }: 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 - initialSnapPoints ?? [0, 1], + (typeof initialSnapPoints === 'function' ? initialSnapPoints([]) : initialSnapPoints) ?? [0, 1], ); const fadeFromIndex = initialFadeFromIndex ?? (snapPoints && snapPoints.length - 1); @@ -676,15 +676,16 @@ export function Root({ } function updateSnapPoints() { - if (!drawerRef.current) return; + 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 newSnapPoints = Array.from(snapPointsNodes).map((snapPoint) => { + 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; @@ -697,7 +698,7 @@ export function Root({ const newSnapPoints = updateSnapPoints(); if (newSnapPoints) { setActiveSnapPoint(newSnapPoints[0]); - } else if (!initialSnapPoints || initialSnapPoints.length === 0) { + } else if (!initialSnapPoints) { setSnapPoints(undefined); } }); 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..99f7ffc --- /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. +
+
+
+
+
+
+
+
+ ); +} From 869140182a7adaceb0a0d202915a9916a1b01df6 Mon Sep 17 00:00:00 2001 From: Sam Fletcher Date: Wed, 12 Feb 2025 11:41:38 -0600 Subject: [PATCH 3/3] Update snap points in test example --- test/src/app/with-embedded-snap-points/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/app/with-embedded-snap-points/page.tsx b/test/src/app/with-embedded-snap-points/page.tsx index 99f7ffc..a133fee 100644 --- a/test/src/app/with-embedded-snap-points/page.tsx +++ b/test/src/app/with-embedded-snap-points/page.tsx @@ -97,6 +97,7 @@ export default function Page() { +

Module 01. The Details

@@ -114,7 +115,6 @@ export default function Page() {
-