From ccf42800900f0bb9b9eb62554fcfa17392aac0ec Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Tue, 16 Jul 2024 12:02:52 -0400 Subject: [PATCH 1/2] feat(MultiTypeaheadSelect): Add MultiTypeaheadSelect to react-templates --- .../Select/MultiTypeaheadSelect.tsx | 348 +++++++++++++++ .../__tests__/MultiTypeaheadSelect.test.tsx | 414 ++++++++++++++++++ .../MultiTypeaheadSelect.test.tsx.snap | 180 ++++++++ .../examples/MultiTypeaheadSelectDemo.tsx | 35 ++ .../Select/examples/SelectTemplates.md | 8 +- .../src/components/Select/index.ts | 1 + 6 files changed, 985 insertions(+), 1 deletion(-) create mode 100644 packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx create mode 100644 packages/react-templates/src/components/Select/__tests__/MultiTypeaheadSelect.test.tsx create mode 100644 packages/react-templates/src/components/Select/__tests__/__snapshots__/MultiTypeaheadSelect.test.tsx.snap create mode 100644 packages/react-templates/src/components/Select/examples/MultiTypeaheadSelectDemo.tsx diff --git a/packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx b/packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx new file mode 100644 index 00000000000..1a408bb6209 --- /dev/null +++ b/packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx @@ -0,0 +1,348 @@ +import React from 'react'; +import { + Select, + SelectOption, + SelectList, + SelectOptionProps, + MenuToggle, + MenuToggleElement, + TextInputGroup, + TextInputGroupMain, + TextInputGroupUtilities, + Button, + MenuToggleProps, + SelectProps, + ChipGroup, + Chip +} from '@patternfly/react-core'; +import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; + +export interface MultiTypeaheadSelectOption extends Omit { + /** Content of the select option. */ + content: string | number; + /** Value of the select option. */ + value: string | number; +} + +export interface MultiTypeaheadSelectProps extends Omit { + /** @hide Forwarded ref */ + innerRef?: React.Ref; + /** Initial options of the select. */ + initialOptions: MultiTypeaheadSelectOption[]; + /** Callback triggered on selection. */ + onSelectionChange?: ( + _event: React.MouseEvent | React.KeyboardEvent, + selections: (string | number)[] + ) => void; + /** Callback triggered when the select opens or closes. */ + onToggle?: (nextIsOpen: boolean) => void; + /** Callback triggered when the text in the input field changes. */ + onInputChange?: (newValue: string) => void; + /** Placeholder text for the select input. */ + placeholder?: string; + /** Message to display when no options match the filter. */ + noOptionsFoundMessage?: string | ((filter: string) => string); + /** Flag indicating the select should be disabled. */ + isDisabled?: boolean; + /** Width of the toggle. */ + toggleWidth?: string; + /** Additional props passed to the toggle. */ + toggleProps?: MenuToggleProps; +} + +export const MultiTypeaheadSelectBase: React.FunctionComponent = ({ + innerRef, + initialOptions, + onSelectionChange, + onToggle, + onInputChange, + placeholder = 'Select an option', + noOptionsFoundMessage = (filter) => `No results found for "${filter}"`, + isDisabled, + toggleWidth, + toggleProps, + ...props +}: MultiTypeaheadSelectProps) => { + const [isOpen, setIsOpen] = React.useState(false); + const [selected, setSelected] = React.useState<(string | number)[]>( + (initialOptions?.filter((o) => o.selected) ?? []).map((o) => o.value) + ); + const [inputValue, setInputValue] = React.useState(); + const [selectOptions, setSelectOptions] = React.useState(initialOptions); + const [focusedItemIndex, setFocusedItemIndex] = React.useState(null); + const [activeItemId, setActiveItemId] = React.useState(null); + const textInputRef = React.useRef(); + + const NO_RESULTS = 'no results'; + + const openMenu = () => { + onToggle && onToggle(true); + setIsOpen(true); + }; + + React.useEffect(() => { + let newSelectOptions: MultiTypeaheadSelectOption[] = initialOptions; + + // Filter menu items based on the text input value when one exists + if (inputValue) { + newSelectOptions = initialOptions.filter((option) => + String(option.content).toLowerCase().includes(inputValue.toLowerCase()) + ); + + // When no options are found after filtering, display 'No results found' + if (!newSelectOptions.length) { + newSelectOptions = [ + { + isAriaDisabled: true, + content: + typeof noOptionsFoundMessage === 'string' ? noOptionsFoundMessage : noOptionsFoundMessage(inputValue), + value: NO_RESULTS + } + ]; + } + + // Open the menu when the input value changes and the new value is not empty + openMenu(); + } + + setSelectOptions(newSelectOptions); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [inputValue, initialOptions]); + + React.useEffect( + () => setSelected((initialOptions?.filter((o) => o.selected) ?? []).map((o) => o.value)), + [initialOptions] + ); + + const setActiveAndFocusedItem = (itemIndex: number) => { + setFocusedItemIndex(itemIndex); + const focusedItem = selectOptions[itemIndex]; + setActiveItemId(focusedItem.value as string); + }; + + const resetActiveAndFocusedItem = () => { + setFocusedItemIndex(null); + setActiveItemId(null); + }; + + const closeMenu = () => { + onToggle && onToggle(false); + setIsOpen(false); + resetActiveAndFocusedItem(); + setInputValue(''); + }; + + const onInputClick = () => { + if (!isOpen) { + openMenu(); + } else if (!inputValue) { + closeMenu(); + } + }; + + const selectOption = ( + _event: React.MouseEvent | React.KeyboardEvent | undefined, + option: string | number + ) => { + const selections = selected.includes(option) ? selected.filter((o) => option === o) : [...selected, option]; + + onSelectionChange && onSelectionChange(_event, selections); + setSelected(selections); + }; + + const clearOption = ( + _event: React.MouseEvent | React.KeyboardEvent | undefined, + option: string | number + ) => { + const selections = selected.filter((o) => option !== o); + onSelectionChange && onSelectionChange(_event, selections); + setSelected(selections); + }; + + const _onSelect = (_event: React.MouseEvent | undefined, value: string | number | undefined) => { + if (value && value !== NO_RESULTS) { + selectOption(_event, value); + } + }; + + const onTextInputChange = (_event: React.FormEvent, value: string) => { + setInputValue(value); + onInputChange && onInputChange(value); + + resetActiveAndFocusedItem(); + }; + + const handleMenuArrowKeys = (key: string) => { + let indexToFocus = 0; + + if (!isOpen) { + openMenu(); + } + + if (selectOptions.every((option) => option.isDisabled)) { + return; + } + + if (key === 'ArrowUp') { + // When no index is set or at the first index, focus to the last, otherwise decrement focus index + if (focusedItemIndex === null || focusedItemIndex === 0) { + indexToFocus = selectOptions.length - 1; + } else { + indexToFocus = focusedItemIndex - 1; + } + + // Skip disabled options + while (selectOptions[indexToFocus].isDisabled) { + indexToFocus--; + if (indexToFocus === -1) { + indexToFocus = selectOptions.length - 1; + } + } + } + + if (key === 'ArrowDown') { + // When no index is set or at the last index, focus to the first, otherwise increment focus index + if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) { + indexToFocus = 0; + } else { + indexToFocus = focusedItemIndex + 1; + } + + // Skip disabled options + while (selectOptions[indexToFocus].isDisabled) { + indexToFocus++; + if (indexToFocus === selectOptions.length) { + indexToFocus = 0; + } + } + } + + setActiveAndFocusedItem(indexToFocus); + }; + + const onInputKeyDown = (event: React.KeyboardEvent) => { + const focusedItem = focusedItemIndex !== null ? selectOptions[focusedItemIndex] : null; + + switch (event.key) { + case 'Enter': + if (isOpen && focusedItem && focusedItem.value !== NO_RESULTS && !focusedItem.isAriaDisabled) { + selectOption(event, focusedItem?.value); + } + + if (!isOpen) { + onToggle && onToggle(true); + setIsOpen(true); + } + + break; + case 'ArrowUp': + case 'ArrowDown': + event.preventDefault(); + handleMenuArrowKeys(event.key); + break; + } + }; + + const onToggleClick = () => { + onToggle && onToggle(!isOpen); + setIsOpen(!isOpen); + textInputRef?.current?.focus(); + }; + + const onClearButtonClick = (ev: React.MouseEvent) => { + setSelected([]); + onInputChange && onInputChange(''); + resetActiveAndFocusedItem(); + textInputRef?.current?.focus(); + onSelectionChange && onSelectionChange(ev, []); + }; + + const toggle = (toggleRef: React.Ref) => ( + + + + + {selected.map((selection, index) => ( + { + ev.stopPropagation(); + clearOption(ev, selection); + }} + > + {initialOptions.find((o) => o.value === selection)?.content} + + ))} + + + + + + + + ); + + return ( + + ); +}; + +MultiTypeaheadSelectBase.displayName = 'MultiTypeaheadSelectBase'; + +export const MultiTypeaheadSelect = React.forwardRef((props: MultiTypeaheadSelectProps, ref: React.Ref) => ( + +)); + +MultiTypeaheadSelect.displayName = 'MultiTypeaheadSelect'; diff --git a/packages/react-templates/src/components/Select/__tests__/MultiTypeaheadSelect.test.tsx b/packages/react-templates/src/components/Select/__tests__/MultiTypeaheadSelect.test.tsx new file mode 100644 index 00000000000..7316007c57c --- /dev/null +++ b/packages/react-templates/src/components/Select/__tests__/MultiTypeaheadSelect.test.tsx @@ -0,0 +1,414 @@ +import * as React from 'react'; +import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { MultiTypeaheadSelect } from '../MultiTypeaheadSelect'; +import styles from '@patternfly/react-styles/css/components/Menu/menu'; + +const getToggle = () => screen.getByRole('button', { name: 'Multi select Typeahead menu toggle' }); + +describe('MultiTypeaheadSelect', () => { + it('renders MultiTypeaheadSelect with options', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + + render(); + + const toggle = getToggle(); + + await user.click(toggle); + + const option1 = screen.getByText('Option 1'); + const option2 = screen.getByText('Option 2'); + const option3 = screen.getByText('Option 3'); + + expect(option1).toBeInTheDocument(); + expect(option2).toBeInTheDocument(); + expect(option3).toBeInTheDocument(); + }); + + it('selects options when clicked', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + + render(); + + const toggle = getToggle(); + + await user.click(toggle); + + const option1 = screen.getByRole('option', { name: 'Option 1' }); + + expect(option1).not.toHaveClass(styles.modifiers.selected); + + await user.click(option1); + + expect(option1).toHaveClass(styles.modifiers.selected); + }); + + it('calls the onSelectionChange callback with the selected value when an option is selected', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + const onSelectionChangeMock = jest.fn(); + + render(); + + const toggle = getToggle(); + + await user.click(toggle); + + expect(onSelectionChangeMock).not.toHaveBeenCalled(); + + const option1 = screen.getByRole('option', { name: 'Option 1' }); + + await user.click(option1); + + expect(onSelectionChangeMock).toHaveBeenCalledTimes(1); + expect(onSelectionChangeMock).toHaveBeenCalledWith(expect.anything(), 'option1'); + }); + + it('deselects options when clear button is clicked', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + + render(); + + const toggle = getToggle(); + + await user.click(toggle); + + const option1 = screen.getByRole('option', { name: 'Option 1' }); + await user.click(option1); + expect(option1).toHaveClass(styles.modifiers.selected); + + const input = screen.getByRole('combobox'); + + const clearButton = screen.getByRole('button', { name: 'Clear input value' }); + await user.click(clearButton); + + expect(input).toHaveValue(''); + + await user.click(toggle); + expect(screen.getByRole('option', { name: 'Option 1' })).not.toHaveClass(styles.modifiers.selected); + }); + + it('toggles the select menu when the toggle button is clicked', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + + render(); + + const toggle = getToggle(); + + await user.click(toggle); + + const menu = screen.getByRole('listbox'); + expect(menu).toBeInTheDocument(); + + await user.click(toggle); + + await waitForElementToBeRemoved(menu); + + expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); + }); + + it('calls the onToggle callback when the toggle is clicked', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + const onToggleMock = jest.fn(); + + render(); + + const toggle = getToggle(); + + expect(onToggleMock).not.toHaveBeenCalled(); + + await user.click(toggle); + + expect(onToggleMock).toHaveBeenCalledTimes(1); + expect(onToggleMock).toHaveBeenCalledWith(true); + + await user.click(toggle); + + expect(onToggleMock).toHaveBeenCalledTimes(2); + expect(onToggleMock).toHaveBeenCalledWith(false); + }); + + it('Passes toggleWidth', () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + render(); + + const toggle = getToggle(); + expect(toggle.parentElement).toHaveAttribute('style', 'width: 500px;'); + }); + + it('Passes additional toggleProps', () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + render(); + + const toggle = getToggle(); + expect(toggle.parentElement).toHaveAttribute('id', 'toggle'); + }); + + it('passes custom placeholder text', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + render(); + + const input = screen.getByRole('combobox'); + + expect(input).toHaveAttribute('placeholder', 'custom'); + }); + + it('displays noOptionsFoundMessage when filter returns no results', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + const user = userEvent.setup(); + + render(); + + const toggle = getToggle(); + const input = screen.getByRole('combobox'); + + expect(input).toHaveAttribute('placeholder', 'Select an option'); + + await user.click(toggle); + + const option1 = screen.getByRole('option', { name: 'Option 1' }); + const option2 = screen.getByRole('option', { name: 'Option 2' }); + const option3 = screen.getByRole('option', { name: 'Option 3' }); + + expect(option1).toBeInTheDocument(); + expect(option2).toBeInTheDocument(); + expect(option3).toBeInTheDocument(); + + await user.type(input, '9'); + expect(input).toHaveValue('9'); + + expect(option1).not.toBeInTheDocument(); + expect(option2).not.toBeInTheDocument(); + expect(option3).not.toBeInTheDocument(); + expect(screen.getByRole('option', { name: 'No results found for "9"' })).toBeInTheDocument(); + }); + + it('displays custom noOptionsFoundMessage', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + const user = userEvent.setup(); + + render(); + + const toggle = getToggle(); + const input = screen.getByRole('combobox'); + + expect(input).toHaveAttribute('placeholder', 'Select an option'); + + await user.click(toggle); + + const option1 = screen.getByRole('option', { name: 'Option 1' }); + const option2 = screen.getByRole('option', { name: 'Option 2' }); + const option3 = screen.getByRole('option', { name: 'Option 3' }); + + expect(option1).toBeInTheDocument(); + expect(option2).toBeInTheDocument(); + expect(option3).toBeInTheDocument(); + + await user.type(input, '9'); + expect(input).toHaveValue('9'); + + expect(option1).not.toBeInTheDocument(); + expect(option2).not.toBeInTheDocument(); + expect(option3).not.toBeInTheDocument(); + expect(screen.getByRole('option', { name: 'custom' })).toBeInTheDocument(); + }); + + it('disables the select when isDisabled prop is true', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + + render(); + + const toggle = getToggle(); + + expect(toggle.parentElement).toHaveClass(styles.modifiers.disabled); + + await user.click(toggle); + + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + }); + + it('passes other SelectOption props to the SelectOption component', async () => { + const initialOptions = [{ content: 'Option 1', value: 'option1', isDisabled: true }]; + + const user = userEvent.setup(); + + render(); + + const toggle = getToggle(); + + await user.click(toggle); + + const option1 = screen.getByRole('option', { name: 'Option 1' }); + expect(option1).toBeDisabled(); + }); + + it('displays the option in chip when option is selected', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + + render(); + + const toggle = getToggle(); + const input = screen.getByRole('combobox'); + + expect(input).toHaveAttribute('placeholder', 'Select an option'); + + await user.click(toggle); + + const option1 = screen.getByRole('option', { name: 'Option 1' }); + + await user.click(option1); + + await user.click(toggle); + + // TODO: How to find the chips? + + const clearButton = screen.getByRole('button', { name: 'Clear input value' }); + await user.click(clearButton); + + // TODO: How to find the chips? + }); + + it('typing in input filters options', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + + render(); + + const toggle = getToggle(); + const input = screen.getByRole('combobox'); + + expect(input).toHaveAttribute('placeholder', 'Select an option'); + + await user.click(toggle); + + const option1 = screen.getByRole('option', { name: 'Option 1' }); + const option2 = screen.getByRole('option', { name: 'Option 2' }); + const option3 = screen.getByRole('option', { name: 'Option 3' }); + + expect(option1).toBeInTheDocument(); + expect(option2).toBeInTheDocument(); + expect(option3).toBeInTheDocument(); + + await user.type(input, '1'); + expect(input).toHaveValue('1'); + + expect(option1).toBeInTheDocument(); + expect(option2).not.toBeInTheDocument(); + expect(option3).not.toBeInTheDocument(); + }); + + it('typing in input triggers onInputChange callback', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + const onInputChangeMock = jest.fn(); + + render(); + + const input = screen.getByRole('combobox'); + + expect(input).toHaveAttribute('placeholder', 'Select an option'); + + await user.type(input, '1'); + + expect(input).toHaveValue('1'); + expect(onInputChangeMock).toHaveBeenCalledTimes(1); + expect(onInputChangeMock).toHaveBeenCalledWith('1'); + }); + + it('Matches snapshot', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + + const { asFragment } = render(); + + const toggle = getToggle(); + await user.click(toggle); + + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/packages/react-templates/src/components/Select/__tests__/__snapshots__/MultiTypeaheadSelect.test.tsx.snap b/packages/react-templates/src/components/Select/__tests__/__snapshots__/MultiTypeaheadSelect.test.tsx.snap new file mode 100644 index 00000000000..258a5f8bfdd --- /dev/null +++ b/packages/react-templates/src/components/Select/__tests__/__snapshots__/MultiTypeaheadSelect.test.tsx.snap @@ -0,0 +1,180 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MultiTypeaheadSelect Matches snapshot 1`] = ` + +
+
+
+ + + +
+ +
+ +
+
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+
+`; diff --git a/packages/react-templates/src/components/Select/examples/MultiTypeaheadSelectDemo.tsx b/packages/react-templates/src/components/Select/examples/MultiTypeaheadSelectDemo.tsx new file mode 100644 index 00000000000..6d383760cee --- /dev/null +++ b/packages/react-templates/src/components/Select/examples/MultiTypeaheadSelectDemo.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { MultiTypeaheadSelect, MultiTypeaheadSelectOption } from '@patternfly/react-templates'; + +const Options = [ + { content: 'Alabama', value: 'option1' }, + { content: 'Arizona', value: 'option2' }, + { content: 'California', value: 'option3' }, + { content: 'Florida', value: 'option4' }, + { content: 'Massachusetts', value: 'option5' }, + { content: 'Maine', value: 'option6' }, + { content: 'New Jersey', value: 'option7' }, + { content: 'New Mexico', value: 'option8' }, + { content: 'New York', value: 'option9' }, + { content: 'North Carolina', value: 'option10' } +]; + +type SelectionsType = (string | number)[]; + +export const MultiSelectTypeaheadDemo: React.FunctionComponent = () => { + const [selected, setSelected] = React.useState(['option5']); + + const initialOptions = React.useMemo( + () => Options.map((o) => ({ ...o, selected: selected.includes(o.value) })), + [selected] + ); + + return ( + `No state was found for "${filter}"`} + onSelectionChange={(_ev, selections) => setSelected(selections)} + /> + ); +}; diff --git a/packages/react-templates/src/components/Select/examples/SelectTemplates.md b/packages/react-templates/src/components/Select/examples/SelectTemplates.md index 237d67df510..73dc07edd6b 100644 --- a/packages/react-templates/src/components/Select/examples/SelectTemplates.md +++ b/packages/react-templates/src/components/Select/examples/SelectTemplates.md @@ -12,7 +12,7 @@ Note: Templates live in their own package at [@patternfly/react-templates](https For custom use cases, please see the select component suite from [@patternfly/react-core](https://www.npmjs.com/package/@patternfly/react-core). import { Checkbox } from '@patternfly/react-core'; -import { SimpleSelect, CheckboxSelect, TypeaheadSelect } from '@patternfly/react-templates'; +import { SimpleSelect, CheckboxSelect, TypeaheadSelect, MultiTypeaheadSelect } from '@patternfly/react-templates'; ## Select template examples @@ -33,3 +33,9 @@ import { SimpleSelect, CheckboxSelect, TypeaheadSelect } from '@patternfly/react ```ts file="TypeaheadSelectDemo.tsx" ``` + +### Multi-Typeahead + +```ts file="MultiTypeaheadSelectDemo.tsx" + +``` diff --git a/packages/react-templates/src/components/Select/index.ts b/packages/react-templates/src/components/Select/index.ts index b5a5cb3446e..65582f19ab3 100644 --- a/packages/react-templates/src/components/Select/index.ts +++ b/packages/react-templates/src/components/Select/index.ts @@ -1,3 +1,4 @@ export * from './SimpleSelect'; export * from './CheckboxSelect'; export * from './TypeaheadSelect'; +export * from './MultiTypeaheadSelect'; From 06fec0f49a54540acfe8477a3a8b1fc870789d53 Mon Sep 17 00:00:00 2001 From: Titani Labaj <39532947+tlabaj@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:57:54 -0400 Subject: [PATCH 2/2] Update packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx Co-authored-by: kmcfaul <45077788+kmcfaul@users.noreply.github.com> --- .../src/components/Select/MultiTypeaheadSelect.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx b/packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx index 1a408bb6209..ff8a7973224 100644 --- a/packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx +++ b/packages/react-templates/src/components/Select/MultiTypeaheadSelect.tsx @@ -144,7 +144,7 @@ export const MultiTypeaheadSelectBase: React.FunctionComponent | React.KeyboardEvent | undefined, option: string | number ) => { - const selections = selected.includes(option) ? selected.filter((o) => option === o) : [...selected, option]; + const selections = selected.includes(option) ? selected.filter((o) => option !== o) : [...selected, option]; onSelectionChange && onSelectionChange(_event, selections); setSelected(selections);