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

feat: export useDrawerContext #467

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

jasongerbes
Copy link

Vaul doesn't currently export the useDrawerContext hook, which would be helpful for getting the direction of the <Drawer.Root> to style the <Drawer.Content> appropriately.

For context, I'm updating my instance of the shadcn/ui Drawer component to support all directions (similar to the Sheet component).

This PR exports useDrawerContext and DrawerContext, and includes some format fixes for the new JSDocs.

@@ -1112,3 +1112,5 @@ export const Drawer = {
Title: DialogPrimitive.Title,
Description: DialogPrimitive.Description,
};

export { DrawerContext, useDrawerContext };
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to export DrawerContext? This can't be used externally.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@emilkowalski Hi, Is it any way to get access to context to close drawer?

@jasongerbes
Copy link
Author

Hey @emilkowalski, you're right that DrawerContext can't currently be used externally.

My use case requires knowing the value of the direction prop to position the Drawer.Handle appropriately:

  • bottom - show Drawer.Handle at the top of the drawer.
  • top - show Drawer.Handle at the bottom of the drawer.
  • left or right - hide Drawer.Handle and show Drawer.Close instead.

I'm currently using a custom DrawerContext to share the direction prop with children of the Drawer.Root.

Here's the full implementation for context. It builds upon the shadcn/ui Drawer component.

components/ui/drawer.tsx
'use client';

import * as React from 'react';
import { Cross2Icon } from '@radix-ui/react-icons';
import { cva } from 'class-variance-authority';
import { Drawer as DrawerPrimitive } from 'vaul';

import { cn } from '@/lib/utils';

type DrawerContextValue = Pick<DrawerProps, 'direction'>;
const DrawerContext = React.createContext<DrawerContextValue>({});

const useDrawerContext = () => {
  const context = React.useContext(DrawerContext);
  if (!context) {
    throw new Error('useDrawerContext must be used within a Drawer.Root');
  }
  return context;
};

export type DrawerProps = React.ComponentProps<typeof DrawerPrimitive.Root>;

const Drawer = ({
  shouldScaleBackground = true,
  direction = 'bottom',
  ...props
}: DrawerProps) => (
  <DrawerContext.Provider value={{ direction }}>
    <DrawerPrimitive.Root
      shouldScaleBackground={shouldScaleBackground}
      direction={direction}
      {...props}
    />
  </DrawerContext.Provider>
);
Drawer.displayName = 'Drawer';

const DrawerTrigger = DrawerPrimitive.Trigger;

const DrawerPortal = DrawerPrimitive.Portal;

const DrawerClose = DrawerPrimitive.Close;

const DrawerOverlay = React.forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <DrawerPrimitive.Overlay
    ref={ref}
    className={cn('fixed inset-0 z-50 bg-black/80', className)}
    {...props}
  />
));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;

const drawContentVariants = cva(
  'fixed z-50 flex flex-col bg-background outline-none',
  {
    variants: {
      direction: {
        top: 'inset-x-0 top-0 mb-24 h-auto max-h-[90%] rounded-b-[10px]',
        bottom: 'inset-x-0 bottom-0 mt-24 h-auto max-h-[90%] rounded-t-[10px]',
        left: 'inset-y-0 left-0 mr-24 w-full max-w-[85%] rounded-r-[10px] lg:max-w-3xl',
        right:
          'inset-y-0 right-0 ml-24 w-full max-w-[85%] rounded-l-[10px] lg:max-w-3xl',
      },
    },
    defaultVariants: {
      direction: 'bottom',
    },
  },
);

const DrawerContent = React.forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => {
  const { direction } = useDrawerContext();
  const isHorizontal = direction === 'left' || direction === 'right';

  return (
    <DrawerPortal>
      <DrawerOverlay />
      <DrawerPrimitive.Content
        ref={ref}
        className={cn(drawContentVariants({ direction }), className)}
        {...props}
      >
        {direction === 'bottom' && <DrawerHandle />}

        <div
          className={cn(
            'flex-grow overflow-y-auto',
            direction !== 'bottom' && 'pt-3',
          )}
        >
          {children}
        </div>

        {direction === 'top' && <DrawerHandle />}

        {isHorizontal && (
          <DrawerClose className="absolute right-6 top-6 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
            <Cross2Icon className="size-5" />
            <span className="sr-only">Close</span>
          </DrawerClose>
        )}
      </DrawerPrimitive.Content>
    </DrawerPortal>
  );
});
DrawerContent.displayName = 'DrawerContent';

const DrawerHandle = () => (
  <div className="pb-2 pt-1.5">
    <DrawerPrimitive.Handle />
  </div>
);

const DrawerHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn('grid gap-1.5 p-4 text-left sm:px-6', className)}
    {...props}
  />
);
DrawerHeader.displayName = 'DrawerHeader';

const DrawerBody = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn('mt-auto flex flex-col px-4 sm:px-6', className)}
    {...props}
  />
);
DrawerBody.displayName = 'DrawerBody';

const DrawerFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn('mt-auto flex flex-col gap-2 p-4', className)}
    {...props}
  />
);
DrawerFooter.displayName = 'DrawerFooter';

const DrawerTitle = React.forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
  <DrawerPrimitive.Title
    ref={ref}
    className={cn(
      'text-lg font-semibold leading-none tracking-tight',
      className,
    )}
    {...props}
  />
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;

const DrawerDescription = React.forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
  <DrawerPrimitive.Description
    ref={ref}
    className={cn('text-sm text-muted-foreground', className)}
    {...props}
  />
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;

export {
  Drawer,
  DrawerPortal,
  DrawerOverlay,
  DrawerTrigger,
  DrawerClose,
  DrawerContent,
  DrawerHeader,
  DrawerBody,
  DrawerFooter,
  DrawerTitle,
  DrawerDescription,
};

This workaround is sufficient, so feel free to close this PR if there are specific issues with exporting DrawerContext.

@marcusforsberg
Copy link

I also have a need to access useDrawerContext to customize the behaviour of the Overlay. I have non-modal Drawer, which currently force hides the Overlay entirely. I still need the overlay to be visible on certain snap points. If I had access to the context, I could implement my own Overlay logic.

@AhmedBaset
Copy link

Is there anything blocking merging this? I have the exact use case as the author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants