Skip to content

Commit

Permalink
feat: add combo box component (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
PHILLIPS71 authored May 3, 2024
1 parent dbaaa3e commit e5b3366
Show file tree
Hide file tree
Showing 9 changed files with 388 additions and 0 deletions.
82 changes: 82 additions & 0 deletions packages/react/src/components/combo-box/ComboBox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { ComboBoxProps } from '@/components/combo-box'
import type { Meta, StoryFn } from '@storybook/react'

import { combobox } from '@giantnodes/theme'

import { Typography } from '../typography'

import { Avatar } from '@/components/avatar'
import { ComboBox } from '@/components/combo-box'
import { Input } from '@/components/input'

const Component: Meta<typeof ComboBox> = {
title: 'Components/ComboBox',
component: ComboBox,
argTypes: {
size: {
control: { type: 'select' },
},
status: {
control: { type: 'select' },
},
variant: {
control: { type: 'select' },
},
},
}

const defaultProps = {
...combobox.defaultVariants,
}

const people = [
{ id: 1, name: 'Raymond Chappell' },
{ id: 2, name: 'David Bolden' },
{ id: 3, name: 'Charles Marcano' },
{ id: 4, name: 'Jeanne Livesay' },
{ id: 5, name: 'David Bolden' },
]

export const Default: StoryFn<ComboBoxProps<object>> = (args) => (
<ComboBox {...args}>
<Input.Group>
<Input placeholder="People" type="text" />
</Input.Group>

<ComboBox.Popover>
<ComboBox.List items={people}>{(item) => <ComboBox.Item key={item.id}>{item.name}</ComboBox.Item>}</ComboBox.List>
</ComboBox.Popover>
</ComboBox>
)

Default.args = {
...defaultProps,
}

export const Custom: StoryFn<ComboBoxProps<object>> = (args) => (
<ComboBox {...args}>
<Input.Group>
<Input placeholder="People" type="text" />
</Input.Group>

<ComboBox.Popover>
<ComboBox.List items={people}>
{(item) => (
<ComboBox.Item key={item.id} className="flex items-center gap-2" textValue={item.name}>
<Avatar size="xs">
<Avatar.Image alt={item.name} src={`https://api.dicebear.com/8.x/personas/svg?seed=${item.name}`} />
</Avatar>

<Typography.Text slot="label">{item.name}</Typography.Text>
</ComboBox.Item>
)}
</ComboBox.List>
</ComboBox.Popover>
</ComboBox>
)

Custom.args = {
...defaultProps,
}

export default Component
60 changes: 60 additions & 0 deletions packages/react/src/components/combo-box/ComboBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type * as Polymophic from '@/utilities/polymorphic'
import type { ComboBoxVariantProps } from '@giantnodes/theme'
import type { ComboBoxProps } from 'react-aria-components'

import React from 'react'
import { ComboBox } from 'react-aria-components'

import ComboBoxItem from '@/components/combo-box/ComboBoxItem'
import ComboBoxList from '@/components/combo-box/ComboBoxList'
import ComboBoxPopover from '@/components/combo-box/ComboBoxPopover'
import { ComboBoxContext, useComboBox } from '@/components/combo-box/use-combo-box.hook'

const __ELEMENT_TYPE__ = 'div'

type ComponentOwnProps<TData extends object> = ComboBoxProps<TData> & ComboBoxVariantProps

type ComponentProps<
TData extends object,
TElement extends React.ElementType = typeof __ELEMENT_TYPE__,
> = Polymophic.ComponentPropsWithRef<TElement, ComponentOwnProps<TData>>

type ComponentType = <TData extends object, TElement extends React.ElementType = typeof __ELEMENT_TYPE__>(
props: ComponentProps<TData, TElement>
) => React.ReactNode

const Component: ComponentType = React.forwardRef(
<TData extends object, TElement extends React.ElementType = typeof __ELEMENT_TYPE__>(
props: ComponentProps<TData, TElement>,
ref: Polymophic.Ref<TElement>
) => {
const { as, children, className, size, status, ...rest } = props

const Element = as || ComboBox

const context = useComboBox({ size, status })

const component = React.useMemo<ComboBoxProps<TData>>(
() => ({
className: context.slots.combobox({ className: className?.toString() }),
...rest,
}),
[className, context.slots, rest]
)

return (
<ComboBoxContext.Provider value={context}>
<Element {...component} ref={ref}>
{children}
</Element>
</ComboBoxContext.Provider>
)
}
)

export type { ComponentOwnProps as ComboBoxOwnProps, ComponentProps as ComboBoxProps }
export default Object.assign(Component, {
Popover: ComboBoxPopover,
List: ComboBoxList,
Item: ComboBoxItem,
})
50 changes: 50 additions & 0 deletions packages/react/src/components/combo-box/ComboBoxItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type * as Polymophic from '@/utilities/polymorphic'
import type { ListBoxItemProps } from 'react-aria-components'

import React from 'react'
import { ListBoxItem } from 'react-aria-components'

import { useComboBoxContext } from '@/components/combo-box/use-combo-box.hook'

const __ELEMENT_TYPE__ = 'div'

type ComponentOwnProps = ListBoxItemProps

type ComponentProps<TElement extends React.ElementType = typeof __ELEMENT_TYPE__> = Polymophic.ComponentPropsWithRef<
TElement,
ComponentOwnProps
>

type ComponentType = <TElement extends React.ElementType = typeof __ELEMENT_TYPE__>(
props: ComponentProps<TElement>
) => React.ReactNode

const Component: ComponentType = React.forwardRef(
<TElement extends React.ElementType = typeof __ELEMENT_TYPE__>(
props: ComponentProps<TElement>,
ref: Polymophic.Ref<TElement>
) => {
const { as, children, className, ...rest } = props

const Element = as || ListBoxItem

const { slots } = useComboBoxContext()

const component = React.useMemo<ListBoxItemProps>(
() => ({
className: slots.item({ className: className?.toString() }),
...rest,
}),
[className, rest, slots]
)

return (
<Element {...component} ref={ref}>
{children}
</Element>
)
}
)

export type { ComponentOwnProps as ComboBoxItemOwnProps, ComponentProps as ComboBoxItemProps }
export default Component
50 changes: 50 additions & 0 deletions packages/react/src/components/combo-box/ComboBoxList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type * as Polymophic from '@/utilities/polymorphic'
import type { ListBoxProps } from 'react-aria-components'

import React from 'react'
import { ListBox } from 'react-aria-components'

import { useComboBoxContext } from '@/components/combo-box/use-combo-box.hook'

const __ELEMENT_TYPE__ = 'div'

type ComponentOwnProps<TData extends object> = ListBoxProps<TData>

type ComponentProps<
TData extends object,
TElement extends React.ElementType = typeof __ELEMENT_TYPE__,
> = Polymophic.ComponentPropsWithRef<TElement, ComponentOwnProps<TData>>

type ComponentType = <TData extends object, TElement extends React.ElementType = typeof __ELEMENT_TYPE__>(
props: ComponentProps<TData, TElement>
) => React.ReactNode

const Component: ComponentType = React.forwardRef(
<D extends object, TElement extends React.ElementType = typeof __ELEMENT_TYPE__>(
props: ComponentProps<D, TElement>,
ref: Polymophic.Ref<TElement>
) => {
const { as, children, className, ...rest } = props

const Element = as || ListBox

const { slots } = useComboBoxContext()

const component = React.useMemo<ListBoxProps<D>>(
() => ({
className: slots.list({ className: className?.toString() }),
...rest,
}),
[className, rest, slots]
)

return (
<Element {...component} ref={ref}>
{children}
</Element>
)
}
)

export type { ComponentOwnProps as ComboBoxListOwnProps, ComponentProps as ComboBoxListProps }
export default Component
50 changes: 50 additions & 0 deletions packages/react/src/components/combo-box/ComboBoxPopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type * as Polymophic from '@/utilities/polymorphic'
import type { PopoverProps } from 'react-aria-components'

import React from 'react'
import { Popover } from 'react-aria-components'

import { useComboBoxContext } from '@/components/combo-box/use-combo-box.hook'

const __ELEMENT_TYPE__ = 'div'

type ComponentOwnProps = PopoverProps

type ComponentProps<TElement extends React.ElementType = typeof __ELEMENT_TYPE__> = Polymophic.ComponentPropsWithRef<
TElement,
ComponentOwnProps
>

type ComponentType = <TElement extends React.ElementType = typeof __ELEMENT_TYPE__>(
props: ComponentProps<TElement>
) => React.ReactNode

const Component: ComponentType = React.forwardRef(
<TElement extends React.ElementType = typeof __ELEMENT_TYPE__>(
props: ComponentProps<TElement>,
ref: Polymophic.Ref<TElement>
) => {
const { as, children, className, ...rest } = props

const Element = as || Popover

const { slots } = useComboBoxContext()

const component = React.useMemo<PopoverProps>(
() => ({
className: slots.popover({ className: className?.toString() }),
...rest,
}),
[className, rest, slots]
)

return (
<Element {...component} ref={ref}>
{children}
</Element>
)
}
)

export type { ComponentOwnProps as ComboBoxPopoverOwnProps, ComponentProps as ComboBoxPopoverProps }
export default Component
6 changes: 6 additions & 0 deletions packages/react/src/components/combo-box/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type * from '@/components/combo-box/ComboBox'
export type * from '@/components/combo-box/ComboBoxItem'
export type * from '@/components/combo-box/ComboBoxList'
export type * from '@/components/combo-box/ComboBoxPopover'

export { default as ComboBox } from '@/components/combo-box/ComboBox'
26 changes: 26 additions & 0 deletions packages/react/src/components/combo-box/use-combo-box.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ComboBoxVariantProps } from '@giantnodes/theme'

