Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix controlled state #166

Merged
merged 4 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DrawerContextValue>({
Expand All @@ -34,6 +36,7 @@ export const DrawerContext = React.createContext<DrawerContextValue>({
onNestedDrag: () => {},
onNestedOpenChange: () => {},
onNestedRelease: () => {},
openProp: undefined,
dismissible: false,
isOpen: false,
keyboardIsOpen: { current: false },
Expand All @@ -42,6 +45,7 @@ export const DrawerContext = React.createContext<DrawerContextValue>({
modal: false,
shouldFade: false,
activeSnapPoint: null,
onOpenChange: () => {},
setActiveSnapPoint: () => {},
visible: false,
closeDrawer: () => {},
Expand Down
15 changes: 12 additions & 3 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,11 @@ function Root({
<DialogPrimitive.Root
modal={modal}
onOpenChange={(o: boolean) => {
if (openProp !== undefined) {
onOpenChange?.(o);
return;
}

if (!o) {
closeDrawer();
} else {
Expand All @@ -631,6 +636,7 @@ function Root({
drawerRef,
overlayRef,
scaleBackground,
onOpenChange,
onPress,
setVisible,
onRelease,
Expand All @@ -643,6 +649,7 @@ function Root({
onNestedOpenChange,
onNestedRelease,
keyboardIsOpen,
openProp,
modal,
snapPointsOffset,
}}
Expand Down Expand Up @@ -694,6 +701,8 @@ const Content = React.forwardRef<HTMLDivElement, ContentProps>(function (
visible,
closeDrawer,
modal,
openProp,
onOpenChange,
setVisible,
} = useDrawerContext();
const composedRef = useComposedRefs(ref, drawerRef);
Expand All @@ -715,6 +724,7 @@ const Content = React.forwardRef<HTMLDivElement, ContentProps>(function (
}}
onPointerDown={onPress}
onPointerDownOutside={(e) => {
onPointerDownOutside?.(e);
if (!modal) {
e.preventDefault();
return;
Expand All @@ -723,13 +733,12 @@ const Content = React.forwardRef<HTMLDivElement, ContentProps>(function (
keyboardIsOpen.current = false;
}
e.preventDefault();

if (!dismissible) {
onOpenChange?.(false);
if (!dismissible || openProp !== undefined) {
return;
}

closeDrawer();
onPointerDownOutside?.(e);
}}
onPointerMove={onDrag}
onPointerUp={onRelease}
Expand Down
206 changes: 206 additions & 0 deletions test/src/app/controlled/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="w-screen h-screen bg-white p-8 flex justify-center items-center" vaul-drawer-wrapper="">
<Drawer.Root open={open}>
<Drawer.Trigger asChild onClick={() => setOpen(true)}>
<button data-testid="trigger" className="text-2xl">
Open Drawer
</button>
</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay data-testid="overlay" className="fixed inset-0 bg-black/40" />
<Drawer.Content
data-testid="content"
className="bg-zinc-100 flex flex-col rounded-t-[10px] h-[96%] mt-24 fixed bottom-0 left-0 right-0"
>
<Drawer.Close data-testid="drawer-close">Close</Drawer.Close>
<button data-testid="controlled-close" onClick={() => setOpen(false)} className="text-2xl">
Close
</button>
<div className="p-4 bg-white rounded-t-[10px] flex-1">
<div className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-zinc-300 mb-8" />
<div className="max-w-md mx-auto">
<Drawer.Title className="font-medium mb-4">Unstyled drawer for React.</Drawer.Title>
<p className="text-zinc-600 mb-2">
This component can be used as a replacement for a Dialog on mobile and tablet devices.
</p>
<p className="text-zinc-600 mb-8">
It uses{' '}
<a
href="https://www.radix-ui.com/docs/primitives/components/dialog"
className="underline"
target="_blank"
>
Radix&apos;s Dialog primitive
</a>{' '}
under the hood and is inspired by{' '}
<a
href="https://twitter.com/devongovett/status/1674470185783402496"
className="underline"
target="_blank"
>
this tweet.
</a>
</p>
</div>
</div>
<div className="p-4 bg-zinc-100 border-t border-zinc-200 mt-auto">
<div className="flex gap-6 justify-end max-w-md mx-auto">
<a
className="text-xs text-zinc-600 flex items-center gap-0.25"
href="https://github.com/emilkowalski/vaul"
target="_blank"
>
GitHub
<svg
fill="none"
height="16"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="16"
aria-hidden="true"
className="w-3 h-3 ml-1"
>
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
<path d="M15 3h6v6"></path>
<path d="M10 14L21 3"></path>
</svg>
</a>
<a
className="text-xs text-zinc-600 flex items-center gap-0.25"
href="https://twitter.com/emilkowalski_"
target="_blank"
>
Twitter
<svg
fill="none"
height="16"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="16"
aria-hidden="true"
className="w-3 h-3 ml-1"
>
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
<path d="M15 3h6v6"></path>
<path d="M10 14L21 3"></path>
</svg>
</a>
</div>
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
<Drawer.Root open={fullyControlled} onOpenChange={(o) => setFullyControlled(o)}>
<Drawer.Trigger asChild>
<button data-testid="fully-controlled-trigger" className="text-2xl">
Open Drawer
</button>
</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay data-testid="overlay" className="fixed inset-0 bg-black/40" />
<Drawer.Content
data-testid="fully-controlled-content"
className="bg-zinc-100 flex flex-col rounded-t-[10px] h-[96%] mt-24 fixed bottom-0 left-0 right-0"
>
<Drawer.Close data-testid="drawer-close">Close</Drawer.Close>
<button data-testid="controlled-close" onClick={() => setOpen(false)} className="text-2xl">
Close
</button>
<div className="p-4 bg-white rounded-t-[10px] flex-1">
<div className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-zinc-300 mb-8" />
<div className="max-w-md mx-auto">
<Drawer.Title className="font-medium mb-4">Unstyled drawer for React.</Drawer.Title>
<p className="text-zinc-600 mb-2">
This component can be used as a replacement for a Dialog on mobile and tablet devices.
</p>
<p className="text-zinc-600 mb-8">
It uses{' '}
<a
href="https://www.radix-ui.com/docs/primitives/components/dialog"
className="underline"
target="_blank"
>
Radix&apos;s Dialog primitive
</a>{' '}
under the hood and is inspired by{' '}
<a
href="https://twitter.com/devongovett/status/1674470185783402496"
className="underline"
target="_blank"
>
this tweet.
</a>
</p>
</div>
</div>
<div className="p-4 bg-zinc-100 border-t border-zinc-200 mt-auto">
<div className="flex gap-6 justify-end max-w-md mx-auto">
<a
className="text-xs text-zinc-600 flex items-center gap-0.25"
href="https://github.com/emilkowalski/vaul"
target="_blank"
>
GitHub
<svg
fill="none"
height="16"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="16"
aria-hidden="true"
className="w-3 h-3 ml-1"
>
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
<path d="M15 3h6v6"></path>
<path d="M10 14L21 3"></path>
</svg>
</a>
<a
className="text-xs text-zinc-600 flex items-center gap-0.25"
href="https://twitter.com/emilkowalski_"
target="_blank"
>
Twitter
<svg
fill="none"
height="16"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="16"
aria-hidden="true"
className="w-3 h-3 ml-1"
>
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
<path d="M15 3h6v6"></path>
<path d="M10 14L21 3"></path>
</svg>
</a>
</div>
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
</div>
);
}
1 change: 1 addition & 0 deletions test/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default function Page() {
<Link href="/nested-drawers">Nested drawers</Link>
<Link href="/non-dismissible">Non-dismissible</Link>
<Link href="/initial-snap">Initial snap</Link>
<Link href="/controlled">Controlled</Link>
</div>
);
}
30 changes: 30 additions & 0 deletions test/tests/controlled.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});