ui: `ScrollArea` doesn't work inside `Combobox`

Problem

Can’t scroll the contents inside the CommandGroup of the Combobox component.

image

However, the scroll only works in the tiny scrollbar.

image

Expected

Can scroll if cursor on top of the items

Reproducable

Codesandbox: link

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 7
  • Comments: 28

Commits related to this issue

Most upvoted comments

Wrap the <CommandEmpty /> and the <CommandGroup /> with the <ScrollArea />

Something like this, it works for me:

<ScrollArea className="h-96">
  <CommandEmpty>Not found</CommandEmpty>
  <CommandGroup>
    {data.map((item) => (
        <CommandItem
          key={item.value}
          value={item.value}
          onSelect={handleOnChange}
        >
          {item.label}
          <CheckIcon
            className={cn(
              "ml-auto h-4 w-4",
              selected === item.value ? "opacity-100" : "opacity-0"
            )}
          />
        </CommandItem>
      ))
    }
  </CommandGroup>
</ScrollArea>

thanks @atleugim it works fine.

I had to remove the PopoverPrimitive.Portal from the Popover component, too ( replaced it with <> - source: https://github.com/shadcn-ui/ui/issues/607#issuecomment-1610187963 )

https://github.com/shadcn-ui/ui/issues/542#issuecomment-1587142689

Setting modal={true} as suggested by ^ helped fix for me w/o the need of <ScrollArea />

I could fix the error adding className="h-48 overflow-auto" to the className of the ScrollArea

                    <ScrollArea className="h-48 overflow-auto">
                      <CommandGroup>
                        {ingredients.map((ingredient) => (
                          <CommandItem
                            value={ingredient.name}
                            key={ingredient.id}
                            onSelect={() => {
                              form.setValue("ingredient_id", ingredient.id);
                            }}
                          >
                            {ingredient.name}
                            <CheckIcon
                              className={cn(
                                "ml-auto h-4 w-4",
                                ingredient.id === field.value
                                  ? "opacity-100"
                                  : "opacity-0"
                              )}
                            />
                          </CommandItem>
                        ))}
                      </CommandGroup>
                    </ScrollArea>

Now I can scroll the items of the CommandGroup

Hey! Sorry but what do you mean by removing the portal part from Popover?

I mean just not render the PopoverPrimitive.Portal component

I mean just not render the PopoverPrimitive.Portal component

The same issue persists if there’s no Portal component. For example, I’m not able to scroll inside the combobox here:

<FormItem className="flex flex-col">
                        <FormLabel>Options</FormLabel>
                        <Popover>
                          <PopoverTrigger asChild>
                            <FormControl>
                              <Button
                                variant="outline"
                                role="combobox"
                                className={cn(
                                  "justify-between",
                                  !field.value && "text-muted-foreground"
                                )}
                              >
                                {field.value
                                  ? options.find(
                                      (option) => option.value === field.value
                                    )?.label
                                  : "Select option"}
                                <Icon
                                  name="chevrons-up-down"
                                  className="ml-1 h-4 w-4 shrink-0 opacity-50"
                                />
                              </Button>
                            </FormControl>
                          </PopoverTrigger>
                          <PopoverContent
                            className="max-h-[150px] overflow-y-scroll w-[110px] p-0" //trying to set a max height and overflow but no luck
                            sticky="always"
                            side="bottom"
                          >
                            <Command>
                              <CommandInput
                                placeholder="Search..."
                                className="h-9"
                              />
                              <CommandEmpty>Nothing found.</CommandEmpty>
                              <CommandGroup>
                                {options.map((option) => (
                                  <CommandItem
                                    value={option.value}
                                    key={option.value}
                                    onSelect={(value) => {
                                      form.setValue("option", value);
                                    }}
                                  >
                                    {option.label}
                                    <Icon
                                      name="check"
                                      asSpan={false}
                                      className={cn(
                                        "ml-auto h-4 w-4",
                                        option.value === field.value
                                          ? "opacity-100"
                                          : "opacity-0"
                                      )}
                                    />
                                  </CommandItem>
                                ))}
                              </CommandGroup>
                            </Command>
                          </PopoverContent>
                        </Popover>
                        <FormMessage />
                      </FormItem>
                    )}
/>

It’s an issue with Radix https://github.com/radix-ui/primitives/issues/1159. In the meantime, you can remove the portal part from Popover

For anyone who just wants to make the combobox scrollable, you can also use CommandList. Example: https://github.com/shadcn-ui/ui/blob/main/apps/www/components/command-menu.tsx#L77

I wanted a solution that allows the ComboBox to use a portal unless it’s inside a Dialog. To avoid passing props through consumers, I added a DialogContext that communicates down-tree that a Dialog is present. This allows the Popover to render the portal or not accordingly.

I assume there’s a downside to this approach I can’t see, but it seems less destructive than globally removing the popover portal just for the ComboBox in some less common scenarios. Thanks to @noxify and @atleugim for the pointers.

// popover

