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

feat: add combo box component #63

Merged
merged 2 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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