diff --git a/src/context.ts b/src/context.ts index dd14e08f..4b9113bb 100644 --- a/src/context.ts +++ b/src/context.ts @@ -22,6 +22,8 @@ interface DrawerContextValue { visible: boolean; closeDrawer: () => void; setVisible: (o: boolean) => void; + openProp?: boolean; + onOpenChange?: (o: boolean) => void; } export const DrawerContext = React.createContext({ @@ -34,6 +36,7 @@ export const DrawerContext = React.createContext({ onNestedDrag: () => {}, onNestedOpenChange: () => {}, onNestedRelease: () => {}, + openProp: undefined, dismissible: false, isOpen: false, keyboardIsOpen: { current: false }, @@ -42,6 +45,7 @@ export const DrawerContext = React.createContext({ modal: false, shouldFade: false, activeSnapPoint: null, + onOpenChange: () => {}, setActiveSnapPoint: () => {}, visible: false, closeDrawer: () => {}, diff --git a/src/index.tsx b/src/index.tsx index 891a0a8d..e043d470 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -613,6 +613,11 @@ function Root({ { + if (openProp !== undefined) { + onOpenChange?.(o); + return; + } + if (!o) { closeDrawer(); } else { @@ -631,6 +636,7 @@ function Root({ drawerRef, overlayRef, scaleBackground, + onOpenChange, onPress, setVisible, onRelease, @@ -643,6 +649,7 @@ function Root({ onNestedOpenChange, onNestedRelease, keyboardIsOpen, + openProp, modal, snapPointsOffset, }} @@ -694,6 +701,8 @@ const Content = React.forwardRef(function ( visible, closeDrawer, modal, + openProp, + onOpenChange, setVisible, } = useDrawerContext(); const composedRef = useComposedRefs(ref, drawerRef); @@ -715,6 +724,7 @@ const Content = React.forwardRef(function ( }} onPointerDown={onPress} onPointerDownOutside={(e) => { + onPointerDownOutside?.(e); if (!modal) { e.preventDefault(); return; @@ -723,13 +733,12 @@ const Content = React.forwardRef(function ( keyboardIsOpen.current = false; } e.preventDefault(); - - if (!dismissible) { + onOpenChange?.(false); + if (!dismissible || openProp !== undefined) { return; } closeDrawer(); - onPointerDownOutside?.(e); }} onPointerMove={onDrag} onPointerUp={onRelease} diff --git a/test/src/app/controlled/page.tsx b/test/src/app/controlled/page.tsx new file mode 100644 index 00000000..0805fc44 --- /dev/null +++ b/test/src/app/controlled/page.tsx @@ -0,0 +1,206 @@ +'use client'; + +import { useState } from 'react'; +import { Drawer } from 'vaul'; + +export default function Page() { + const [open, setOpen] = useState(false); + const [fullyControlled, setFullyControlled] = useState(false); + + return ( +
+ + setOpen(true)}> + + + + + + Close + +
+
+
+ Unstyled drawer for React. +

+ This component can be used as a replacement for a Dialog on mobile and tablet devices. +

+

+ It uses{' '} + + Radix's Dialog primitive + {' '} + under the hood and is inspired by{' '} + + this tweet. + +

+
+
+ + + + + setFullyControlled(o)}> + + + + + + + Close + +
+
+
+ Unstyled drawer for React. +

+ This component can be used as a replacement for a Dialog on mobile and tablet devices. +

+

+ It uses{' '} + + Radix's Dialog primitive + {' '} + under the hood and is inspired by{' '} + + this tweet. + +

+
+
+ + + + +
+ ); +} diff --git a/test/src/app/page.tsx b/test/src/app/page.tsx index dd00526f..c49d797b 100644 --- a/test/src/app/page.tsx +++ b/test/src/app/page.tsx @@ -12,6 +12,7 @@ export default function Page() { Nested drawers Non-dismissible Initial snap + Controlled
); } diff --git a/test/tests/controlled.spec.ts b/test/tests/controlled.spec.ts new file mode 100644 index 00000000..ade8c599 --- /dev/null +++ b/test/tests/controlled.spec.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { ANIMATION_DURATION } from './constants'; + +test.beforeEach(async ({ page }) => { + await page.goto('/controlled'); +}); + +test.describe('Initial-snap', () => { + test('should not close when clicked on overlay and only the open prop is passsed', async ({ page }) => { + await expect(page.getByTestId('content')).not.toBeVisible(); + await page.getByTestId('trigger').click(); + await expect(page.getByTestId('content')).toBeVisible(); + // Click on the background + await page.mouse.click(0, 0); + + await page.waitForTimeout(ANIMATION_DURATION); + await expect(page.getByTestId('content')).toBeVisible(); + }); + + test('should close when clicked on overlay and open and onOpenChange props are passed', async ({ page }) => { + await expect(page.getByTestId('fully-controlled-content')).not.toBeVisible(); + await page.getByTestId('fully-controlled-trigger').click(); + await expect(page.getByTestId('fully-controlled-content')).toBeVisible(); + // Click on the background + await page.mouse.click(0, 0); + + await page.waitForTimeout(ANIMATION_DURATION); + await expect(page.getByTestId('fully-controlled-content')).not.toBeVisible(); + }); +});