const PopoverContent = React.forwardRef<
  React.ElementRef<typeof PopoverPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => {
  // do not want to render portals inside dialogs
  // this causes scroll problems
  // ref: https://github.com/shadcn-ui/ui/issues/607

  const isInsideDialog = React.useContext(DialogContext);
  const shouldRenderWithoutPortal = isInsideDialog;

  const content = (
    <PopoverPrimitive.Content
      ref={ref}
      align={align}
      sideOffset={sideOffset}
      className={cn(
        "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
        className
      )}
      {...props}
    />
  );

  if (shouldRenderWithoutPortal) {
    return content;
  }

  return <PopoverPrimitive.Portal>{content}</PopoverPrimitive.Portal>;
});
// dialog

const Dialog: typeof DialogPrimitive.Root = (props) => (
  // this provider exists to tell downstream consumers (like Popover) that they
  // are inside a dialog and should render themselves accordingly

  <DialogProvider isInsideDialog={true}>
    <DialogPrimitive.Root {...props} />
  </DialogProvider>
);

// ... same as reference for other comps

type DialogContextValue = {
  isInsideDialog: boolean;
};

export const DialogContext = React.createContext<DialogContextValue>({
  isInsideDialog: false,
});

const DialogProvider = ({
  children,
}: React.PropsWithChildren<DialogContextValue>) => (
  <DialogContext.Provider value={{ isInsideDialog: true }}>
    {children}
  </DialogContext.Provider>
);

My combo box code is the same as the reference except I added classes for max height and overflow. I am not using a ScrollArea.

// combo box
// ... same as ref except for:

 <CommandGroup className="max-h-60 overflow-y-auto">

// .. same

For me 2 solutions worked:

  1. Remove PopoverPrimitive.Portal - Simpler but may affect operation

  2. Use the onWheel configuration in PopoverPrimitive.Content mentioned above by @pa4080 in conjunction with the ScrollArea around CommandEmpty and CommandGroup - More complex but does not need to remove PopoverPrimitive.Portal

  • ScrollArea Captura de Tela 2024-01-30 às 18 59 25
  • PopoverPrimitive.Content

code23

Here is a nice workaround, which simulates arrow-up and arrow-down when you using the muse wheel over the the <PopoverContent /> component. Replace the relevant lines in your ui/popover.tsx file with these:

const PopoverContent = React.forwardRef<
	React.ElementRef<typeof PopoverPrimitive.Content>,
	React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
	<PopoverPrimitive.Portal>
		<PopoverPrimitive.Content
			ref={ref}
			align={align}
			className={cn(
				"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
				className
			)}
			sideOffset={sideOffset}
			/**
			 * Fix for issue with scrolling:
			 * @see https://github.com/shadcn-ui/ui/issues/607
			 */
			onWheel={(e) => {
				e.stopPropagation();

				const isScrollingDown = e.deltaY > 0;

				if (isScrollingDown) {
					// Simulate arrow down key press
					e.currentTarget.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown" }));
				} else {
					// Simulate arrow up key press
					e.currentTarget.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowUp" }));
				}
			}}
			{...props}
		/>
	</PopoverPrimitive.Portal>
));

For me, setting max height for Command component and ScrollArea’s overflow to auto worked:

<Command className="max-h-96">
   <CommandInput placeholder="Search region..." />
   <CommandEmpty>No region found.</CommandEmpty>
   <ScrollArea className="overflow-auto">
      <CommandGroup>
         {regions.map((region) => (...
          )}
      </CommandGroup>
   </ScrollArea>
</Command>

There are 2 solutions outlined and they both work but the styling is not what I expect.

  1. Like @krisantuswanandi mentioned, you can use CommandList from cmdk to create a scrollbar in ComboBox. ComboBox uses the Command component and uses cmdk.

  2. Use ScrollArea to create a scrollbar in ComboBox and pass in overflow-y-auto like everyone else that has mentioned.

This is the scrollbar styling I expect from shadcn ScrollArea here. Screenshot 2024-02-08 at 1 44 04 pm

However, both solutions creates a Scrollbar that gives me this and the styling can be found from cmdk here. Screenshot 2024-02-08 at 1 43 22 pm

Is there a fix for this issue? / Can we have a version of the component without using cmdk?

Here is a nice workaround, which simulates arrow-up and arrow-down when you using the muse wheel over the the <PopoverContent /> component. Replace the relevant lines in your ui/popover.tsx file with these:

const PopoverContent = React.forwardRef<
	React.ElementRef<typeof PopoverPrimitive.Content>,
	React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
	<PopoverPrimitive.Portal>
		<PopoverPrimitive.Content
			ref={ref}
			align={align}
			className={cn(
				"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
				className
			)}
			sideOffset={sideOffset}
			/**
			 * Fix for issue with scrolling:
			 * @see https://github.com/shadcn-ui/ui/issues/607
			 */
			onWheel={(e) => {
				e.stopPropagation();

				const isScrollingDown = e.deltaY > 0;

				if (isScrollingDown) {
					// Simulate arrow down key press
					e.currentTarget.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown" }));
				} else {
					// Simulate arrow up key press
					e.currentTarget.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowUp" }));
				}
			}}
			{...props}
		/>
	</PopoverPrimitive.Portal>
));

This works great! Hopefully this gets fixed soon!

If it’s helpful for anyone, I’m using it inside a Popover similar to the example given in the docs, the installed component using npx for popover, the PopoverContent component is returning it wrapped in <PopoverPrimitive.Portal> I just removed the portal and instead wrapped it with <></> and now scrolling works. I haven’t yet figured out if it breaks anything else, I’ll update here if I find an issue with my solution.