shadcn-ui-expansions
shadcn-ui-expansions copied to clipboard
More accessible using `Popover`
It will be more accessible if using Popover. Without Popover
, it's always give additional height if I put the input at the very bottom of the screen. Thank you for your work!
Hi @ImamAlfariziSyahputra, I think you are right. Have you by any chance worked on this? Else I will myself probably :D. Thanks
Im changing mine to use popover. its not finishing yet, but i think that wont be too much diferent:
<Popover>
<div className="flex flex-col">
<div>
{selected.map((option) => (
<Badge
key={option.value}
className={cn(
"data-[disabled]:bg-muted-foreground data-[disabled]:text-muted data-[disabled]:hover:bg-muted-foreground",
"data-[fixed]:bg-muted-foreground data-[fixed]:text-muted data-[fixed]:hover:bg-muted-foreground",
badgeClassName
)}
data-fixed={option.fixed}
data-disabled={disabled}
>
{option.label}
<button
className={cn(
"ring-offset-background focus:ring-ring ml-1 rounded-full outline-none focus:ring-2 focus:ring-offset-2",
(disabled || option.fixed) && "hidden"
)}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleUnselect(option)
}
}}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={() => handleUnselect(option)}
>
<X className="text-muted-foreground hover:text-foreground h-3 w-3" />
</button>
</Badge>
))}
</div>
<Command
{...commandProps}
onKeyDown={(e) => {
handleKeyDown(e)
commandProps?.onKeyDown?.(e)
}}
className={cn(
"overflow-visible bg-transparent",
commandProps?.className
)}
shouldFilter={
commandProps?.shouldFilter !== undefined
? commandProps.shouldFilter
: !onSearch
} // When onSearch is provided, we don't want to filter the options. You can still override it.
filter={commandFilter()}
>
<PopoverTrigger>
<Input
className="px-2"
value={inputValue}
disabled={disabled}
onChange={(e) => {
setInputValue(e.target.value)
inputProps?.onValueChange?.(e.target.value)
}}
onBlur={(event) => {
setOpen(false)
inputProps?.onBlur?.(event)
}}
onFocus={(event) => {
setOpen(true)
triggerSearchOnFocus && onSearch?.(debouncedSearchTerm)
inputProps?.onFocus?.(event)
}}
placeholder={
hidePlaceholderWhenSelected && selected.length !== 0
? ""
: placeholder
}
/>
</PopoverTrigger>
<CommandPrimitive.Input
{...inputProps}
ref={inputRef}
value={inputValue}
className={cn("hidden", inputProps?.className)}
/>
<PopoverContent
autoFocus={false}
asChild
onOpenAutoFocus={(e) => e.preventDefault()}
>
<CommandList className=" w-full rounded-md border shadow-md outline-none animate-in">
{isLoading ? (
<>{loadingIndicator}</>
) : (
<>
{EmptyItem()}
{CreatableItem()}
{!selectFirstItem && (
<CommandItem value="-" className="hidden" />
)}
{Object.entries(selectables).map(([key, dropdowns]) => (
<CommandGroup key={key} heading={key} className="h-full">
{dropdowns.map((option) => (
<CommandItem
key={option.value}
value={option.value}
disabled={option.disable}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onSelect={() => {
if (selected.length >= maxSelected) {
onMaxSelected?.(selected.length)
return
}
setInputValue("")
const newOptions = [...selected, option]
setSelected(newOptions)
onChange?.(newOptions)
}}
className={cn(
"cursor-pointer",
option.disable &&
"text-muted-foreground cursor-default"
)}
>
{option.label}
</CommandItem>
))}
</CommandGroup>
))}
</>
)}
</CommandList>
</PopoverContent>
</Command>
</div>
</Popover>
Im changing mine to use popover. its not finishing yet, but i think that wont be too much diferent:
<Popover> <div className="flex flex-col"> <div> {selected.map((option) => ( <Badge key={option.value} className={cn( "data-[disabled]:bg-muted-foreground data-[disabled]:text-muted data-[disabled]:hover:bg-muted-foreground", "data-[fixed]:bg-muted-foreground data-[fixed]:text-muted data-[fixed]:hover:bg-muted-foreground", badgeClassName )} data-fixed={option.fixed} data-disabled={disabled} > {option.label} <button className={cn( "ring-offset-background focus:ring-ring ml-1 rounded-full outline-none focus:ring-2 focus:ring-offset-2", (disabled || option.fixed) && "hidden" )} onKeyDown={(e) => { if (e.key === "Enter") { handleUnselect(option) } }} onMouseDown={(e) => { e.preventDefault() e.stopPropagation() }} onClick={() => handleUnselect(option)} > <X className="text-muted-foreground hover:text-foreground h-3 w-3" /> </button> </Badge> ))} </div> <Command {...commandProps} onKeyDown={(e) => { handleKeyDown(e) commandProps?.onKeyDown?.(e) }} className={cn( "overflow-visible bg-transparent", commandProps?.className )} shouldFilter={ commandProps?.shouldFilter !== undefined ? commandProps.shouldFilter : !onSearch } // When onSearch is provided, we don't want to filter the options. You can still override it. filter={commandFilter()} > <PopoverTrigger> <Input className="px-2" value={inputValue} disabled={disabled} onChange={(e) => { setInputValue(e.target.value) inputProps?.onValueChange?.(e.target.value) }} onBlur={(event) => { setOpen(false) inputProps?.onBlur?.(event) }} onFocus={(event) => { setOpen(true) triggerSearchOnFocus && onSearch?.(debouncedSearchTerm) inputProps?.onFocus?.(event) }} placeholder={ hidePlaceholderWhenSelected && selected.length !== 0 ? "" : placeholder } /> </PopoverTrigger> <CommandPrimitive.Input {...inputProps} ref={inputRef} value={inputValue} className={cn("hidden", inputProps?.className)} /> <PopoverContent autoFocus={false} asChild onOpenAutoFocus={(e) => e.preventDefault()} > <CommandList className=" w-full rounded-md border shadow-md outline-none animate-in"> {isLoading ? ( <>{loadingIndicator}</> ) : ( <> {EmptyItem()} {CreatableItem()} {!selectFirstItem && ( <CommandItem value="-" className="hidden" /> )} {Object.entries(selectables).map(([key, dropdowns]) => ( <CommandGroup key={key} heading={key} className="h-full"> {dropdowns.map((option) => ( <CommandItem key={option.value} value={option.value} disabled={option.disable} onMouseDown={(e) => { e.preventDefault() e.stopPropagation() }} onSelect={() => { if (selected.length >= maxSelected) { onMaxSelected?.(selected.length) return } setInputValue("") const newOptions = [...selected, option] setSelected(newOptions) onChange?.(newOptions) }} className={cn( "cursor-pointer", option.disable && "text-muted-foreground cursor-default" )} > {option.label} </CommandItem> ))} </CommandGroup> ))} </> )} </CommandList> </PopoverContent> </Command> </div> </Popover>
Were you able to make it functional with desired multiselect features?
@zahidiqbalnbs Yep! Its works very well for me, here the demo. We need to fix a bug in the docs but you can see how it works
Hi @flipvh @mamlzy @zahidiqbalnbs @Willienn @tabarra i have created a version using the popover and i have deleted some divs to enable overlay between button used within the popover trigger and the input used to search elements this is my latest version .... IMHO for me it works well if someone like it and when i have time i can make a pull request, let me know guys if it will be useful for you
<Popover open={open} modal={false} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost_neutral"
role="combobox"
aria-expanded={open}
onClick={() => {
if (disabled) return
inputRef.current?.focus()
}}
className={cn(
open ? 'opacity-0 h-[0px] mb-2' : 'opacity-100 h-auto',
'transition-all ease-in-out duration-200 font-body dark:text-dark-neutral-200 w-full justify-between border border-neutral-100 pl-3 text-left text-neutral-950 hover:border-neutral-400 hover:bg-transparent dark:border-neutral-400 dark:hover:border-neutral-50 dark:hover:bg-transparent dark:hover:text-neutral-50'
)}
>
<div className="flex h-2 items-center">
{isEmpty(selected)
? placeholder
: selected.map((s) => s.label).join(', ')}
</div>
<ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
sticky={'always'}
align={'start'}
sideOffset={-20}
alignOffset={10}
collisionPadding={10}
className="popover-content-width-full m-0 p-0"
>
<Command
{...commandProps}
onKeyDown={(e) => {
handleKeyDown(e)
commandProps?.onKeyDown?.(e)
}}
className={cn('w-full overflow-visible', commandProps?.className)}
shouldFilter={
commandProps?.shouldFilter !== undefined
? commandProps.shouldFilter
: !onSearch
} // When onSearch is provided, we don't want to filter the options. You can still override it.
filter={commandFilter()}
>
<div
className={cn(
'h-10 rounded-md border border-input text-sm ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
{
'px-3 py-2': selected.length !== 0,
'cursor-text': !disabled && selected.length !== 0
},
className
)}
onClick={() => {
if (disabled) return
inputRef.current?.focus()
}}
>
<div className="flex flex-wrap gap-1">
{selected.map((option) => {
return (
<Badge
key={option.value}
variant={'select'}
className={cn(
'data-[disabled]:bg-muted-foreground data-[disabled]:text-muted data-[disabled]:hover:bg-muted-foreground',
'data-[fixed]:bg-muted-foreground data-[fixed]:text-muted data-[fixed]:hover:bg-muted-foreground',
badgeClassName
)}
data-fixed={option.fixed}
data-disabled={disabled || undefined}
>
{option.label}
<button
className={cn(
'ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2',
(disabled || option.fixed) && 'hidden'
)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleUnselect(option)
}
}}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={() => handleUnselect(option)}
>
<X className="text-muted-foreground hover:text-foreground size-3" />
</button>
</Badge>
)
})}
{/* Avoid having the "Search" Icon */}
<CommandPrimitive.Input
{...inputProps}
ref={inputRef}
value={inputValue}
disabled={disabled}
onValueChange={(value) => {
setInputValue(value)
inputProps?.onValueChange?.(value)
}}
onBlur={(event) => {
setOpen(false)
inputProps?.onBlur?.(event)
}}
onFocus={(event) => {
setOpen(true)
triggerSearchOnFocus && onSearch?.(debouncedSearchTerm)
inputProps?.onFocus?.(event)
}}
placeholder={
hidePlaceholderWhenSelected && selected.length !== 0
? ''
: placeholder
}
className={cn(
'flex-1 bg-transparent outline-none placeholder:text-muted-foreground',
{
'w-full': hidePlaceholderWhenSelected,
'px-3 py-2': selected.length === 0,
'ml-1': selected.length !== 0
},
inputProps?.className
)}
/>
{isLoading ? <SpinnerUi size="small" className="mx-4" /> : null}
</div>
</div>
<CommandList className={'w-full'}>
<>
<div
className={isLoading ? 'h-auto opacity-100' : 'h-0 opacity-0'}
>
{loadingIndicator}
</div>
{EmptyItem()}
{CreatableItem()}
{!selectFirstItem && (
<CommandItem value="-" className="hidden" />
)}
{Object.entries(selectables).map(([key, dropdowns]) => (
<CommandGroup
key={key}
heading={key}
className="h-full overflow-auto"
>
<>
{dropdowns.map((option) => {
return (
<CommandItem
key={option.value}
value={option.label}
disabled={option.disable}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onSelect={() => {
if (selectMode === 'single') {
setInputValue('')
const newOptions = [option]
setSelected(newOptions)
onChange?.(newOptions)
} else {
if (selected.length >= maxSelected) {
onMaxSelected?.(selected.length)
return
}
setInputValue('')
const newOptions = [...selected, option]
setSelected(newOptions)
onChange?.(newOptions)
}
}}
className={cn(
'cursor-pointer',
option.disable &&
'cursor-default text-muted-foreground'
)}
>
<Check
className={cn(
'mr-2 h-4 w-4',
isUndefined(
selected?.find((v) => {
return v.value === option.value
})
)
? 'opacity-0'
: 'opacity-100'
)}
/>
{option.label}
</CommandItem>
)
})}
</>
</CommandGroup>
))}
</>
</CommandList>
</Command>
</PopoverContent>
</Popover>