From 41054301d267d69bbc8621c4e98d9712750ddbcf Mon Sep 17 00:00:00 2001 From: Jeff Phillips Date: Mon, 29 Jul 2024 11:44:33 -0400 Subject: [PATCH] feat(TypeaheadSelect) Add creation options to TypeaheadSelect (#10802) --- .../src/components/Select/TypeaheadSelect.tsx | 57 ++++++++++++++----- .../Select/examples/TypeaheadSelectDemo.tsx | 48 +++++++++++++--- 2 files changed, 82 insertions(+), 23 deletions(-) diff --git a/packages/react-templates/src/components/Select/TypeaheadSelect.tsx b/packages/react-templates/src/components/Select/TypeaheadSelect.tsx index 16804dc7d28..7bbc5c24ad6 100644 --- a/packages/react-templates/src/components/Select/TypeaheadSelect.tsx +++ b/packages/react-templates/src/components/Select/TypeaheadSelect.tsx @@ -40,6 +40,12 @@ export interface TypeaheadSelectProps extends Omit { onClearSelection?: () => void; /** Placeholder text for the select input. */ placeholder?: string; + /** Flag to indicate if the typeahead select allows new items */ + isCreatable?: boolean; + /** Flag to indicate if create option should be at top of typeahead */ + isCreateOptionOnTop?: boolean; + /** Message to display to create a new option */ + createOptionMessage?: string | ((newValue: string) => string); /** Message to display when no options are available. */ noOptionsAvailableMessage?: string; /** Message to display when no options match the filter. */ @@ -52,6 +58,9 @@ export interface TypeaheadSelectProps extends Omit { toggleProps?: MenuToggleProps; } +const defaultNoOptionsFoundMessage = (filter: string) => `No results found for "${filter}"`; +const defaultCreateOptionMessage = (newValue: string) => `Create "${newValue}"`; + export const TypeaheadSelectBase: React.FunctionComponent = ({ innerRef, initialOptions, @@ -61,7 +70,10 @@ export const TypeaheadSelectBase: React.FunctionComponent onClearSelection, placeholder = 'Select an option', noOptionsAvailableMessage = 'No options are available', - noOptionsFoundMessage = (filter) => `No results found for "${filter}"`, + noOptionsFoundMessage = defaultNoOptionsFoundMessage, + isCreatable = false, + isCreateOptionOnTop = false, + createOptionMessage = defaultCreateOptionMessage, isDisabled, toggleWidth, toggleProps, @@ -89,6 +101,20 @@ export const TypeaheadSelectBase: React.FunctionComponent String(option.content).toLowerCase().includes(filterValue.toLowerCase()) ); + if ( + isCreatable && + filterValue && + !initialOptions.find((o) => String(o.content).toLowerCase() === filterValue.toLowerCase()) + ) { + const createOption = { + content: typeof createOptionMessage === 'string' ? createOptionMessage : createOptionMessage(filterValue), + value: filterValue + }; + newSelectOptions = isCreateOptionOnTop + ? [createOption, ...newSelectOptions] + : [...newSelectOptions, createOption]; + } + // When no options are found after filtering, display 'No results found' if (!newSelectOptions.length) { newSelectOptions = [ @@ -102,9 +128,7 @@ export const TypeaheadSelectBase: React.FunctionComponent } // Open the menu when the input value changes and the new value is not empty - if (!isOpen) { - openMenu(); - } + openMenu(); } // When no options are available, display 'No options available' @@ -119,7 +143,15 @@ export const TypeaheadSelectBase: React.FunctionComponent } setSelectOptions(newSelectOptions); - }, [filterValue, initialOptions]); + }, [ + filterValue, + initialOptions, + noOptionsFoundMessage, + isCreatable, + isCreateOptionOnTop, + createOptionMessage, + noOptionsAvailableMessage + ]); React.useEffect(() => { const selectedOption = initialOptions.find((o) => o.selected); @@ -138,8 +170,10 @@ export const TypeaheadSelectBase: React.FunctionComponent }; const openMenu = () => { - onToggle && onToggle(true); - setIsOpen(true); + if (!isOpen) { + onToggle && onToggle(true); + setIsOpen(true); + } }; const closeMenu = () => { @@ -191,9 +225,7 @@ export const TypeaheadSelectBase: React.FunctionComponent const handleMenuArrowKeys = (key: string) => { let indexToFocus = 0; - if (!isOpen) { - openMenu(); - } + openMenu(); if (selectOptions.every((option) => option.isDisabled)) { return; @@ -245,10 +277,7 @@ export const TypeaheadSelectBase: React.FunctionComponent selectOption(event, focusedItem); } - if (!isOpen) { - onToggle && onToggle(true); - setIsOpen(true); - } + openMenu(); break; case 'ArrowUp': diff --git a/packages/react-templates/src/components/Select/examples/TypeaheadSelectDemo.tsx b/packages/react-templates/src/components/Select/examples/TypeaheadSelectDemo.tsx index 3489810f744..6fbbb933092 100644 --- a/packages/react-templates/src/components/Select/examples/TypeaheadSelectDemo.tsx +++ b/packages/react-templates/src/components/Select/examples/TypeaheadSelectDemo.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { TypeaheadSelect, TypeaheadSelectOption } from '@patternfly/react-templates'; +import { Checkbox } from '@patternfly/react-core'; const Options = [ { content: 'Alabama', value: 'option1' }, @@ -13,10 +14,13 @@ const Options = [ /* eslint-disable no-console */ export const SelectTypeaheadDemo: React.FunctionComponent = () => { const [selected, setSelected] = React.useState(); + const [options, setOptions] = React.useState(Options); + const [isCreatable, setIsCreatable] = React.useState(false); + const [isCreateOptionOnTop, setIsCreateOptionOnTop] = React.useState(false); const initialOptions = React.useMemo( - () => Options.map((o) => ({ ...o, selected: o.value === selected })), - [selected] + () => options.map((o) => ({ ...o, selected: o.value === selected })), + [options, selected] ); React.useEffect(() => { @@ -24,12 +28,38 @@ export const SelectTypeaheadDemo: React.FunctionComponent = () => { }, [selected]); return ( - `No state was found for "${filter}"`} - onClearSelection={() => setSelected(undefined)} - onSelect={(_ev, selection) => setSelected(String(selection))} - /> + <> + `No state was found for "${filter}"`} + onClearSelection={() => setSelected(undefined)} + onSelect={(_ev, selection) => { + if (!options.find((o) => o.content === selection)) { + setOptions([...options, { content: String(selection), value: String(selection) }]); + } + setSelected(String(selection)); + }} + isCreatable={isCreatable} + isCreateOptionOnTop={isCreateOptionOnTop} + /> + setIsCreatable(checked)} + aria-label="toggle creatable checkbox" + id="toggle-creatable-typeahead" + name="toggle-creatable-typeahead" + /> + setIsCreateOptionOnTop(checked)} + aria-label="toggle createOptionOnTop checkbox" + id="toggle-create-option-on-top-typeahead" + name="toggle-create-option-on-top-typeahead" + /> + ); };