shadcn-ui / ui

Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.
https://ui.shadcn.com
MIT License
75.46k stars 4.73k forks source link

ScrollArea doesn't work in Dialog #922

Closed neo773 closed 1 year ago

neo773 commented 1 year ago

When using ScrollArea inside Dialog gesture scrolling doesn't work only way to navigate is by grabbing the scroll indicator

https://github.com/shadcn/ui/assets/62795688/ea1abc45-19f7-4969-812e-5b376e1a3f9a

To reproduce this bug index.ts

import { Button } from "@/components/ui/button"
import { ComboBox } from "@/components/ui/combo-box"
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"

const Demo = () => {
    const languages = [
        { label: "English", value: "en" },
        { label: "French", value: "fr" },
        { label: "German", value: "de" },
        { label: "Spanish", value: "es" },
        { label: "Portuguese", value: "pt" },
        { label: "Russian", value: "ru" },
        { label: "Japanese", value: "ja" },
        { label: "Korean", value: "ko" },
        { label: "Chinese", value: "zh" },
        { label: "Italian", value: "it" },
        { label: "Dutch", value: "nl" },
        { label: "Swedish", value: "sv" },
        { label: "Greek", value: "el" },
        { label: "Czech", value: "cs" },
        { label: "Polish", value: "pl" },
        { label: "Hungarian", value: "hu" },
        { label: "Turkish", value: "tr" },
        { label: "Arabic", value: "ar" },
        { label: "Hebrew", value: "he" },
        { label: "Hindi", value: "hi" }
    ] as const;

  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">Test</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[400px] px-0">
        <ComboBox
          options={languages.map((country) => {
            return {
              value: country.value,
              label: `${country.label}`,
            }
          })}
          placeholder={"Search Country"}
          defaultValue={"en"}
          onValueChange={() => {}}
        />
      </DialogContent>
    </Dialog>
  )
}

export default Demo

ComboBox.ts


import { cn } from "../../lib/utils"
import { Button } from "./button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
} from "./command"
import { Popover, PopoverContent, PopoverTrigger } from "./popover"
import { ScrollArea } from "./scroll-area"
import { Check, ChevronsUpDown } from "lucide-react"
import * as React from "react"

interface Item {
  value: string
  label: string
}

interface ComboBoxProps {
  options: Item[]
  defaultValue?: string
  placeholder: string
  className?: string
  onValueChange: (value: string) => void
}

export const ComboBox: React.FC<ComboBoxProps> = ({
  options,
  defaultValue = "",
  placeholder,
  className,
  onValueChange,
}) => {
  const [open, setOpen] = React.useState(false)

  const handleSelect = (currentValue: string) => {
    setOpen(false)
    onValueChange(currentValue)
  }

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="ComboBox"
          aria-expanded={open}
          className={cn("w-[350px] justify-between", className)}
        >
          {defaultValue
            ? options.find((option) => option.value === defaultValue)?.label
            : placeholder}
          <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-[350px] p-0">
        <Command>
          <CommandInput
            placeholder={`Search ${placeholder.toLowerCase()}...`}
          />
          <ScrollArea className="h-[200px]">
            <CommandEmpty>No {placeholder.toLowerCase()} found.</CommandEmpty>
            <CommandGroup>
              {options.map((option) => (
                <CommandItem
                  key={option.value}
                  onSelect={() => handleSelect(option.value)}
                >
                  <Check
                    className={cn(
                      "mr-2 h-4 w-4",
                      defaultValue === option.value
                        ? "opacity-100"
                        : "opacity-0"
                    )}
                  />
                  {option.label}
                </CommandItem>
              ))}
            </CommandGroup>
          </ScrollArea>
        </Command>
      </PopoverContent>
    </Popover>
  )
}
joaom00 commented 1 year ago

You can make a couple of things:

  1. Not render the Portal component in popover (Maybe you have z-index issues)
  2. Set modal={false} on Dialog (The overlay will no be rendered)
  3. Wrap Dialog.Content with Dialog.Overlay (You can not set modal={false} because the overlay will not be rendered)
neo773 commented 1 year ago

You can make a couple of things:

  1. Not render the Portal component in popover (Maybe you have z-index issues)
  2. Set modal={false} on Dialog (The overlay will no be rendered)
  3. Wrap Dialog.Content with Dialog.Overlay (You can not set modal={false} because the overlay will not be rendered)

