From 4d5dc684258f3f5766b402f988191572aaf42848 Mon Sep 17 00:00:00 2001 From: Shadi Date: Wed, 6 Sep 2023 11:45:33 -0500 Subject: [PATCH] feat(multiselect combobox): expose state hook / inversion of control (#3470) * feat(multiselect-combobox): add state hook functionality * chore: minor improvement to the dropdown open orientation * docs(multiselect-combobox): fix link to hook * fix: changesets message --- .changeset/late-forks-vanish.md | 6 +- .changeset/odd-weeks-pull.md | 6 ++ .../paste-codemods/tools/.cache/mappings.json | 1 + .../__tests__/MultiselectCombobox.spec.tsx | 62 ++++++++++++- .../combobox/src/ListboxPositioner.tsx | 11 ++- .../components/combobox/src/index.tsx | 5 +- .../src/multiselect/MultiselectCombobox.tsx | 16 ++-- .../src/multiselect/extractPropsFromState.tsx | 28 ++++++ .../components/combobox/src/types.ts | 5 +- .../stories/MultiselectCombobox.stories.tsx | 89 ++++++++++++++---- .../MultiselectComboboxExamples.ts | 91 +++++++++++++++++++ .../components/multiselect-combobox/index.mdx | 54 ++++++++++- 12 files changed, 342 insertions(+), 32 deletions(-) create mode 100644 .changeset/odd-weeks-pull.md create mode 100644 packages/paste-core/components/combobox/src/multiselect/extractPropsFromState.tsx diff --git a/.changeset/late-forks-vanish.md b/.changeset/late-forks-vanish.md index a99e79680c..27206bafba 100644 --- a/.changeset/late-forks-vanish.md +++ b/.changeset/late-forks-vanish.md @@ -3,4 +3,8 @@ '@twilio-paste/core': patch --- -[Codemods] Include new ProgressBar Exports +[Codemods] Include new mappings: + +- `ProgressBar` +- `useMultiselectCombobox` +- `useMultiSelectPrimitive` diff --git a/.changeset/odd-weeks-pull.md b/.changeset/odd-weeks-pull.md new file mode 100644 index 0000000000..5036efb3b1 --- /dev/null +++ b/.changeset/odd-weeks-pull.md @@ -0,0 +1,6 @@ +--- +'@twilio-paste/combobox': minor +'@twilio-paste/core': minor +--- + +[MultiSelect Combobox] allow inversion of control with the internal state so that consumers can manage the selectedItems array themselves. This enables functionality like clearing the selectedItems through an external Button. diff --git a/packages/paste-codemods/tools/.cache/mappings.json b/packages/paste-codemods/tools/.cache/mappings.json index 4c041c5846..9bd7d6fcca 100644 --- a/packages/paste-codemods/tools/.cache/mappings.json +++ b/packages/paste-codemods/tools/.cache/mappings.json @@ -77,6 +77,7 @@ "ComboboxListboxOption": "@twilio-paste/core/combobox", "MultiselectCombobox": "@twilio-paste/core/combobox", "useCombobox": "@twilio-paste/core/combobox", + "useMultiselectCombobox": "@twilio-paste/core/combobox", "DataGrid": "@twilio-paste/core/data-grid", "DataGridBody": "@twilio-paste/core/data-grid", "DataGridCell": "@twilio-paste/core/data-grid", diff --git a/packages/paste-core/components/combobox/__tests__/MultiselectCombobox.spec.tsx b/packages/paste-core/components/combobox/__tests__/MultiselectCombobox.spec.tsx index 11942b643e..1311a97bc5 100644 --- a/packages/paste-core/components/combobox/__tests__/MultiselectCombobox.spec.tsx +++ b/packages/paste-core/components/combobox/__tests__/MultiselectCombobox.spec.tsx @@ -1,12 +1,13 @@ import * as React from 'react'; -import {render, screen, fireEvent} from '@testing-library/react'; +import {render, act, screen, fireEvent} from '@testing-library/react'; import type {RenderOptions} from '@testing-library/react'; import {Theme} from '@twilio-paste/theme'; import {Form} from '@twilio-paste/form'; +import {Button} from '@twilio-paste/button'; import filter from 'lodash/filter'; import uniq from 'lodash/uniq'; -import {MultiselectCombobox} from '../src'; +import {MultiselectCombobox, useMultiselectCombobox} from '../src'; import type {MultiselectComboboxProps} from '../src'; const items = [ @@ -38,6 +39,7 @@ const MultiselectComboboxMock: React.FC> = (pr const filteredItems = React.useMemo(() => getFilteredItems(inputValue), [inputValue]); return ( > = (pr onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: string[]) => { + onSelectedItemsChange={() => { // eslint-disable-next-line no-console // console.log(selectedItems); }} @@ -108,6 +110,40 @@ const GroupedMultiselectComboboxMock: React.FC ); }; +const StateHookMock: React.FC> = (props) => { + const [inputValue, setInputValue] = React.useState(''); + const filteredItems = React.useMemo(() => getFilteredGroupedItems(inputValue), [inputValue]); + + const state = useMultiselectCombobox({ + initialSelectedItems: filteredItems.slice(0, 2), + onSelectedItemsChange: props.onSelectedItemsChange, + }); + + return ( + <> + + (item ? item.label : '')} + onInputValueChange={({inputValue: newInputValue = ''}) => { + setInputValue(newInputValue); + }} + onSelectedItemsChange={props.onSelectedItemsChange} + labelText="Choose a Paste Component" + selectedItemsLabelText="Selected Paste components" + helpText="Paste components are the building blocks of your product UI." + initialIsOpen + optionTemplate={(item: GroupedItem) =>
{item.label}
} + /> + + ); +}; + describe('MultiselectCombobox', () => { beforeEach(() => { jest.clearAllMocks(); @@ -280,4 +316,24 @@ describe('MultiselectCombobox', () => { expect(mockSelectedItemsChangeFn.mock.results[2].value).toEqual([{group: 'Components', label: 'Button'}]); }); }); + + describe('Inversion of control', () => { + it('allows clearing selected items from an external button click', () => { + const mockSelectedItemsChangeFn = jest.fn((selectedItems) => selectedItems); + render(, { + wrapper: ThemeWrapper, + }); + + const pillGroup = screen.getAllByRole('listbox')[0]; + expect(pillGroup?.childNodes.length).toBe(2); + + act(() => { + screen.getByRole('button', {name: 'Clear'}).click(); + }); + + expect(pillGroup?.childNodes.length).toBe(0); + expect(mockSelectedItemsChangeFn).toHaveBeenCalledTimes(1); + expect(mockSelectedItemsChangeFn.mock.results[0].value).toEqual({activeIndex: -1, selectedItems: [], type: 10}); + }); + }); }); diff --git a/packages/paste-core/components/combobox/src/ListboxPositioner.tsx b/packages/paste-core/components/combobox/src/ListboxPositioner.tsx index 84ede0eee1..dc1ac5055d 100644 --- a/packages/paste-core/components/combobox/src/ListboxPositioner.tsx +++ b/packages/paste-core/components/combobox/src/ListboxPositioner.tsx @@ -16,6 +16,7 @@ export const ListBoxPositioner: React.FC = ({inputBoxRef const dropdownBoxHeight = dropdownBoxDimensions?.height; const styles = React.useMemo((): BoxStyleProps => { + // If it's closed, return an empty object if (dropdownBoxHeight == null || inputBoxDimensions == null || dropdownBoxHeight === 0) { return {}; } @@ -25,7 +26,10 @@ export const ListBoxPositioner: React.FC = ({inputBoxRef * 1- Dropdown height is bigger than window height * - Then show at the top of the viewport * 2- Dropdown height + inputbox bottom is bigger than viewport height - * - Show upwards + * 2.1- inputbox top - Dropdown height is < 0 (offscreen topwise) + * - Show downwards + * 2.2- else + * - Show upwards * 3- Dropdown height + inputbox bottom is smaller than viewport height * - Show downwards */ @@ -39,7 +43,10 @@ export const ListBoxPositioner: React.FC = ({inputBoxRef width: inputBoxDimensions?.width, }; } - if (dropdownBoxHeight + inputBoxDimensions?.bottom >= windowHeight) { + if ( + dropdownBoxHeight + inputBoxDimensions?.bottom >= windowHeight && + inputBoxDimensions?.top - dropdownBoxHeight > 0 + ) { return { position: 'fixed', // 6px to account for border things, should be fine on all themes diff --git a/packages/paste-core/components/combobox/src/index.tsx b/packages/paste-core/components/combobox/src/index.tsx index 4db639c052..56a01ec7c1 100644 --- a/packages/paste-core/components/combobox/src/index.tsx +++ b/packages/paste-core/components/combobox/src/index.tsx @@ -1,4 +1,7 @@ -export {useComboboxPrimitive as useCombobox} from '@twilio-paste/combobox-primitive'; +export { + useComboboxPrimitive as useCombobox, + useMultiSelectPrimitive as useMultiselectCombobox, +} from '@twilio-paste/combobox-primitive'; export type { UseComboboxPrimitiveState as UseComboboxState, UseComboboxPrimitiveStateChange as UseComboboxStateChange, diff --git a/packages/paste-core/components/combobox/src/multiselect/MultiselectCombobox.tsx b/packages/paste-core/components/combobox/src/multiselect/MultiselectCombobox.tsx index 7591eec510..1e5c1d1b18 100644 --- a/packages/paste-core/components/combobox/src/multiselect/MultiselectCombobox.tsx +++ b/packages/paste-core/components/combobox/src/multiselect/MultiselectCombobox.tsx @@ -9,15 +9,16 @@ import {Label} from '@twilio-paste/label'; import {HelpText} from '@twilio-paste/help-text'; import {ScreenReaderOnly} from '@twilio-paste/screen-reader-only'; import {FormPillGroup, FormPill, useFormPillState} from '@twilio-paste/form-pill-group'; -import {useComboboxPrimitive, useMultiSelectPrimitive} from '@twilio-paste/combobox-primitive'; +import {useComboboxPrimitive} from '@twilio-paste/combobox-primitive'; import {InputBox, InputChevronWrapper, getInputChevronIconColor} from '@twilio-paste/input-box'; import {Portal} from '@twilio-paste/reakit-library'; -import {ListBoxPositioner} from '../ListboxPositioner'; import {GrowingInput} from './GrowingInput'; -import {ComboboxListbox} from '../styles/ComboboxListbox'; +import {extractPropsFromState} from './extractPropsFromState'; +import {ListBoxPositioner} from '../ListboxPositioner'; import {ComboboxItems} from '../ComboboxItems'; -import type {Item, MultiselectComboboxProps} from '../types'; +import {ComboboxListbox} from '../styles/ComboboxListbox'; +import type {MultiselectComboboxProps} from '../types'; import {getHelpTextVariant} from '../helpers'; export const MultiselectCombobox = React.forwardRef( @@ -27,6 +28,7 @@ export const MultiselectCombobox = React.forwardRef({ + } = extractPropsFromState({ + state, initialSelectedItems, onSelectedItemsChange, }); @@ -252,7 +255,6 @@ export const MultiselectCombobox = React.forwardRef) => { @@ -265,7 +267,7 @@ export const MultiselectCombobox = React.forwardRef => { + // If they're providing their own state management, we don't need to do anything + if (state != null && !isEmpty(state)) { + return state; + } + + // Otherwise, we'll use our own state management + return useMultiSelectPrimitive({ + initialSelectedItems, + onSelectedItemsChange, + }); +}; diff --git a/packages/paste-core/components/combobox/src/types.ts b/packages/paste-core/components/combobox/src/types.ts index bb21530c3f..f8786d96eb 100644 --- a/packages/paste-core/components/combobox/src/types.ts +++ b/packages/paste-core/components/combobox/src/types.ts @@ -4,6 +4,8 @@ import type { UseComboboxPrimitiveProps, UseComboboxPrimitiveState, UseComboboxPrimitiveReturnValue, + UseMultiSelectPrimitiveReturnValue, + UseMultiSelectPrimitiveStateChange, } from '@twilio-paste/combobox-primitive'; import type {InputVariants, InputProps} from '@twilio-paste/input'; import type {VirtualItem} from 'react-virtual'; @@ -97,10 +99,11 @@ export interface MultiselectComboboxProps | 'hideVisibleLabel' > { initialSelectedItems?: any[]; - onSelectedItemsChange?: (newSelectedItems: any[]) => void; + onSelectedItemsChange?: (newSelectedItems: UseMultiSelectPrimitiveStateChange) => void; selectedItemsLabelText: string; i18nKeyboardControls?: string; maxHeight?: BoxStyleProps['maxHeight']; + state?: UseMultiSelectPrimitiveReturnValue; } export interface ComboboxItemsProps diff --git a/packages/paste-core/components/combobox/stories/MultiselectCombobox.stories.tsx b/packages/paste-core/components/combobox/stories/MultiselectCombobox.stories.tsx index 5644dea1d2..d3a791c20d 100644 --- a/packages/paste-core/components/combobox/stories/MultiselectCombobox.stories.tsx +++ b/packages/paste-core/components/combobox/stories/MultiselectCombobox.stories.tsx @@ -12,7 +12,7 @@ import {Modal, ModalBody, ModalHeader, ModalHeading} from '@twilio-paste/modal'; import {Button} from '@twilio-paste/button'; import {useUID} from '@twilio-paste/uid-library'; -import {MultiselectCombobox} from '../src'; +import {MultiselectCombobox, useMultiselectCombobox} from '../src'; function createLargeArray>( template: (index?: number | undefined) => TemplateResult @@ -57,7 +57,7 @@ export const MultiselectComboboxBasic = (): React.ReactNode => { onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: string[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} @@ -81,7 +81,7 @@ export const BottomOfScreen = (): React.ReactNode => { onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: string[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} @@ -112,7 +112,7 @@ export const MultiselectComboboxInverse = (): React.ReactNode => { onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: string[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} @@ -140,7 +140,7 @@ export const MultiselectComboboxDisabled = (): React.ReactNode => { onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: string[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} @@ -169,7 +169,7 @@ export const MultiselectComboboxDisabledInverseRequired = (): React.ReactNode => onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: string[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} @@ -198,7 +198,7 @@ export const MultiselectComboboxError = (): React.ReactNode => { onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: string[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} @@ -232,7 +232,7 @@ export const MultiselectComboboxRequired = (): React.ReactNode => { onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: string[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} @@ -259,7 +259,7 @@ export const MultiselectComboboxInitialSelectedItems = (): React.ReactNode => { onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: string[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} @@ -325,7 +325,7 @@ export const MultiselectComboboxBeforeAfter = (): React.ReactNode => { onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: Book[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} @@ -371,7 +371,7 @@ export const MultiselectComboboxMaxHeight = (): React.ReactNode => { onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: Book[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} @@ -405,7 +405,7 @@ export const MultiselectComboboxOptionTemplate = (): React.ReactNode => { onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: Book[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} @@ -441,7 +441,7 @@ export const MultiselectComboboxOptionTemplatedisabled = (): React.ReactNode => onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: Book[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} @@ -490,7 +490,7 @@ export const MultiselectComboboxOptionGroups = (): React.ReactNode => { onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: GroupedItem[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} @@ -541,7 +541,7 @@ export const MultiselectComboboxEmptyState = (): React.ReactNode => { onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: GroupedItem[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} @@ -571,6 +571,63 @@ export const MultiselectComboboxEmptyState = (): React.ReactNode => { MultiselectComboboxEmptyState.storyName = 'with empty state'; +export const MultiselectComboboxStateHook = (): React.ReactNode => { + const [inputValue, setInputValue] = React.useState(''); + const filteredItems = React.useMemo(() => getFilteredGroupedItems(inputValue), [inputValue]); + + const onSelectedItemsChange = React.useCallback((selectedItems) => { + // eslint-disable-next-line no-console + console.log(selectedItems); + }, []); + + const state = useMultiselectCombobox({ + initialSelectedItems: filteredItems.slice(0, 2), + onSelectedItemsChange, + }); + + return ( + <> + + (item ? item.label : '')} + onInputValueChange={({inputValue: newInputValue = ''}) => { + setInputValue(newInputValue); + }} + onSelectedItemsChange={onSelectedItemsChange} + labelText="Choose a Paste Component" + selectedItemsLabelText="Selected Paste components" + helpText="Paste components are the building blocks of your product UI." + initialIsOpen + optionTemplate={(item: GroupedItem) => { + return
{item.label}
; + }} + groupLabelTemplate={(groupName: string) => { + if (groupName === 'Components') { + return ( + + + + + {groupName} + + ); + } + return groupName; + }} + /> + + ); +}; + +MultiselectComboboxStateHook.storyName = 'with state hook'; + export const MultiselectComboboxInModal: StoryFn = () => { const [modalIsOpen, setModalIsOpen] = React.useState(true); const handleOpen = (): void => setModalIsOpen(true); @@ -599,7 +656,7 @@ export const MultiselectComboboxInModal: StoryFn = () => { onInputValueChange={({inputValue: newInputValue = ''}) => { setInputValue(newInputValue); }} - onSelectedItemsChange={(selectedItems: string[]) => { + onSelectedItemsChange={(selectedItems) => { // eslint-disable-next-line no-console console.log(selectedItems); }} diff --git a/packages/paste-website/src/component-examples/MultiselectComboboxExamples.ts b/packages/paste-website/src/component-examples/MultiselectComboboxExamples.ts index abbad54ffe..d651d66b3f 100644 --- a/packages/paste-website/src/component-examples/MultiselectComboboxExamples.ts +++ b/packages/paste-website/src/component-examples/MultiselectComboboxExamples.ts @@ -534,3 +534,94 @@ render( ) `.trim(); + +export const stateHookExample = ` +const groupedItems = [ + {group: 'Components', label: 'Alert'}, + {group: 'Components', label: 'Anchor'}, + {group: 'Components', label: 'Button'}, + {group: 'Components', label: 'Card'}, + {group: 'Components', label: 'Heading'}, + {group: 'Components', label: 'List'}, + {group: 'Components', label: 'Modal'}, + {group: 'Components', label: 'Paragraph'}, + {group: 'Primitives', label: 'Box'}, + {group: 'Primitives', label: 'Text'}, + {group: 'Primitives', label: 'Non-modal dialog'}, + {group: 'Layout', label: 'Grid'}, + {label: 'Design Tokens'}, +]; + +function getFilteredGroupedItems(inputValue) { + const lowerCasedInputValue = inputValue.toLowerCase(); + return filter(groupedItems, (item) => item.label.toLowerCase().includes(lowerCasedInputValue)); +} + +const SampleEmptyState = () => ( + + + No results found + + +); + +const MultiselectComboboxExample = () => { + const [inputValue, setInputValue] = React.useState(''); + const filteredItems = React.useMemo(() => getFilteredGroupedItems(inputValue), [inputValue]); + + const onSelectedItemsChange = React.useCallback((selectedItems) => { + console.log(selectedItems); + }, []); + + const state = useMultiselectCombobox({ + initialSelectedItems: filteredItems.slice(0, 2), + onSelectedItemsChange, + }); + + return ( + <> + + + + (item ? item.label : '')} + onInputValueChange={({inputValue = ''}) => { + setInputValue(newInputValue); + }} + onSelectedItemsChange={onSelectedItemsChange} + labelText="Choose a Paste Component" + selectedItemsLabelText="Selected Paste components" + helpText="Paste components are the building blocks of your product UI." + initialIsOpen + optionTemplate={(item) => { + return
{item.label}
; + }} + groupLabelTemplate={(groupName) => { + if (groupName === 'Components') { + return ( + + + + + {groupName} + + ); + } + return groupName; + }} + /> + + ); +} + +render( + +) +`.trim(); diff --git a/packages/paste-website/src/pages/components/multiselect-combobox/index.mdx b/packages/paste-website/src/pages/components/multiselect-combobox/index.mdx index 41fc08098c..2bb7647fd9 100644 --- a/packages/paste-website/src/pages/components/multiselect-combobox/index.mdx +++ b/packages/paste-website/src/pages/components/multiselect-combobox/index.mdx @@ -9,7 +9,7 @@ export const meta = { import {Anchor} from '@twilio-paste/anchor'; import {Box} from '@twilio-paste/box'; import {Text} from '@twilio-paste/text'; -import {MultiselectCombobox} from '@twilio-paste/combobox'; +import {MultiselectCombobox, useMultiselectCombobox} from '@twilio-paste/combobox'; import {Button} from '@twilio-paste/button'; import {Table, THead, TBody, Td, Th, Tr} from '@twilio-paste/table'; import {MediaObject, MediaFigure, MediaBody} from '@twilio-paste/media-object'; @@ -37,6 +37,7 @@ import { errorExample, emptyStateExample, maxHeightExample, + stateHookExample, } from '../../../component-examples/MultiselectComboboxExamples'; import packageJson from '@twilio-paste/combobox/package.json'; import DefaultLayout from '../../../layouts/DefaultLayout'; @@ -206,6 +207,57 @@ it resizes vertically, you can provide a `maxHeight` prop. {maxHeightExample} +### useMultiselectCombobox state hook + + + Power user move! + + Only use this property if you are a power user. It's very easy to break your implementation and unfortunately the + Paste team will not be able to debug this for you. Proceed with extreme caution. + + + +In addition to being a controlled component, the Multiselect Combobox comes with the option +of "hooking" into the internal state by using the state hook originally provided by +[Downshift](https://github.com/downshift-js/downshift/tree/master/src/hooks/useMultipleSelection). + +Rather than the state be internal to the component, you can use the `useMultiselectCombobox` +hook and pass the returned state to `MultiselectCombobox` as the `state` prop. + +This allows you to destructure certain returned props from the state hook, +including action methods like `reset`. + +An example use case of this might be programmatically providing the user a way to +clear or reset the Multiselect Combobox of its previous selections. + +It should be noted that when doing so, the `state` prop takes precident over the +[other properties](#state-props) that affect the state or initial state of the +`MultiselectCombobox`. They will be ignored in favour of them being provided as +arguments to the `useMultiselectCombobox` hook. + +For full details on how to use the state hook, and what props to provide it, +follow the [Combobox Primitive documentation](/primitives/combobox-primitive#usemultiselectprimitive-control-props). +It's the same hook, just renamed. + + + {stateHookExample} + + ## States ### Disabled Multiselect Combobox