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
62.84k stars 3.53k forks source link

ScrollArea doesn't work in Dialog #922

Closed neo773 closed 11 months ago

neo773 commented 11 months 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 11 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)
neo773 commented 11 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)

The 3rd option worked, Thanks.

Anish-Karthik commented 8 months ago

could you post the working code

KaramveerSinghSidhu commented 8 months 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 7 months 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 7 months 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 7 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 7 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 6 months ago

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

max-programming commented 5 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 5 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 2 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>
));