The 3rd option worked, Thanks.

Anish-Karthik commented 1 year ago

could you post the working code

KaramveerSinghSidhu commented 1 year ago

I am having the same issue.

<FormField
          control={form.control}
          name="timezone"
          render={({ field }) => (
            <FormItem className="flex flex-col">
              <FormLabel>Timezone</FormLabel>
              <Popover>
                <PopoverTrigger asChild>
                  <FormControl>
                    <Button
                      variant="outline"
                      role="combobox"
                      className={cn(
                        "w-[200px] justify-between",
                        !field.value && "text-muted-foreground"
                      )}
                    >
                      {field.value
                        ? TimeZones.find((timezone) => timezone === field.value)
                        : "Select timezone"}
                      <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
                    </Button>
                  </FormControl>
                </PopoverTrigger>
                <PopoverContent className="w-[200px] p-0">
                  <Command>
                    <CommandInput placeholder="Search timezone..." />
                    <CommandEmpty>No timezone found.</CommandEmpty>
                    <ScrollArea className="h-60">
                      <CommandGroup>
                        {TimeZones.map((timezone) => (
                          <CommandItem
                            value={timezone}
                            key={timezone}
                            onSelect={() => {
                              form.setValue("timezone", timezone);
                            }}
                          >
                            <CheckIcon
                              className={cn(
                                "mr-2 h-4 w-4",
                                timezone === field.value
                                  ? "opacity-100"
                                  : "opacity-0"
                              )}
                            />
                            {timezone}
                          </CommandItem>
                        ))}
                      </CommandGroup>
                    </ScrollArea>
                  </Command>
                </PopoverContent>
              </Popover>
              <FormMessage />
            </FormItem>
          )}
        />
aleciavogel commented 1 year ago

In order to make the ScrollArea work inside of a Dialog, I ended up having to add a container prop to my PopoverContent, like so:

interface PopoverContentProps {
  container?: HTMLElement
}

const PopoverContent = React.forwardRef<
  React.ElementRef<typeof PopoverPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & PopoverContentProps
