Files
logseq/packages/ui/@/components/ui/multi-select.tsx
Charlie 8040168d1e Feat: New UI components (#10694)
* enhance(plugin): call apis with the sdk ns

* enhance(plugin): types

* enhance(api): get value from the computed style

* enhance(api): types

* enhance(plugin): types

* enhance(plugin): types

* fix: lint

* fix(apis): incorrect shortcut command registion for block editing mode #10392

* fix(api): types

* enhance(apis): support register shortcuts with multi binding vals

* fix(plugins): normalize command key to make the internal keyword legal

* chore(plugin): build libs core

* chore(plugin): bump version

* enhance(apis): normalize apis cljs data

* chore(plugin): update libs user sdk

* chore(plugin): CHANGELOG.md

* fix: typo

* feat(ui): add package

* Update .gitignore

* feat(ui): set up shui infrastructure

* feat(ui): add storybook macro

* enhance(ui): storybook themes

* feat(ui): adapt ui button to classic

* enhance(ui): shui story

* feat(ui): shui toaster

* enhance(ui): shui toaster

* feat(ui): imperative API for shui toaster

* enhance(shui): update API for shui toaster

* enhance(shui): update hooks for shui toaster

* enhance(shui): remove debug

* feat(ui): story for the shui toaster

* feat(ui): story

* feat(ui): story docs

* feat(ui): more variants for the shui toaster

* feat(ui): story

* fix(ux): support querying plugins with right space chars

* feat(ui): add shui `Alert` component

* enhance(ui): shui demo

* feat(ui): add logseq UI readme

* enhance(ui): default shui theme

* feat(ui): add shui `Badge` component & demo

* fix(ui): outline theme for shui button

* feat(ui): custom icon for the toaster item

* feat(ui): add shui dropdown & demo

* feat(ui): WIP shui form related components

* feat(ui): WIP shui form-related components

* feat(ui): WIP shui form

* feat(ui): WIP shui form state for validation

* fix(ui): missing rounded for ui button

* feat(ui): add yup for shui form as default validation resolver

* enhance(ui): simplify validation schema input for the shui form

* fix(ui): accent ring color for input

* feat(ui): add shui switch

* feat(ui): add shui checkbox & switch

* feat(ui): add shui radio group

* fix(ui): missing file

* feat(ui): add Textarea component

* feat(ui): add shui card & skeleton

* feat(ui): add shui context menu component & demo

* fix(ui): accent color for the context menu item

* feat(ui): add shui select component & demo

* enhance(ui): ui css priority

* feat(ui): add shui calendar & ui details

* feat(ui): add shui popover

* feat(ui): add date picker & demo

* feat(ui): add shui dialog

* feat(ui): WIP add shui dialog

* feat(ui): WIP shui dialog as modal

* feat(ui): WIP imperative APIs for the shui modal

* feat(ui): imperative APIs for the shui modal/alert

* feat(ui): support imperative API alert!/confirm! return promise

* feat(ui): simplify shui components resources

* feat(ui): response layout for the demo ui page

* feat(ui): simplify colors

* feat(ui): simplify colors

* feat(ui): simplify colors

* refactor(ui): WIP Adapt to the new button component

* refactor(ui): polish new button & colors

* fix(ui): the new theme color for the plugin settings nav item link

* fix(ui): blockquote colors

* enhance(ui): more custom colors for shui button

* feat(ui): WIP make logseq green as a theme color

* enhance(ui): polish logseq classical theme color

* fix(ui): theme details of all pages

* enhance(ui): polish logseq theme color for dark mode

* fix(ui): missing table style

* refactor(ui): simplify the all shui buttons & shortcuts for the cmdk component

* fix(ui): missing file

* refactor(ui): clear up stuff

* fix(ui): theme color related issues

* enhance(ui): polish button style

* enhance(ui): polish the keymap setting pane

* fix(ui): hint button from the cmdk pane footer

* fix(ui): logseq colors for the storybook

* enhance(ui): stories for the shui components

* fix(ui): active color for the old toggle component

* enhance(ui): keep the constant size of the settings pane

* fix(ui): polish search input for the plugins pane

* enhance(ui): polish number list bullet colors

* feat(ui): add shui tooltip component

* chore: build ui

* chore(ui): clean up resources

* fix: lint

* fix: lint

* fix: lint

* fix(ui): alignment of the keymap title from the settings pane

* fix: tests

* fix(ui): close button for the classic notification tip

* fix(ui): polish toaster viewport

* enhance(ui): polish the ghost button colors

* enhance(ui): demos for tips

* fix(ui): accent colors for the rc-datepicker

* fix(ui): accent color for the menu item

* refactor(ui): remove unless code for the accent colors

* enhance(ui): polish pdf viewer background color for the accent color mode

* fix: lint

* fix: lint

* fix: lint

* enhance(ui): support button with the custom href link

* enhance(ui): polish aside setting items

* enhance(ui): polish accent color for buttons

* enhance(ui): polish all pages

---------

Co-authored-by: Gabriel Horner <97210743+logseq-cldwalker@users.noreply.github.com>
2023-12-26 16:31:37 +08:00

162 lines
5.1 KiB
TypeScript

import * as React from 'react'
import { cn } from '@/lib/utils'
import { Check, ChevronsUpDown, X } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from '@/components/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
export type OptionType = Record<'value' | 'label', string>
interface MultiSelectProps {
options: Record<'value' | 'label', string>[]
selected: Record<'value' | 'label', string>[]
onChange: React.Dispatch<
React.SetStateAction<Record<'value' | 'label', string>[]>
>
className?: string
placeholder?: string
}
const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(
({ options, selected, onChange, className, ...props }, ref) => {
const [open, setOpen] = React.useState(false)
const [query, setQuery] = React.useState<string>('')
const handleUnselect = (item: Record<'value' | 'label', string>) => {
onChange(selected.filter((i) => i.value !== item.value))
}
// on delete key press, remove last selected item
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Backspace' && query === '' && selected.length > 0) {
onChange(selected.filter((_, index) => index !== selected.length - 1))
}
// close on escape
if (e.key === 'Escape') {
setOpen(false)
}
}
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
}, [onChange, query, selected])
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild className={className}>
<Button
ref={ref}
variant="outline"
role="combobox"
aria-expanded={open}
className={`group w-full justify-between ${
selected.length > 1 ? 'h-full' : 'h-9'
}`}
onClick={() => setOpen(!open)}
>
<div className="flex flex-wrap items-center gap-1">
{selected.map((item) => (
<Badge
variant="outline"
key={item.value}
className="flex items-center gap-1 group-hover:bg-background"
>
{item.label}
{open && (
<Button
asChild
variant="outline"
size="icon"
className={`border-none duration-300 ${
open ? 'opacity-100 ease-in' : 'opacity-0 ease-out'
}`}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleUnselect(item)
}
}}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleUnselect(item)
}}
>
<X className="h-3 w-3 text-muted-foreground hover:text-foreground"/>
</Button>
)}
</Badge>
))}
{selected.length === 0 && (
<span>{props.placeholder ?? 'Select ...'}</span>
)}
</div>
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50"/>
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command className={className}>
<CommandInput
onValueChange={(item) => {
console.log('dd', item)
setQuery(item)
}}
placeholder="Search ..."
/>
<CommandEmpty>No item found.</CommandEmpty>
<CommandGroup className="max-h-64 overflow-auto">
{options.map((option) => (
<CommandItem
key={option.value}
onSelect={() => {
onChange(
selected.some((item) => item.value === option.value)
? selected.filter((item) => item.value !== option.value)
: [...selected, option]
)
setOpen(true)
}}
>
<Check
className={cn(
'mr-2 h-4 w-4',
selected.some((item) => item.value === option.value)
? 'opacity-100'
: 'opacity-0'
)}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
)
}
)
MultiSelect.displayName = 'MultiSelect'
export { MultiSelect }