ui
ui copied to clipboard
`ScrollArea` doesn't work inside `Combobox`
Problem
Can't scroll the contents inside the CommandGroup
of the Combobox
component.
However, the scroll only works in the tiny scrollbar.
Expected
Can scroll if cursor on top of the items
Reproducable
Codesandbox: link
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
Hey can you try this? I tried editing in the sandbox but it lagged so bad.
<Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button variant='outline' role='combobox' aria-expanded={open} className='w-[200px] justify-between'> {value ? frameworks.find((framework) => framework.value === value)?.label : 'Select framework...'} <ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' /> </Button> </PopoverTrigger> <PopoverContent className='w-[200px] p-0'> <Command> <CommandInput placeholder='Search framework...' /> <CommandEmpty>No framework found.</CommandEmpty> <ScrollArea className='h-24 overflow-auto'> <CommandGroup> {frameworks.map((framework) => ( <CommandItem key={framework.value} onSelect={(currentValue) => { setValue(currentValue === value ? '' : currentValue); setOpen(false); }}> <Check className={cn( 'mr-2 h-4 w-4', value === framework.value + 'a' ? 'opacity-100' : 'opacity-0' )} /> {framework.label + ' '} </CommandItem> ))} </CommandGroup> </ScrollArea> </Command> </PopoverContent> </Popover>
<Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button variant='outline' role='combobox' aria-expanded={open} className='w-[200px] justify-between'> {value ? frameworks.find((framework) => framework.value === value)?.label : 'Select framework...'} <ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' /> </Button> </PopoverTrigger> <PopoverContent className='w-[200px] p-0'> <Command> <CommandInput placeholder='Search framework...' /> <CommandEmpty>No framework found.</CommandEmpty> <ScrollArea className='h-24 overflow-auto'> <CommandGroup> {frameworks.map((framework) => ( <CommandItem key={framework.value} onSelect={(currentValue) => { setValue(currentValue === value ? '' : currentValue); setOpen(false); }}> <Check className={cn( 'mr-2 h-4 w-4', value === framework.value + 'a' ? 'opacity-100' : 'opacity-0' )} /> {framework.label + ' '} </CommandItem> ))} </CommandGroup> </ScrollArea> </Command> </PopoverContent> </Popover>
Hey! Sorry but what do you mean by removing the portal part from Popover?
Hey! Sorry but what do you mean by removing the portal part from Popover?
I mean just not render the PopoverPrimitive.Portal
component
I am not sure if it's the same issue from radix, but I also am not getting a working scroll area in the following setup:
<Sheet>
<SheetContent>
<Tabs>
<ScrollArea>
<TabsContent> <-- this is not scrolling
using overflow-auto
on TabsContent instead does work but does not have the benefits of ScrollArea
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>
)}
/>
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 )
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
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.
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
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 />
#542 (comment)
This works for me too! Thanks!
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>
Works great!
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>
));
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 yourui/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!
#542 (comment)
This also worked me. No need to scrollarea
For me 2 solutions worked:
-
Remove PopoverPrimitive.Portal - Simpler but may affect operation
-
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
- PopoverPrimitive.Content
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
There are 2 solutions outlined and they both work but the styling is not what I expect.
-
Like @krisantuswanandi mentioned, you can use
CommandList
from cmdk to create a scrollbar inComboBox
. ComboBox uses the Command component and uses cmdk. -
Use
ScrollArea
to create a scrollbar inComboBox
and pass inoverflow-y-auto
like everyone else that has mentioned.
This is the scrollbar styling I expect from shadcn ScrollArea
here.
However, both solutions creates a Scrollbar that gives me this and the styling can be found from cmdk here.
Is there a fix for this issue? / Can we have a version of the component without using cmdk?
I wanted a solution that allows the
ComboBox
to use a portal unless it's inside aDialog
. To avoid passing props through consumers, I added aDialogContext
that communicates down-tree that aDialog
is present. This allows thePopover
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
I like your idea but i think removing the portal isn't the best solution i found that you just have to put modal={true}
in popover so i made this change to the popover component:
import { DialogContext } from '@/components/ui/dialog'
const Popover: typeof PopoverPrimitive.Root = (props) => {
const isInsideDialog = React.useContext(DialogContext)
return <PopoverPrimitive.Root {...props} modal={Boolean(isInsideDialog)} />
}
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>
Oh wow, this did it for me:
https://github.com/shadcn-ui/ui/issues/607#issuecomment-1672111729
Also adding a scroll area made this work too. What's the portal for?
How can I focus on the selected value when open the combobox again? Select component has the behavior, need to achieve it with combobox.
Hey! Sorry but what do you mean by removing the portal part from Popover?
I mean just not render the
PopoverPrimitive.Portal
component
it worked with select if removed </SelectPrimitive.Portal>
thx👌
Setting
modal={true}
as suggested by ^ helped fix for me w/o the need of<ScrollArea />
This is much simpler solution than any of above. You can go even simpler by ignoring ={true}
as JSX will interpreter it as true by default <Popover modal>
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>
));
Thankyou it really worked for me !!
Inside popover.tsx just replace PopoverContent with below one :-
`const PopoverContent = React.forwardRef< React.ElementRef<typeof PopoverPrimitive.Content>, React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( <> <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} /> </> ))`
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> )} />
For anyone still having this issue, the Shad PopoverContent component automatically wraps it's children in the questionable Portal component. Just change your Popover file to this:
const PopoverContentNoPortal = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<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}
/>
))
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>((props, ref) => (
<PopoverPrimitive.Portal>
<PopoverContentNoPortal {...props} ref={ref} />
</PopoverPrimitive.Portal>
))
and use the PopoverContentNoPortal
component instead of the PopoverContent
in places where you're having issues with scrolling.
This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please leave a comment. Thank you.