>(({ className, container, align = 'center', sideOffset = 4, ...props }, ref) => (
  <PopoverPrimitive.Portal container={container}>
    <PopoverPrimitive.Content
      ref={ref}
      align={align}
      sideOffset={sideOffset}
      className={cn(
      ...

Then I wrapped all <CommandItem />'s inside of a ScrollArea with a className of h-[300px] so that the search will remain at the top while I can scroll through all of the options.

Finally, in the component where I define the Dialog that wraps everything, I defined a dialogRef, like so:

  const dialogRef = React.useRef<HTMLDivElement>(null)

  return (
    <Dialog defaultOpen>
      <DialogContent
        ref={dialogRef}
        ...

I also had to add the dialogRef to my ComboBox props.

interface ComboboxProps {
  // All my other props are defined above here...
  dialogRef?: React.RefObject<HTMLElement>
}

const Combobox: FC<ComboboxProps> = ({
  dialogRef,
  ...

Lastly, add your dialogRef to the PopoverContent inside of the ComboBox:

<PopoverContent
  container={dialogRef?.current === null ? undefined : dialogRef?.current}
  className="w-[350px] p-0"
>

So now you should be able to use your ComboBox and the ScrollArea should do its magic:

<Combobox dialogRef={dialogRef} />

Hope this helps!

desiboli commented 1 year ago

I am having the same issue.

@KaramveerSinghSidhu Instead of ScrollArea, use the CommandList component like so:

                 <Command>
                    <CommandInput placeholder="Search language..." />
                    <CommandList>
                      <CommandEmpty>No language found.</CommandEmpty>
                      <CommandGroup>
                        {countryOptions.map((country) => {
                          console.log("COUNTRY>>>>", country)
                          return (
                            <CommandItem
                              value={country.label}
                              key={country.value}
                              onSelect={() => {
                                form.setValue("country", country.value)
                              }}
                            >
                              <CheckIcon
                                className={cn(
                                  "mr-2 h-4 w-4",
                                  country.value === field.value
                                    ? "opacity-100"
                                    : "opacity-0"
                                )}
                              />
                              {country.label}
                            </CommandItem>
                          )
                        })}
                      </CommandGroup>
                    </CommandList>
                  </Command>
KaramveerSinghSidhu commented 11 months ago

@desiboli still not working.

    <FormField
      control={form.control}
      name="timezone"
      render={({ field }) => (
        <FormItem className="flex flex-col">
          <FormLabel>Timezone</FormLabel>
          <Popover>
            <PopoverTrigger asChild>
              <FormControl>
                <Button
                  variant="outline"
                  role="combobox"
                  className={cn(
                    "w-[200px] justify-between",
                    !field.value && "text-muted-foreground"
                  )}
                >
                  {field.value
                    ? TimeZones.find((timezone) => timezone === field.value)
                    : "Select timezone"}
                  <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
                </Button>
              </FormControl>
            </PopoverTrigger>
            <PopoverContent className="w-[200px] p-0">
              <Command>
                <CommandInput placeholder="Search language..." />
                <CommandList>
                  <CommandEmpty>No language found.</CommandEmpty>
                  <CommandGroup>
                    {TimeZones.map((timezone) => {
                      return (
                        <CommandItem
                          value={timezone}
                          key={timezone}
                          onSelect={() => {
                            form.setValue("timezone", timezone);
                          }}
                        >
                          <CheckIcon
                            className={cn(
                              "mr-2 h-4 w-4",
                              timezone === field.value
                                ? "opacity-100"
                                : "opacity-0"
                            )}
                          />
                          {timezone}
                        </CommandItem>
                      );
                    })}
                  </CommandGroup>
                </CommandList>
              </Command>
            </PopoverContent>
          </Popover>
          <FormMessage />
        </FormItem>
desiboli commented 11 months ago

@KaramveerSinghSidhu That's weird it works for me, see video.

https://github.com/shadcn-ui/ui/assets/6296494/fdf647fe-5f0a-4e19-b163-37d43bbb3a77

Could you setup a sandbox where you can reproduce this ?

7hourspg commented 11 months ago

Set modal={true} in popover like this <Popover open={open} modal={true}> ✔ Thanks 😎

max-programming commented 10 months ago

Isn't there a straightforward way to show a scroll bar on a Dialog if it contains too much content or is too big for the screen to fit?

PS: I am not using popover just pure input fields in the dialog

JoelInman-Dev commented 10 months ago

I had this issue with the DropdownMenu component. In the end I managed to get this working by simply setting max-h- and overflow-scroll as tailwind classes on the DropdownMenuContent element, also tried it with the PopOverContent and it works, so i would assume this would also work on dialog and commandList

<DropdownMenu>
    <DropdownMenuTrigger asChild>
        <Button variant="outline" className="px-4">
            {itemId > 0 ? 'Selected: ' + itemId : 'Select Range Item'}
        </Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent className="max-h-80 overflow-scroll">
        {rangeItems.map((item) => (
             <DropdownMenuItem
             className={'text-sm' + (item.id == itemId ? ' bg-gray-200' : '')}
             onClick={() => {
                 setItemId(item.id);
             }}
             >
                {item.id + ' | ' + item.allocation}
              </DropdownMenuItem>
            ))}
    </DropdownMenuContent>
</DropdownMenu>
projetos-sidnei commented 7 months ago

I was trying to use Shadcnui's ScrollArea, but with popover and dialog it wasn't working, it didn't generate the vertical bar, what helped me was indicating the ref in the Scroll within the ComandList, maybe it will work for someone else, remembering that they didn't want the bar system scrolling. I tested it with Popover and it doesn't work, but with Dialog it works fine. my code modification:


  React.ElementRef<typeof CommandPrimitive.List>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
  >(({ className, ...props }, ref) => (
    <ScrollArea ref={ref}>
    <CommandPrimitive.List
      ref={ref}
      className={cn('h-[400px] overflow-hidden overflow-x-hidden', className)}
      {...props}
      />
      <ScrollBar orientation='vertical'/>
    </ScrollArea>
));
jeslenbucci commented 4 months ago

I was able to get the ScrollArea to work with the Dialog by setting a max height to the DialogContent component and then using the ScrollArea component nested within.

<Dialog
      open={isOpen}
      onOpenChange={setIsOpen}
    >
      <DialogContent className='h-full max-h-[96%] p-4'>
        <ScrollArea className='p-4'>
          <DialogHeader>
            <DialogTitle asChild>
              <h2>{title}</h2>
            </DialogTitle>
            <DialogDescription>{description}</DialogDescription>
          </DialogHeader>

          {children}

          <DialogFooter className=''>
            <div className='flex justify-center gap-x-2'>
              <DialogClose asChild>
                <Button
                  variant='outline'
                  className='w-full'
                >
                  Cancel
                </Button>
              </DialogClose>
              <Button
                className='w-full'
              >
                confirm
              </Button>
            </div>
          </DialogFooter>
        </ScrollArea>
      </DialogContent>
    </Dialog>
3li7u commented 4 months ago

You can make a couple of things:

  1. Not render the Portal component in popover (Maybe you have z-index issues)
  2. Set modal={false} on Dialog (The overlay will no be rendered)
  3. Wrap Dialog.Content with Dialog.Overlay (You can not set modal={false} because the overlay will not be rendered)

@joaom00 The third one solves the problem for me but I still have another issue when I press tab to jumb to the next input the focus move to the dialog instade of the next input the second one solves this but I need the overlay image

MentalGear commented 3 months ago

Would be nice if the dialog content would be scrollable by default since that is what's expected. Takes quite a bit of extra frustration to figure out how to make it work:

<!--  -->
<Dialog.Root>
    <Dialog.Trigger>
        <slot name="OpenButton" />
    </Dialog.Trigger>

    <Dialog.Content class="!p-0">
        <!-- scroll area needed to make Dialog's content scrollable -->
        <ScrollArea>
            <Dialog.Header>
                <div class="stickyDialogBars top-0">
                    <Dialog.Title>
                        <slot name="Title" />
                    </Dialog.Title>
                </div>

                <div class="p-4">
                    <Dialog.Description>
                        <slot name="Content" />
                    </Dialog.Description>
                </div>
            </Dialog.Header>

            <div class="stickyDialogBars bottom-0">
                <Dialog.Close class="">
                    <Button>Back</Button>
                </Dialog.Close>
            </div>
        </ScrollArea>
        <!--  -->
    </Dialog.Content>
</Dialog.Root>

<style lang="sass">

    .stickyDialogBars
        @apply sticky flex place-content-center py-4
        @apply bg-background bg-opacity-90

    // hide close icon on top right corner for m
</style>
Evavic44 commented 3 months ago

It seems this issue also happens with custom scroll components. In my case, I was not using the ScrollArea component but @7hourspg's solution worked regardless.

<Popover modal={true}>
  <PopoverTrigger>Open</PopoverTrigger>
  <PopoverContent
    className="flex flex-col gap-4 w-60 h-60 overflow-y-scroll p-2"
    align="start"
  >
    {content.map((c) => (
      <button key={c.id} className="w-full">
        {c.title}
      </button>
    ))}
  </PopoverContent>
</Popover>
andreidionisie commented 2 months ago

Set modal={true} in popover like this <Popover open={open} modal={true}> ✔ Thanks 😎

This makes another side-effect. Dialog closes and reopens when you click outside of popover.

Tshiring commented 2 weeks ago

Set modal={true} in popover like this <Popover open={open} modal={true}> ✔ Thanks 😎

This makes another side-effect. Dialog closes and reopens when you click outside of popover.

works fine for me

Tshiring commented 2 weeks ago

`

  {!disabled && (
    <PopoverContent
      className={cn('p-0', isAccountSetupPage && 'w-[--radix-popover-trigger-width]')}
      align="start"
    >
      <Command>
        <CommandInput placeholder="Search Timezone" className="h-9" />

        {isLoading ? (
          <div className="flex items-center justify-center p-4">
            <Loader />
          </div>
        ) : (
          <>
            <CommandEmpty>No Result found.</CommandEmpty>
            <CommandGroup>
              <CommandList className="h-48">
                {filteredData.map((filteredData) => (
                  <CommandItem
                    key={filteredData.label}
                    value={filteredData.label}
                    onSelect={() => handleSelectTimezone(filteredData.label)}
                    className=" cursor-pointer text-sm "
                  >
                    <Check
                      className={cn(
                        'absolute right-2 mr-2 h-4 w-4',
                        defaultValue === filteredData.label ? 'opacity-100 ' : 'opacity-0  '
                      )}
                    />
                    {filteredData.label}
                  </CommandItem>
                ))}
              </CommandList>
            </CommandGroup>
          </>
        )}
      </Command>
    </PopoverContent>
  )}
</Popover>`
setting modal to true in Popover works for me thanks!
samislam commented 1 week ago

Set modal={true} in popover like this <Popover open={open} modal={true}> ✔ Thanks 😎

This works thanks