Skip to content

Commit

Permalink
feat(ui): add Combobox component with demo and documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
tszhong0411 committed Jan 16, 2025
1 parent 7f4807c commit cb1125f
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/cool-birds-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tszhong0411/ui': patch
---

Add Combobox component
48 changes: 48 additions & 0 deletions apps/docs/src/app/ui/components/combobox.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
title: Combobox
description: Autocomplete input and command palette with a list of suggestions.
---

<ComponentPreview name='combobox/combobox' />

## Usage

```tsx
import {
Combobox,
ComboboxContent,
ComboboxControl,
ComboboxInput,
ComboboxItem,
ComboboxItemGroup,
ComboboxItemGroupLabel,
ComboboxItemIndicator,
ComboboxItemText,
ComboboxTrigger
} from '@tszhong0411/ui'
```

```tsx
<Combobox items={['React', 'Vue']}>
<ComboboxLabel>Framework</ComboboxLabel>
<ComboboxControl>
<ComboboxInput placeholder='Select a framework' />
<ComboboxTrigger>
<ChevronsUpDownIcon />
</ComboboxTrigger>
</ComboboxControl>
<ComboboxContent>
<ComboboxItemGroup>
<ComboboxItemGroupLabel>Frameworks</ComboboxItemGroupLabel>
{['React', 'Vue'].map((item) => (
<ComboboxItem key={item} item={item}>
<ComboboxItemText>{item}</ComboboxItemText>
<ComboboxItemIndicator>
<CheckIcon />
</ComboboxItemIndicator>
</ComboboxItem>
))}
</ComboboxItemGroup>
</ComboboxContent>
</Combobox>
```
93 changes: 93 additions & 0 deletions apps/docs/src/components/demos/combobox/combobox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use client'

import {
type ComboboxInputValueChangeDetails,
ComboboxLabel,
createListCollection
} from '@tszhong0411/ui'
import {
Combobox,
ComboboxContent,
ComboboxControl,
ComboboxInput,
ComboboxItem,
ComboboxItemGroup,
ComboboxItemGroupLabel,
ComboboxItemIndicator,
ComboboxItemText,
ComboboxTrigger
} from '@tszhong0411/ui'
import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'
import { useState } from 'react'

const frameworks = [
{
value: 'next.js',
label: 'Next.js'
},
{
value: 'sveltekit',
label: 'SvelteKit',
disabled: true
},
{
value: 'nuxt.js',
label: 'Nuxt.js'
},
{
value: 'remix',
label: 'Remix'
},
{
value: 'astro',
label: 'Astro'
}
]

const initialCollection = createListCollection({ items: frameworks })

const ComboboxDemo = () => {
const [collection, setCollection] = useState(initialCollection)
const handleInputChange = (details: ComboboxInputValueChangeDetails) => {
const filtered = frameworks.filter((item) =>
item.label.toLowerCase().includes(details.inputValue.toLowerCase())
)
if (filtered.length > 0) setCollection(createListCollection({ items: filtered }))
}

const handleOpenChange = () => {
setCollection(initialCollection)
}

return (
<Combobox
className='w-64'
collection={collection}
onInputValueChange={handleInputChange}
onOpenChange={handleOpenChange}
>
<ComboboxLabel>Framework</ComboboxLabel>
<ComboboxControl className='relative'>
<ComboboxInput placeholder='Select a framework' className='pr-6' />
<ComboboxTrigger className='absolute right-2 top-0 h-full'>
<ChevronsUpDownIcon className='size-4 shrink-0 opacity-50' />
</ComboboxTrigger>
</ComboboxControl>
<ComboboxContent>
<ComboboxItemGroup>
<ComboboxItemGroupLabel>Frameworks</ComboboxItemGroupLabel>
{collection.items.map((item) => (
<ComboboxItem key={item.value} item={item}>
<ComboboxItemText>{item.label}</ComboboxItemText>
<ComboboxItemIndicator className='ml-auto'>
<CheckIcon className='size-4' />
</ComboboxItemIndicator>
</ComboboxItem>
))}
</ComboboxItemGroup>
</ComboboxContent>
</Combobox>
)
}