import { combobox } from '@giantnodes/theme'
import React from 'react'

import { createContext } from '@/utilities/context'

type UseComboBoxProps = ComboBoxVariantProps

type UseComboBoxReturn = ReturnType<typeof useComboBox>

export const useComboBox = (props: UseComboBoxProps) => {
const { size, status } = props

const slots = React.useMemo(() => combobox({ size, status }), [size, status])

return {
slots,
}
}

export const [ComboBoxContext, useComboBoxContext] = createContext<UseComboBoxReturn>({
name: 'ComboBoxContext',
strict: true,
errorMessage: 'useComboBox: `context` is undefined. Seems you forgot to wrap component within <ComboBox />',
})
63 changes: 63 additions & 0 deletions packages/theme/src/components/combo-box.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { VariantProps } from 'tailwind-variants'

import { tv } from 'tailwind-variants'

export const combobox = tv({
slots: {
combobox: ['group flex flex-col gap-1'],
popover: ['bg-foreground', 'border border-solid border-partition', 'rounded-md', 'w-[--trigger-width]'],
list: ['flex flex-col gap-1', 'p-1', 'outline-none'],
item: [
'flex tems-center',
'px-2 py-1',
'rounded-md',
'cursor-pointer',
'outline-none',
'text-content',
'overflow-hidden',
'disabled:opacity-50 disabled:cursor-default',
],
},
variants: {
size: {
xs: {
item: ['text-xs'],
},
sm: {
item: ['text-sm'],
},
md: {
item: ['text-base'],
},
lg: {
item: ['text-lg'],
},
},
status: {
neutral: {
item: ['hover:bg-middleground'],
},
brand: {
item: ['hover:bg-brand/20'],
},
success: {
item: ['hover:bg-success/20'],
},
info: {
item: ['hover:bg-info/20'],
},
warning: {
item: ['hover:bg-warning/20'],
},
danger: {
item: ['hover:bg-danger/20'],
},
},
},
defaultVariants: {
size: 'md',
status: 'neutral',
},
})

export type ComboBoxVariantProps = VariantProps<typeof combobox>
Loading

0 comments on commit e5b3366

Please sign in to comment.