import React, { useState } from 'react'; import { storybookArgTypes, storybookExcludedControlParams, type StoryMetaType, StoryType, } from '@lg-tools/storybook-utils'; import { StoryContext, StoryFn } from '@storybook/react'; import { userEvent, within } from '@storybook/test'; import { Badge } from '@leafygreen-ui/badge'; import { Button } from '@leafygreen-ui/button'; import { css } from '@leafygreen-ui/emotion'; import { getComboboxOptions } from './test-utils/getTestOptions.testutils'; import { ComboboxSize, DropdownWidthBasis, Overflow, SearchState, State, TruncationLocation, } from './types'; import { Combobox, ComboboxOption, ComboboxProps } from '.'; const wrapperStyle = css` width: 256px; padding-block: 64px; display: flex; `; const multiValue = ['apple', 'banana']; const meta: StoryMetaType = { title: 'Components/Inputs/Combobox', component: Combobox, decorators: [ StoryFn => (
), ], parameters: { default: 'LiveExample', controls: { exclude: [ ...storybookExcludedControlParams, 'as', 'filteredOptions', 'initialValue', 'setError', 'value', 'children', ], }, generate: { storyNames: ['SingleSelect', 'MultiSelect', 'DisabledInput'], combineArgs: { darkMode: [false, true], clearable: [true, false], description: [undefined, 'Please pick fruit(s)'], label: [undefined, 'Choose a fruit'], state: Object.values(State), size: Object.values(ComboboxSize), }, excludeCombinations: [ ['description', { label: undefined }], { clearable: false, value: undefined, }, { multiselect: true, value: 'apple', }, { multiselect: false, value: multiValue, }, ], }, }, args: { label: 'Choose a fruit', description: 'Please pick fruit(s)', placeholder: 'Select fruit', multiselect: false, darkMode: false, disabled: false, clearable: true, dropdownWidthBasis: DropdownWidthBasis.Trigger, errorMessage: 'No Pomegranates!', children: getComboboxOptions(), }, argTypes: { darkMode: storybookArgTypes.darkMode, multiselect: { control: 'boolean' }, disabled: { control: 'boolean' }, clearable: { control: 'boolean' }, label: { control: 'text' }, description: { control: 'text' }, placeholder: { control: 'text' }, inputValue: { control: 'text' }, size: { options: Object.values(ComboboxSize), control: 'select', }, state: { options: Object.values(State), control: 'select', }, errorMessage: { control: 'text', }, searchEmptyMessage: { control: 'text' }, searchState: { options: Object.values(SearchState), control: 'select', }, searchErrorMessage: { control: 'text', if: { arg: 'searchState', eq: SearchState.Error }, }, searchLoadingMessage: { control: 'text', if: { arg: 'searchState', eq: SearchState.Loading }, }, chipTruncationLocation: { options: Object.values(TruncationLocation), control: 'select', if: { arg: 'multiselect' }, }, chipCharacterLimit: { control: 'number', if: { arg: 'multiselect' }, }, overflow: { options: Object.values(Overflow), control: 'select', if: { arg: 'multiselect' }, }, dropdownWidthBasis: { options: Object.values(DropdownWidthBasis), control: 'select', }, }, }; export default meta; export const LiveExample: StoryFn> = args => { return ( <> {/* Since Combobox doesn't fully refresh when `multiselect` changes, we need to explicitly render a different instance */} {args.multiselect ? ( // @ts-ignore - multiselect check ensures props match ComboboxProps ) : ( // @ts-ignore - multiselect check ensures props match ComboboxProps )} ); }; LiveExample.parameters = { chromatic: { disableSnapshot: true }, }; export const ControlledSingleSelect = () => { const [selection, setSelection] = useState(null); const handleChange = (value: string | null) => { setSelection(value); }; return (
); }; ControlledSingleSelect.parameters = { chromatic: { disableSnapshot: true }, }; export const ExternalFilter = () => { const allOptions = [ 'apple', 'banana', 'carrot', 'dragonfruit', 'eggplant', 'fig', 'grape', 'honeydew', 'iceberg-lettuce', 'jalapeƱo', ]; const [filteredOptions, setOptions] = useState(['carrot', 'grape']); const handleFilter = (input: string) => { setOptions(allOptions.filter(option => option.includes(input))); }; return ( {allOptions.map(option => ( ))} ); }; ExternalFilter.parameters = { chromatic: { disableSnapshot: true }, }; /** * Example showing the `customContent` prop for rendering custom components in dropdown options. * The `customContent` prop accepts any ReactNode, allowing you to add badges, icons, or other * custom components to your options. The `displayName` is still used for filtering and chips. */ export const WithCustomContent = () => { return ( Feature A New } /> Feature B Beta } /> Feature D Deprecated } /> ); }; WithCustomContent.parameters = { chromatic: { disableSnapshot: true }, }; export const SingleSelect: StoryType = () => <>; SingleSelect.args = { multiselect: false, }; SingleSelect.parameters = { generate: { combineArgs: { value: [undefined, 'apple'], }, }, }; export const MultiSelect = () => <>; MultiSelect.args = { multiselect: true, }; MultiSelect.parameters = { generate: { combineArgs: { value: [undefined, multiValue], }, }, }; export const MultiSelectNoIcons: StoryFn> = ( args: ComboboxProps, ) => { return ( // @ts-expect-error - args will have multiselect=true from storybook controls {getComboboxOptions(false)} ); }; MultiSelectNoIcons.parameters = { chromatic: { disableSnapshot: true }, }; export const DisabledInput = () => <>; DisabledInput.args = { disabled: true, }; DisabledInput.parameters = { generate: { combineArgs: { darkMode: [true, false], }, }, }; export const InitialLongComboboxOpen = { render: () => { return ( {getComboboxOptions()} ); }, play: async (ctx: StoryContext) => { const { findByRole } = within(ctx.canvasElement.parentElement!); const trigger = await findByRole('combobox'); userEvent.click(trigger); }, decorators: [ (Story: StoryFn, _ctx: StoryContext) => (
), ], };