export default ComboboxDemo
4 changes: 4 additions & 0 deletions apps/docs/src/config/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ const COMPONENT_LINKS = [
href: '/ui/components/collapsible',
text: 'Collapsible'
},
{
href: '/ui/components/combobox',
text: 'Combobox'
},
{
href: '/ui/components/command',
text: 'Command'
Expand Down
136 changes: 136 additions & 0 deletions packages/ui/src/combobox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
'use client'

import { Portal } from '@ark-ui/react'
import { Combobox as ComboboxPrimitive } from '@ark-ui/react/combobox'
import { cn } from '@tszhong0411/utils'

const ComboboxContext = ComboboxPrimitive.Context
const ComboboxItemContext = ComboboxPrimitive.ItemContext
const ComboboxControl = ComboboxPrimitive.Control
const ComboboxItemText = ComboboxPrimitive.ItemText
const ComboboxItemIndicator = ComboboxPrimitive.ItemIndicator
const ComboboxTrigger = ComboboxPrimitive.Trigger
const ComboboxClearTrigger = ComboboxPrimitive.ClearTrigger
const ComboboxList = ComboboxPrimitive.List
const ComboboxItemGroup = ComboboxPrimitive.ItemGroup

type ComboboxProps = React.ComponentProps<typeof ComboboxPrimitive.Root>

const Combobox = (props: ComboboxProps) => {
const { openOnClick = true, ...rest } = props

return <ComboboxPrimitive.Root openOnClick={openOnClick} {...rest} />
}

type ComboboxInputProps = React.ComponentProps<typeof ComboboxPrimitive.Input>

const ComboboxInput = (props: ComboboxInputProps) => {
const { className, ...rest } = props

return (
<ComboboxPrimitive.Input
className={cn(
'border-input bg-background ring-offset-background flex h-10 w-full items-center justify-between rounded-lg border px-3 py-2 text-sm',
'placeholder:text-muted-foreground',
'focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
'disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...rest}
/>
)
}

type ComboboxLabelProps = React.ComponentProps<typeof ComboboxPrimitive.Label>

const ComboboxLabel = (props: ComboboxLabelProps) => {
const { className, ...rest } = props

return (
<ComboboxPrimitive.Label
className={cn('text-sm font-medium leading-none', className)}
{...rest}
/>
)
}

type ComboboxContentProps = React.ComponentProps<typeof ComboboxPrimitive.Content>

const ComboboxContent = (props: ComboboxContentProps) => {
const { className, ...rest } = props

return (
<Portal>
<ComboboxPrimitive.Positioner>
<ComboboxPrimitive.Content
className={cn(
'bg-popover text-popover-foreground z-50 min-w-32 overflow-hidden rounded-lg border p-1 shadow-lg',
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
className
)}
{...rest}
/>
</ComboboxPrimitive.Positioner>
</Portal>
)
}

type ComboboxItemGroupLabelProps = React.ComponentProps<typeof ComboboxPrimitive.ItemGroupLabel>

const ComboboxItemGroupLabel = (props: ComboboxItemGroupLabelProps) => {
const { className, ...rest } = props

return (
<ComboboxPrimitive.ItemGroupLabel
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
{...rest}
/>
)
}

type ComboboxItemProps = React.ComponentProps<typeof ComboboxPrimitive.Item>

const ComboboxItem = (props: ComboboxItemProps) => {
const { className, ...rest } = props

return (
<ComboboxPrimitive.Item
className={cn(
'relative flex cursor-default select-none items-center rounded-md px-2 py-1.5 text-sm outline-none transition-colors',
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground',
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
className
)}
{...rest}
/>
)
}

export {
Combobox,
ComboboxClearTrigger,
ComboboxContent,
ComboboxContext,
ComboboxControl,
ComboboxInput,
ComboboxItem,
ComboboxItemContext,
ComboboxItemGroup,
ComboboxItemGroupLabel,
ComboboxItemIndicator,
ComboboxItemText,
ComboboxLabel,
ComboboxList,
ComboboxTrigger
}

export type {
CollectionItem,
ComboboxHighlightChangeDetails,
ComboboxInputValueChangeDetails,
ComboboxOpenChangeDetails,
ComboboxValueChangeDetails,
ListCollection
} from '@ark-ui/react/combobox'
export { createListCollection } from '@ark-ui/react/combobox'
1 change: 1 addition & 0 deletions packages/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from './carousel'
export * from './checkbox'
export * from './code-block'
export * from './collapsible'
export * from './combobox'
export * from './command'
export * from './context-menu'
export * from './data-table'
Expand Down

0 comments on commit cb1125f

Please sign in to comment.