Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More accessible using Popover #23

Open
mamlzy opened this issue Jan 22, 2024 · 5 comments
Open

More accessible using Popover #23

mamlzy opened this issue Jan 22, 2024 · 5 comments

Comments

@mamlzy
Copy link

mamlzy commented Jan 22, 2024

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!

@flipvh
Copy link

flipvh commented Mar 26, 2024

Hi @ImamAlfariziSyahputra, I think you are right. Have you by any chance worked on this? Else I will myself probably :D. Thanks

@Willienn
Copy link

Willienn commented May 2, 2024

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>

@zahidiqbalnbs
Copy link

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?

@Willienn
Copy link

Willienn commented May 22, 2024

@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

@muten84
Copy link

muten84 commented Jun 19, 2024

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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants