mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
[Design System] feat: Multi Select (#26719)
* chore: deprecate old files. add new one with first story * init multi select * docs * Update multi-select.mdx * deprecation notices * Delete MultiSelect.stories.tsx
This commit is contained in:
1 parent
d1037d95ba
commit
6dff3a7369
18 files changed
+646
-7
No files matched your search
@@ -1611,6 +1611,28 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"multi-select-demo": {
|
||||
name: "multi-select-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/multi-select-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/multi-select-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"multi-select-form": {
|
||||
name: "multi-select-form",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/multi-select-form")),
|
||||
source: "",
|
||||
files: ["registry/default/example/multi-select-form.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"color-usage-surface-www-and-docs": {
|
||||
name: "color-usage-surface-www-and-docs",
|
||||
type: "docs:example",
|
||||
|
||||
@@ -87,6 +87,11 @@ export const docsConfig: DocsConfig = {
|
||||
href: '/docs/fragments/form-item-layout',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: 'Multi Select',
|
||||
href: '/docs/fragments/multi-select',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: Multi Select
|
||||
description: Multiple selection component.
|
||||
component: true
|
||||
fragment: true
|
||||
---
|
||||
|
||||
<ComponentPreview name="multi-select-demo" description="" peekCode wide />
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import {
|
||||
MultiSelector,
|
||||
MultiSelectorContent,
|
||||
MultiSelectorInput,
|
||||
MultiSelectorItem,
|
||||
MultiSelectorList,
|
||||
MultiSelectorTrigger,
|
||||
} from 'ui-patterns/multi-select'
|
||||
```
|
||||
|
||||
```tsx
|
||||
<MultiSelector values={value} onValuesChange={setValue}>
|
||||
<MultiSelectorTrigger>
|
||||
<MultiSelectorInput placeholder="Select items" />
|
||||
</MultiSelectorTrigger>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
<MultiSelectorItem value="1">Item 1</MultiSelectorItem>
|
||||
<MultiSelectorItem value="2">Item 2</MultiSelectorItem>
|
||||
<MultiSelectorItem value="3">Item 3</MultiSelectorItem>
|
||||
</MultiSelectorList>
|
||||
</MultiSelectorContent>
|
||||
</MultiSelector>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentPreview name="multi-select-form" />
|
||||
@@ -14,6 +14,10 @@ const nextConfig = {
|
||||
protocol: 'https',
|
||||
hostname: 'images.unsplash.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'pbs.twimg.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
async redirects() {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
MultiSelector,
|
||||
MultiSelectorContent,
|
||||
MultiSelectorInput,
|
||||
MultiSelectorItem,
|
||||
MultiSelectorList,
|
||||
MultiSelectorTrigger,
|
||||
} from 'ui-patterns/multi-select'
|
||||
|
||||
export default function MultiSelectDemo() {
|
||||
const [value, setValue] = useState<string[]>([])
|
||||
|
||||
return (
|
||||
<MultiSelector values={value} onValuesChange={setValue} size="small">
|
||||
<MultiSelectorTrigger>
|
||||
<MultiSelectorInput placeholder="Select items" />
|
||||
</MultiSelectorTrigger>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
<MultiSelectorItem value="1">Item 1</MultiSelectorItem>
|
||||
<MultiSelectorItem value="2">Item 2</MultiSelectorItem>
|
||||
<MultiSelectorItem value="3">Item 3</MultiSelectorItem>
|
||||
</MultiSelectorList>
|
||||
</MultiSelectorContent>
|
||||
</MultiSelector>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
MultiSelector,
|
||||
MultiSelectorContent,
|
||||
MultiSelectorInput,
|
||||
MultiSelectorItem,
|
||||
MultiSelectorList,
|
||||
MultiSelectorTrigger,
|
||||
} from 'ui-patterns/multi-select'
|
||||
import {
|
||||
Form_Shadcn_,
|
||||
FormDescription_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
FormItem_Shadcn_,
|
||||
FormLabel_Shadcn_,
|
||||
FormMessage_Shadcn_,
|
||||
useToast,
|
||||
} from 'ui'
|
||||
import z from 'zod'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { toast } from 'ui'
|
||||
import { Button } from 'ui'
|
||||
import Image from 'next/image'
|
||||
|
||||
const form = z.object({
|
||||
value: z.array(z.string()).nonempty('Please select at least one person'),
|
||||
})
|
||||
|
||||
type Form = z.infer<typeof form>
|
||||
|
||||
const users = [
|
||||
{
|
||||
name: 'ThePrimeagen',
|
||||
picture: 'https://pbs.twimg.com/profile_images/1759330620160049152/2i_wkOoK_400x400.jpg',
|
||||
},
|
||||
{
|
||||
name: 'Shadcn',
|
||||
picture: 'https://pbs.twimg.com/profile_images/1593304942210478080/TUYae5z7_400x400.jpg',
|
||||
},
|
||||
{
|
||||
name: 'Theo',
|
||||
picture: 'https://pbs.twimg.com/profile_images/1605762947686375425/lsoGWWty_400x400.jpg',
|
||||
},
|
||||
]
|
||||
|
||||
const MultiSelectZod = () => {
|
||||
const multiForm = useForm<Form>({
|
||||
resolver: zodResolver(form),
|
||||
defaultValues: form.parse({ value: [users[0].name] }),
|
||||
})
|
||||
|
||||
const { toast } = useToast()
|
||||
const onSubmit = (data: Form) => {
|
||||
toast({
|
||||
title: 'Form submitted ',
|
||||
description: JSON.stringify(data, null, 2),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form_Shadcn_ {...multiForm}>
|
||||
<form onSubmit={multiForm.handleSubmit(onSubmit)} className="space-y-3 grid gap-3 w-full">
|
||||
<FormField_Shadcn_
|
||||
control={multiForm.control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_ className="w-full">
|
||||
<FormLabel_Shadcn_>Invite people</FormLabel_Shadcn_>
|
||||
<MultiSelector onValuesChange={field.onChange} values={field.value} size={'small'}>
|
||||
<MultiSelectorTrigger>
|
||||
<MultiSelectorInput placeholder="Select people to invite" />
|
||||
</MultiSelectorTrigger>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
{users.map((user) => (
|
||||
<MultiSelectorItem key={user.name} value={user.name}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Image
|
||||
src={user.picture}
|
||||
alt={user.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
<span>{user.name}</span>
|
||||
</div>
|
||||
</MultiSelectorItem>
|
||||
))}
|
||||
</MultiSelectorList>
|
||||
</MultiSelectorContent>
|
||||
</MultiSelector>
|
||||
<FormDescription_Shadcn_>
|
||||
Select people to invite to this event
|
||||
</FormDescription_Shadcn_>
|
||||
<FormMessage_Shadcn_ />
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
<Button type="secondary" size="medium" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
export default MultiSelectZod
|
||||
@@ -943,6 +943,16 @@ export const examples: Registry = [
|
||||
type: 'components:example',
|
||||
files: ['example/form-item-layout-with-switch.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'multi-select-demo',
|
||||
type: 'components:example',
|
||||
files: ['example/multi-select-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'multi-select-form',
|
||||
type: 'components:example',
|
||||
files: ['example/multi-select-form.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'color-usage-surface-www-and-docs',
|
||||
type: 'docs:example',
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ import {
|
||||
SelectTrigger_Shadcn_,
|
||||
Select_Shadcn_,
|
||||
} from 'ui'
|
||||
import { MultiSelectV2 } from 'ui-patterns/MultiSelect/MultiSelectV2'
|
||||
import { MultiSelectV2 } from 'ui-patterns/MultiSelectDeprecated/MultiSelectV2'
|
||||
|
||||
interface PolicyDetailsV2Props {
|
||||
schema: string
|
||||
|
||||
@@ -2,7 +2,7 @@ import { SYSTEM_ROLES } from 'components/interfaces/Database/Roles/Roles.constan
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
|
||||
import MultiSelect from 'ui-patterns/MultiSelect'
|
||||
import MultiSelect from 'ui-patterns/MultiSelectDeprecated'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
|
||||
import { sortBy } from 'lodash'
|
||||
|
||||
@@ -32,8 +32,8 @@ import {
|
||||
SidePanel,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { MultiSelectOption } from 'ui-patterns/MultiSelect'
|
||||
import { MultiSelectV2 } from 'ui-patterns/MultiSelect/MultiSelectV2'
|
||||
import { MultiSelectOption } from 'ui-patterns/MultiSelectDeprecated'
|
||||
import { MultiSelectV2 } from 'ui-patterns/MultiSelectDeprecated/MultiSelectV2'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { INDEX_TYPES } from './Indexes.constants'
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
Input,
|
||||
InputNumber,
|
||||
} from 'ui'
|
||||
import { MultiSelectV2 } from 'ui-patterns/MultiSelect/MultiSelectV2'
|
||||
import { MultiSelectV2 } from 'ui-patterns/MultiSelectDeprecated/MultiSelectV2'
|
||||
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
Listbox,
|
||||
Separator,
|
||||
} from 'ui'
|
||||
import MultiSelect from 'ui-patterns/MultiSelect'
|
||||
import MultiSelect from 'ui-patterns/MultiSelectDeprecated'
|
||||
import DisabledStateForFreeTier from './DisabledStateForFreeTier'
|
||||
import { CATEGORY_OPTIONS, SERVICE_OPTIONS, SEVERITY_OPTIONS } from './Support.constants'
|
||||
import { formatMessage, uploadAttachments } from './SupportForm.utils'
|
||||
|
||||
+6
@@ -1,5 +1,8 @@
|
||||
import { IconX } from 'ui'
|
||||
|
||||
/**
|
||||
* @deprecated Use ./multi-select instead
|
||||
*/
|
||||
export const BadgeDisabled = ({ name }: { name: string }) => (
|
||||
<div
|
||||
className={[
|
||||
@@ -12,6 +15,9 @@ export const BadgeDisabled = ({ name }: { name: string }) => (
|
||||
</div>
|
||||
)
|
||||
|
||||
/**
|
||||
* @deprecated Use ./multi-select instead
|
||||
*/
|
||||
export const BadgeSelected = ({
|
||||
name,
|
||||
handleRemove,
|
||||
+3
@@ -33,6 +33,9 @@ interface MultiSelectProps {
|
||||
onChange?(x: string[]): void
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use ./multi-select instead
|
||||
*/
|
||||
export const MultiSelectV2 = ({
|
||||
options,
|
||||
value,
|
||||
+1
-1
@@ -44,7 +44,7 @@ interface Props {
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated To be replaced with MultiSelectV2
|
||||
* @deprecated Use ./multi-select instead
|
||||
*/
|
||||
export default function MultiSelect({
|
||||
options,
|
||||
@@ -0,0 +1 @@
|
||||
export * from './multi-select'
|
||||
@@ -0,0 +1,375 @@
|
||||
'use client'
|
||||
|
||||
import { Badge } from 'ui'
|
||||
import {
|
||||
Command,
|
||||
CommandItem,
|
||||
CommandEmpty,
|
||||
CommandList,
|
||||
} from 'ui/src/components/shadcn/ui/command'
|
||||
import { cn } from 'ui/src/lib/utils/cn'
|
||||
import { Command as CommandPrimitive } from 'cmdk'
|
||||
import { X as RemoveIcon, Check } from 'lucide-react'
|
||||
import React, {
|
||||
KeyboardEvent,
|
||||
createContext,
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { CommandInput } from 'cmdk'
|
||||
import {
|
||||
SIZE,
|
||||
SIZE_VARIANTS,
|
||||
SIZE_VARIANTS_DEFAULT,
|
||||
SIZE_VARIANTS_INNER,
|
||||
} from 'ui/src/lib/constants'
|
||||
import { VariantProps, cva } from 'class-variance-authority'
|
||||
import { sizeVariants } from 'ui/src/lib/commonCva'
|
||||
|
||||
const MultiSelectorVariants = cva('', {
|
||||
variants: {
|
||||
size: {
|
||||
...SIZE_VARIANTS,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: SIZE_VARIANTS_DEFAULT,
|
||||
},
|
||||
})
|
||||
|
||||
type MultiSelectorProps = {
|
||||
values: string[]
|
||||
onValuesChange: (value: string[]) => void
|
||||
loop?: boolean
|
||||
} & React.ComponentPropsWithoutRef<typeof Command> &
|
||||
VariantProps<typeof MultiSelectorVariants>
|
||||
|
||||
interface MultiSelectContextProps {
|
||||
value: string[]
|
||||
onValueChange: (value: any) => void
|
||||
open: boolean
|
||||
setOpen: (value: boolean) => void
|
||||
inputValue: string
|
||||
setInputValue: React.Dispatch<React.SetStateAction<string>>
|
||||
activeIndex: number
|
||||
setActiveIndex: React.Dispatch<React.SetStateAction<number>>
|
||||
size: MultiSelectorProps['size']
|
||||
}
|
||||
|
||||
const MultiSelectContext = createContext<MultiSelectContextProps | null>(null)
|
||||
|
||||
const useMultiSelect = () => {
|
||||
const context = useContext(MultiSelectContext)
|
||||
if (!context) {
|
||||
throw new Error('useMultiSelect must be used within MultiSelectProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
const MultiSelector = ({
|
||||
values: value,
|
||||
onValuesChange: onValueChange,
|
||||
loop = false,
|
||||
className,
|
||||
children,
|
||||
dir,
|
||||
size = 'small',
|
||||
...props
|
||||
}: MultiSelectorProps) => {
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
const [activeIndex, setActiveIndex] = useState<number>(-1)
|
||||
|
||||
const onValueChangeHandler = useCallback(
|
||||
(val: string) => {
|
||||
if (value.includes(val)) {
|
||||
onValueChange(value.filter((item) => item !== val))
|
||||
} else {
|
||||
onValueChange([...value, val])
|
||||
}
|
||||
},
|
||||
[value]
|
||||
)
|
||||
|
||||
// TODO : change from else if use to switch case statement
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLDivElement>) => {
|
||||
const moveNext = () => {
|
||||
const nextIndex = activeIndex + 1
|
||||
setActiveIndex(nextIndex > value.length - 1 ? (loop ? 0 : -1) : nextIndex)
|
||||
}
|
||||
|
||||
const movePrev = () => {
|
||||
const prevIndex = activeIndex - 1
|
||||
setActiveIndex(prevIndex < 0 ? value.length - 1 : prevIndex)
|
||||
}
|
||||
|
||||
if ((e.key === 'Backspace' || e.key === 'Delete') && value.length > 0) {
|
||||
if (inputValue.length === 0) {
|
||||
if (activeIndex !== -1 && activeIndex < value.length) {
|
||||
onValueChange(value.filter((item) => item !== value[activeIndex]))
|
||||
const newIndex = activeIndex - 1 < 0 ? 0 : activeIndex - 1
|
||||
setActiveIndex(newIndex)
|
||||
} else {
|
||||
onValueChange(value.filter((item) => item !== value[value.length - 1]))
|
||||
}
|
||||
}
|
||||
} else if (e.key === 'Enter') {
|
||||
setOpen(true)
|
||||
} else if (e.key === 'Escape') {
|
||||
if (activeIndex !== -1) {
|
||||
setActiveIndex(-1)
|
||||
} else {
|
||||
setOpen(false)
|
||||
}
|
||||
} else if (dir === 'rtl') {
|
||||
if (e.key === 'ArrowRight') {
|
||||
movePrev()
|
||||
} else if (e.key === 'ArrowLeft' && (activeIndex !== -1 || loop)) {
|
||||
moveNext()
|
||||
}
|
||||
} else {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
movePrev()
|
||||
} else if (e.key === 'ArrowRight' && (activeIndex !== -1 || loop)) {
|
||||
moveNext()
|
||||
}
|
||||
}
|
||||
},
|
||||
[value, inputValue, activeIndex, loop]
|
||||
)
|
||||
|
||||
return (
|
||||
<MultiSelectContext.Provider
|
||||
value={{
|
||||
value,
|
||||
onValueChange: onValueChangeHandler,
|
||||
open,
|
||||
setOpen,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
activeIndex,
|
||||
setActiveIndex,
|
||||
size: size || 'small',
|
||||
}}
|
||||
>
|
||||
<CommandPrimitive
|
||||
onKeyDown={handleKeyDown}
|
||||
className={cn('overflow-visible bg-transparent flex flex-col', className)}
|
||||
dir={dir}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</CommandPrimitive>
|
||||
</MultiSelectContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ButtonVariantProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
const MultiSelectorTriggerBadgeVariants = cva(
|
||||
`
|
||||
gap-2
|
||||
w-content
|
||||
my-[3px]
|
||||
bg-overlay dark:bg-surface-300
|
||||
border border-control
|
||||
bg-opacity-100
|
||||
rounded-lg
|
||||
items-center
|
||||
flex text-foreground-light
|
||||
border
|
||||
`,
|
||||
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
...SIZE_VARIANTS_INNER,
|
||||
},
|
||||
active: {
|
||||
true: 'ring-2 ring-muted-foreground',
|
||||
false: '',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: SIZE_VARIANTS_DEFAULT,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const MultiSelectorTrigger = forwardRef<HTMLDivElement, ButtonVariantProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
const { value, onValueChange, activeIndex, size } = useMultiSelect()
|
||||
|
||||
const mousePreventDefault = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-wrap gap-1 border border-control bg-foreground/[.026] rounded-lg px-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{value.map((item, index) => (
|
||||
<div
|
||||
key={item}
|
||||
className={cn(
|
||||
MultiSelectorTriggerBadgeVariants({ size: size, active: activeIndex === index })
|
||||
)}
|
||||
>
|
||||
<span>{item}</span>
|
||||
<button
|
||||
aria-label={`Remove ${item} option`}
|
||||
aria-roledescription="button to remove option"
|
||||
type="button"
|
||||
onMouseDown={mousePreventDefault}
|
||||
onClick={() => onValueChange(item)}
|
||||
>
|
||||
<span className="sr-only">Remove {item} option</span>
|
||||
<RemoveIcon
|
||||
className="transition-colors h-3 w-3 text-foreground-muted hover:text-destructive"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
MultiSelectorTrigger.displayName = 'MultiSelectorTrigger'
|
||||
|
||||
const MultiSelectorInputVariants = cva('bg-control border', {
|
||||
variants: {
|
||||
size: {
|
||||
...SIZE_VARIANTS,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: SIZE_VARIANTS_DEFAULT,
|
||||
},
|
||||
})
|
||||
|
||||
const MultiSelectorInput = forwardRef<
|
||||
React.ElementRef<typeof CommandInput>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandInput>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { setOpen, inputValue, setInputValue, activeIndex, setActiveIndex, size } = useMultiSelect()
|
||||
return (
|
||||
<CommandInput
|
||||
{...props}
|
||||
ref={ref}
|
||||
value={inputValue}
|
||||
onValueChange={activeIndex === -1 ? setInputValue : undefined}
|
||||
onBlur={() => setOpen(false)}
|
||||
onFocus={() => setOpen(true)}
|
||||
onClick={() => setActiveIndex(-1)}
|
||||
className={cn(
|
||||
MultiSelectorInputVariants({ size }),
|
||||
'text-sm bg-transparent border-none outline-none placeholder:text-foreground-muted flex-1',
|
||||
className,
|
||||
activeIndex !== -1 && 'caret-transparent'
|
||||
)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
MultiSelectorInput.displayName = 'MultiSelectorInput'
|
||||
|
||||
const MultiSelectorContent = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ children }, ref) => {
|
||||
const { open } = useMultiSelect()
|
||||
return (
|
||||
<div ref={ref} className="relative">
|
||||
{open && children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
MultiSelectorContent.displayName = 'MultiSelectorContent'
|
||||
|
||||
const MultiSelectorList = forwardRef<
|
||||
React.ElementRef<typeof CommandList>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandList>
|
||||
>(({ className, children }, ref) => {
|
||||
return (
|
||||
<CommandList
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'p-2 flex flex-col gap-2 rounded-md scrollbar-thin scrollbar-track-transparent transition-colors scrollbar-thumb-muted-foreground dark:scrollbar-thumb-muted scrollbar-thumb-rounded-lg w-full absolute bg-overlay shadow-md z-10 border border-overlay top-1',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<CommandEmpty>
|
||||
<span className="text-foreground-muted">No results found</span>
|
||||
</CommandEmpty>
|
||||
</CommandList>
|
||||
)
|
||||
})
|
||||
|
||||
MultiSelectorList.displayName = 'MultiSelectorList'
|
||||
|
||||
const MultiSelectorItem = forwardRef<
|
||||
React.ElementRef<typeof CommandItem>,
|
||||
{ value: string } & React.ComponentPropsWithoutRef<typeof CommandItem>
|
||||
>(({ className, value, children, ...props }, ref) => {
|
||||
const { value: Options, onValueChange, setInputValue, size } = useMultiSelect()
|
||||
|
||||
const mousePreventDefault = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}, [])
|
||||
|
||||
const isIncluded = Options.includes(value)
|
||||
return (
|
||||
<CommandItem
|
||||
ref={ref}
|
||||
{...props}
|
||||
onSelect={() => {
|
||||
onValueChange(value)
|
||||
setInputValue('')
|
||||
}}
|
||||
className={cn(
|
||||
'relative',
|
||||
'text-foreground-light',
|
||||
'hover:text-foreground',
|
||||
'rounded-md cursor-pointer px-2 py-1 transition-colors flex justify-between',
|
||||
size && SIZE.text[size],
|
||||
className,
|
||||
isIncluded && 'cursor-default',
|
||||
props.disabled && 'opacity-75 cursor-not-allowed'
|
||||
)}
|
||||
onMouseDown={mousePreventDefault}
|
||||
>
|
||||
<div className={cn(isIncluded && 'opacity-50')}>{children}</div>
|
||||
{isIncluded && (
|
||||
<div className="w-5 h-5 bg-foreground left-2 rounded-full flex items-center justify-center">
|
||||
<Check className="h-3 w-3 text-background" strokeWidth={5} />
|
||||
</div>
|
||||
)}
|
||||
</CommandItem>
|
||||
)
|
||||
})
|
||||
|
||||
MultiSelectorItem.displayName = 'MultiSelectorItem'
|
||||
|
||||
export {
|
||||
MultiSelector,
|
||||
MultiSelectorTrigger,
|
||||
MultiSelectorInput,
|
||||
MultiSelectorContent,
|
||||
MultiSelectorList,
|
||||
MultiSelectorItem,
|
||||
}
|
||||
@@ -66,6 +66,32 @@ export const SIZE = {
|
||||
},
|
||||
}
|
||||
|
||||
// used for internal badges/buttons
|
||||
// such as Button Group Items or Multi Select items
|
||||
export const SIZE_INNER = {
|
||||
text: {
|
||||
tiny: 'text-xs',
|
||||
small: 'text-sm leading-4',
|
||||
medium: 'text-sm',
|
||||
large: 'text-base',
|
||||
xlarge: 'text-base',
|
||||
},
|
||||
padding: {
|
||||
tiny: 'px-2.5 py-1',
|
||||
small: 'px-3 py-2',
|
||||
medium: 'px-4 py-2',
|
||||
large: 'px-4 py-2',
|
||||
xlarge: 'px-6 py-3',
|
||||
},
|
||||
height: {
|
||||
tiny: 'h-[24px]',
|
||||
small: 'h-[28px]',
|
||||
medium: 'h-[32px]',
|
||||
large: 'h-[36px]',
|
||||
xlarge: 'h-[44px]',
|
||||
},
|
||||
}
|
||||
|
||||
export const SIZE_VARIANTS = {
|
||||
tiny: `${SIZE.text['tiny']} ${SIZE.padding['tiny']} ${SIZE.height['tiny']}`,
|
||||
small: `${SIZE.text['small']} ${SIZE.padding['small']} ${SIZE.height['small']}`,
|
||||
@@ -74,4 +100,12 @@ export const SIZE_VARIANTS = {
|
||||
xlarge: `${SIZE.text['xlarge']} ${SIZE.padding['xlarge']} ${SIZE.height['xlarge']}`,
|
||||
}
|
||||
|
||||
export const SIZE_VARIANTS_INNER = {
|
||||
tiny: `${SIZE.text['tiny']} ${SIZE.padding['tiny']} ${SIZE_INNER.height['tiny']}`,
|
||||
small: `${SIZE.text['small']} ${SIZE.padding['small']} ${SIZE_INNER.height['small']}`,
|
||||
medium: `${SIZE.text['medium']} ${SIZE.padding['medium']} ${SIZE_INNER.height['medium']}`,
|
||||
large: `${SIZE.text['large']} ${SIZE.padding['large']} ${SIZE_INNER.height['large']}`,
|
||||
xlarge: `${SIZE.text['xlarge']} ${SIZE.padding['xlarge']} ${SIZE_INNER.height['xlarge']}`,
|
||||
}
|
||||
|
||||
export const SIZE_VARIANTS_DEFAULT = 'small'
|
||||
Reference in new issue
Block a user