From b692dcc5446352825766963af16035f6553b2f7c Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 15 May 2026 14:21:45 +0300 Subject: [PATCH] Refactor Combobox stories and components to enhance usability and add create item functionality - Updated Combobox stories to use ComboboxTrigger and ComboboxValue for better structure. - Introduced ComboboxCreateItem for adding new items directly from the input. - Simplified ComboboxInput and removed unnecessary props. - Improved styling and accessibility features across the component. --- src/components/Combobox/Combobox.stories.tsx | 260 +++++++++++++------ src/components/Combobox/Combobox.tsx | 102 ++++---- 2 files changed, 235 insertions(+), 127 deletions(-) diff --git a/src/components/Combobox/Combobox.stories.tsx b/src/components/Combobox/Combobox.stories.tsx index a8bda3683..5cca0ad43 100644 --- a/src/components/Combobox/Combobox.stories.tsx +++ b/src/components/Combobox/Combobox.stories.tsx @@ -1,5 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite' -import { Fragment } from 'react' +import { Fragment, useState } from 'react' + +import { Spinner } from '../Spinner/Spinner.js' import { Combobox, @@ -8,13 +10,14 @@ import { ComboboxChipsInput, ComboboxCollection, ComboboxContent, + ComboboxCreateItem, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxLabel, ComboboxList, - ComboboxSeparator, + ComboboxTrigger, ComboboxValue, useComboboxAnchor, } from './Combobox.js' @@ -36,31 +39,11 @@ type Story = StoryObj export const Basic: Story = { render: () => ( - - - No items found. - - {item => ( - - {item} - - )} - - - - ), -} - -export const WithClear: Story = { - name: 'With clear', - render: () => ( - - + + + + No items found. {item => ( @@ -74,47 +57,6 @@ export const WithClear: Story = { ), } -function ComboboxMultipleStory() { - const anchor = useComboboxAnchor() - - return ( - - - - {values => ( - - {(values as string[]).map(value => ( - {value} - ))} - - - )} - - - - No items found. - - {item => ( - - {item} - - )} - - - - ) -} - -export const Multiple: Story = { - name: 'Multiple (chips)', - render: () => , -} - const timezones = [ { value: 'Americas', @@ -154,11 +96,14 @@ const timezones = [ export const Groups: Story = { render: () => ( - + + + + No timezones found. - {(group, index) => ( + {group => ( {group.value} @@ -168,7 +113,6 @@ export const Groups: Story = { )} - {index < timezones.length - 1 ? : null} )} @@ -197,8 +141,11 @@ export const CustomItems: Story = { items={frameworkOptions} itemToStringValue={(framework: FrameworkOption) => framework.label} > - + + + + No items found. {(framework: FrameworkOption) => ( @@ -218,12 +165,11 @@ export const CustomItems: Story = { export const Invalid: Story = { render: () => ( - + + + + No items found. {item => ( @@ -239,13 +185,12 @@ export const Invalid: Story = { export const Disabled: Story = { render: () => ( - - + + + + + No items found. {item => ( @@ -263,8 +208,11 @@ export const AutoHighlight: Story = { name: 'Auto highlight', render: () => ( - + + + + No items found. {item => ( @@ -277,3 +225,149 @@ export const AutoHighlight: Story = { ), } + +function WithCreateStory() { + const [items, setItems] = useState([...frameworks]) + const [inputValue, setInputValue] = useState('') + + return ( + + + + + + + + {item => ( + + {item} + + )} + + { + setItems(prev => [...prev, value]) + setInputValue('') + }} + /> + + + ) +} + +export const WithCreate: Story = { + name: 'With create', + render: () => , +} + +function WithAsyncCreateStory() { + const [items, setItems] = useState([...frameworks]) + const [inputValue, setInputValue] = useState('') + const [isCreating, setIsCreating] = useState(false) + + const handleCreate = async (value: string) => { + if (!value || isCreating) { + return + } + setIsCreating(true) + await new Promise(resolve => setTimeout(resolve, 1200)) + setItems(prev => [...prev, value]) + setInputValue('') + setIsCreating(false) + } + + return ( + { + if (!isCreating) { + setInputValue(value) + } + }} + > + + + + + + + {item => ( + + {item} + + )} + + svg:first-child]:hidden' + : undefined + } + > + {isCreating ? ( + <> + + Adding {inputValue}... + + ) : ( + `Add ${inputValue}` + )} + + + + ) +} + +export const WithAsyncCreate: Story = { + name: 'With async create', + render: () => , +} + +function ComboboxMultipleStory() { + const anchor = useComboboxAnchor() + + return ( + + + + {values => ( + + {(values as string[]).map(value => ( + {value} + ))} + + + )} + + + + No items found. + + {item => ( + + {item} + + )} + + + + ) +} + +export const Multiple: Story = { + name: 'Multiple (chips)', + render: () => , +} diff --git a/src/components/Combobox/Combobox.tsx b/src/components/Combobox/Combobox.tsx index f07749abb..91214c29a 100644 --- a/src/components/Combobox/Combobox.tsx +++ b/src/components/Combobox/Combobox.tsx @@ -1,9 +1,16 @@ import { Combobox as ComboboxPrimitive } from '@base-ui/react' -import { CheckIcon, ChevronDownIcon, XIcon } from 'lucide-react' +import { + CheckIcon, + ChevronDownIcon, + PlusIcon, + SearchIcon, + XIcon, +} from 'lucide-react' import { useRef, type ComponentPropsWithRef } from 'react' import { useIsKeyboardFocused } from '../../hooks/useIsKeyboardFocused.js' import { + disabledStyle, focusRingVariants, inputBorderStyle, popupContentStyle, @@ -26,67 +33,44 @@ function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { function ComboboxTrigger({ className, children, + disabled, ...props }: ComboboxPrimitive.Trigger.Props) { return ( {children} - + ) } -function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { - return ( - - - - } - /> - ) -} - function ComboboxInput({ className, - children, disabled = false, - showTrigger = true, - showClear = false, ...props -}: ComboboxPrimitive.Input.Props & { - showTrigger?: boolean - showClear?: boolean -}) { +}: ComboboxPrimitive.Input.Props) { return ( + + + } {...props} /> - - {showTrigger && ( - + + ) +} + function ComboboxChips({ className, ...props @@ -298,8 +311,8 @@ function ComboboxChipsInput({ ) } -function useComboboxAnchor() { - return useRef(null) +function useComboboxAnchor() { + return useRef(null) } export { @@ -309,6 +322,7 @@ export { ComboboxChipsInput, ComboboxCollection, ComboboxContent, + ComboboxCreateItem, ComboboxEmpty, ComboboxGroup, ComboboxInput,