From a167bdaf86fbf400c68f4b8b4621d6edce6e790a Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Mon, 9 Oct 2023 13:52:17 -0400 Subject: [PATCH 1/4] feat(Select): wrapped demo --- .../Select/examples/SelectTypeahead.tsx | 200 +---------------- .../react-core/src/components/Select/index.ts | 1 + .../Select/wrapped/SelectTypeahead.tsx | 206 ++++++++++++++++++ 3 files changed, 215 insertions(+), 192 deletions(-) create mode 100644 packages/react-core/src/components/Select/wrapped/SelectTypeahead.tsx diff --git a/packages/react-core/src/components/Select/examples/SelectTypeahead.tsx b/packages/react-core/src/components/Select/examples/SelectTypeahead.tsx index 8a7ffb04571..a727e1f32c8 100644 --- a/packages/react-core/src/components/Select/examples/SelectTypeahead.tsx +++ b/packages/react-core/src/components/Select/examples/SelectTypeahead.tsx @@ -1,17 +1,5 @@ import React from 'react'; -import { - Select, - SelectOption, - SelectList, - SelectOptionProps, - MenuToggle, - MenuToggleElement, - TextInputGroup, - TextInputGroupMain, - TextInputGroupUtilities, - Button -} from '@patternfly/react-core'; -import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; +import { SelectOptionProps, SelectTypeahead } from '@patternfly/react-core'; const initialSelectOptions: SelectOptionProps[] = [ { value: 'Alabama', children: 'Alabama' }, @@ -22,188 +10,16 @@ const initialSelectOptions: SelectOptionProps[] = [ { value: 'North Carolina', children: 'North Carolina' } ]; -export const SelectBasic: React.FunctionComponent = () => { - const [isOpen, setIsOpen] = React.useState(false); - const [selected, setSelected] = React.useState(''); - const [inputValue, setInputValue] = React.useState(''); - const [filterValue, setFilterValue] = React.useState(''); - const [selectOptions, setSelectOptions] = React.useState(initialSelectOptions); - const [focusedItemIndex, setFocusedItemIndex] = React.useState(null); - const [activeItem, setActiveItem] = React.useState(null); - const textInputRef = React.useRef(); - - React.useEffect(() => { - let newSelectOptions: SelectOptionProps[] = initialSelectOptions; - - // Filter menu items based on the text input value when one exists - if (filterValue) { - newSelectOptions = initialSelectOptions.filter((menuItem) => - String(menuItem.children).toLowerCase().includes(filterValue.toLowerCase()) - ); - - // When no options are found after filtering, display 'No results found' - if (!newSelectOptions.length) { - newSelectOptions = [ - { isDisabled: false, children: `No results found for "${filterValue}"`, value: 'no results' } - ]; - } - - // Open the menu when the input value changes and the new value is not empty - if (!isOpen) { - setIsOpen(true); - } - } - - setSelectOptions(newSelectOptions); - setActiveItem(null); - setFocusedItemIndex(null); - }, [filterValue]); - - const onToggleClick = () => { - setIsOpen(!isOpen); - }; - - const onSelect = (_event: React.MouseEvent | undefined, value: string | number | undefined) => { +export const SelectTypeaheadDemo: React.FunctionComponent = () => { + const onSelect = (value: string | number | undefined) => { // eslint-disable-next-line no-console - console.log('selected', value); - - if (value && value !== 'no results') { - setInputValue(value as string); - setFilterValue(''); - setSelected(value as string); - } - setIsOpen(false); - setFocusedItemIndex(null); - setActiveItem(null); - }; - - const onTextInputChange = (_event: React.FormEvent, value: string) => { - setInputValue(value); - setFilterValue(value); + console.log('selected:', value); }; - const handleMenuArrowKeys = (key: string) => { - let indexToFocus; - - if (isOpen) { - 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; - } - } - - 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; - } - } - - setFocusedItemIndex(indexToFocus); - const focusedItem = selectOptions.filter((option) => !option.isDisabled)[indexToFocus]; - setActiveItem(`select-typeahead-${focusedItem.value.replace(' ', '-')}`); - } - }; - - const onInputKeyDown = (event: React.KeyboardEvent) => { - const enabledMenuItems = selectOptions.filter((option) => !option.isDisabled); - const [firstMenuItem] = enabledMenuItems; - const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem; - - switch (event.key) { - // Select the first available option - case 'Enter': - if (isOpen && focusedItem.value !== 'no results') { - setInputValue(String(focusedItem.children)); - setFilterValue(''); - setSelected(String(focusedItem.children)); - } - - setIsOpen((prevIsOpen) => !prevIsOpen); - setFocusedItemIndex(null); - setActiveItem(null); - - break; - case 'Tab': - case 'Escape': - setIsOpen(false); - setActiveItem(null); - break; - case 'ArrowUp': - case 'ArrowDown': - event.preventDefault(); - handleMenuArrowKeys(event.key); - break; - } + const onInput = (value: string) => { + // eslint-disable-next-line no-console + console.log('new input:', value); }; - const toggle = (toggleRef: React.Ref) => ( - - - - - - {!!inputValue && ( - - )} - - - - ); - - return ( - - ); + return ; }; diff --git a/packages/react-core/src/components/Select/index.ts b/packages/react-core/src/components/Select/index.ts index 5a8bca40468..ad628b4831c 100644 --- a/packages/react-core/src/components/Select/index.ts +++ b/packages/react-core/src/components/Select/index.ts @@ -2,3 +2,4 @@ export * from './Select'; export * from './SelectGroup'; export * from './SelectList'; export * from './SelectOption'; +export * from './wrapped/SelectTypeahead'; diff --git a/packages/react-core/src/components/Select/wrapped/SelectTypeahead.tsx b/packages/react-core/src/components/Select/wrapped/SelectTypeahead.tsx new file mode 100644 index 00000000000..18fa7f4e075 --- /dev/null +++ b/packages/react-core/src/components/Select/wrapped/SelectTypeahead.tsx @@ -0,0 +1,206 @@ +import React from 'react'; +import { Select, SelectOption, SelectList, SelectOptionProps } from '../../Select'; +import { MenuToggle, MenuToggleElement } from '../../MenuToggle'; +import { TextInputGroup, TextInputGroupMain, TextInputGroupUtilities } from '../../TextInputGroup'; +import { Button } from '../../Button'; +import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; + +export const SelectTypeahead: React.FunctionComponent = ({ + initialOptions, + onSelect, + onInput +}: { + initialOptions: SelectOptionProps[]; + onSelect?: (newValue: string) => void; + onInput?: (newValue: string) => void; +}) => { + const [isOpen, setIsOpen] = React.useState(false); + const [selected, setSelected] = React.useState(''); + const [inputValue, setInputValue] = React.useState(''); + const [filterValue, setFilterValue] = React.useState(''); + const [selectOptions, setSelectOptions] = React.useState(initialOptions); + const [focusedItemIndex, setFocusedItemIndex] = React.useState(null); + const [activeItem, setActiveItem] = React.useState(null); + const textInputRef = React.useRef(); + + React.useEffect(() => { + let newSelectOptions: SelectOptionProps[] = initialOptions; + + // Filter menu items based on the text input value when one exists + if (filterValue) { + newSelectOptions = initialOptions.filter((menuItem: SelectOptionProps) => + String(menuItem.children).toLowerCase().includes(filterValue.toLowerCase()) + ); + + // When no options are found after filtering, display 'No results found' + if (!newSelectOptions.length) { + newSelectOptions = [ + { isDisabled: false, children: `No results found for "${filterValue}"`, value: 'no results' } + ]; + } + + // Open the menu when the input value changes and the new value is not empty + if (!isOpen) { + setIsOpen(true); + } + } + + setSelectOptions(newSelectOptions); + setActiveItem(null); + setFocusedItemIndex(null); + }, [filterValue]); + + const onToggleClick = () => { + setIsOpen(!isOpen); + }; + + const _onSelect = (_event: React.MouseEvent | undefined, value: string | number | undefined) => { + if (value && value !== 'no results') { + setInputValue(value as string); + setFilterValue(''); + setSelected(value as string); + onSelect(value as string); + } + setIsOpen(false); + setFocusedItemIndex(null); + setActiveItem(null); + }; + + const onTextInputChange = (_event: React.FormEvent, value: string) => { + setInputValue(value); + onInput(value); + setFilterValue(value); + }; + + const handleMenuArrowKeys = (key: string) => { + let indexToFocus; + + if (isOpen) { + 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; + } + } + + 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; + } + } + + setFocusedItemIndex(indexToFocus); + const focusedItem = selectOptions.filter((option) => !option.isDisabled)[indexToFocus]; + setActiveItem(`select-typeahead-${focusedItem.value.replace(' ', '-')}`); + } + }; + + const onInputKeyDown = (event: React.KeyboardEvent) => { + const enabledMenuItems = selectOptions.filter((option) => !option.isDisabled); + const [firstMenuItem] = enabledMenuItems; + const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem; + + switch (event.key) { + // Select the first available option + case 'Enter': + if (isOpen && focusedItem.value !== 'no results') { + setInputValue(String(focusedItem.children)); + setFilterValue(''); + onSelect(String(focusedItem.children)); + setSelected(String(focusedItem.children)); + } + + setIsOpen((prevIsOpen) => !prevIsOpen); + setFocusedItemIndex(null); + setActiveItem(null); + + break; + case 'Tab': + case 'Escape': + setIsOpen(false); + setActiveItem(null); + break; + case 'ArrowUp': + case 'ArrowDown': + event.preventDefault(); + handleMenuArrowKeys(event.key); + break; + } + }; + + const toggle = (toggleRef: React.Ref) => ( + + + + + + {!!inputValue && ( + + )} + + + + ); + + return ( + + ); +}; +SelectTypeahead.displayName = 'SelectTypeahead'; From 532fff4e4eb7b720e2dd85e2fc3ae14de40b97c3 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Mon, 9 Oct 2023 14:14:35 -0400 Subject: [PATCH 2/4] update loc --- .../components/Select/{wrapped => }/SelectTypeahead.tsx | 8 ++++---- packages/react-core/src/components/Select/index.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename packages/react-core/src/components/Select/{wrapped => }/SelectTypeahead.tsx (97%) diff --git a/packages/react-core/src/components/Select/wrapped/SelectTypeahead.tsx b/packages/react-core/src/components/Select/SelectTypeahead.tsx similarity index 97% rename from packages/react-core/src/components/Select/wrapped/SelectTypeahead.tsx rename to packages/react-core/src/components/Select/SelectTypeahead.tsx index 18fa7f4e075..a5a836afe5d 100644 --- a/packages/react-core/src/components/Select/wrapped/SelectTypeahead.tsx +++ b/packages/react-core/src/components/Select/SelectTypeahead.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { Select, SelectOption, SelectList, SelectOptionProps } from '../../Select'; -import { MenuToggle, MenuToggleElement } from '../../MenuToggle'; -import { TextInputGroup, TextInputGroupMain, TextInputGroupUtilities } from '../../TextInputGroup'; -import { Button } from '../../Button'; +import { Select, SelectOption, SelectList, SelectOptionProps } from '.'; +import { MenuToggle, MenuToggleElement } from '../MenuToggle'; +import { TextInputGroup, TextInputGroupMain, TextInputGroupUtilities } from '../TextInputGroup'; +import { Button } from '../Button'; import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; export const SelectTypeahead: React.FunctionComponent = ({ diff --git a/packages/react-core/src/components/Select/index.ts b/packages/react-core/src/components/Select/index.ts index ad628b4831c..3032cdae87c 100644 --- a/packages/react-core/src/components/Select/index.ts +++ b/packages/react-core/src/components/Select/index.ts @@ -2,4 +2,4 @@ export * from './Select'; export * from './SelectGroup'; export * from './SelectList'; export * from './SelectOption'; -export * from './wrapped/SelectTypeahead'; +export * from './SelectTypeahead'; From 5d339197cc5a451ae63e8d0a2f793a00aef2d72b Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Mon, 9 Oct 2023 14:21:15 -0400 Subject: [PATCH 3/4] dependency --- packages/react-core/src/components/Select/index.ts | 2 +- .../src/{components/Select => demos}/SelectTypeahead.tsx | 8 ++++---- packages/react-core/src/demos/index.ts | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) rename packages/react-core/src/{components/Select => demos}/SelectTypeahead.tsx (97%) diff --git a/packages/react-core/src/components/Select/index.ts b/packages/react-core/src/components/Select/index.ts index 3032cdae87c..ba371183904 100644 --- a/packages/react-core/src/components/Select/index.ts +++ b/packages/react-core/src/components/Select/index.ts @@ -2,4 +2,4 @@ export * from './Select'; export * from './SelectGroup'; export * from './SelectList'; export * from './SelectOption'; -export * from './SelectTypeahead'; +export * from '../../demos/SelectTypeahead'; diff --git a/packages/react-core/src/components/Select/SelectTypeahead.tsx b/packages/react-core/src/demos/SelectTypeahead.tsx similarity index 97% rename from packages/react-core/src/components/Select/SelectTypeahead.tsx rename to packages/react-core/src/demos/SelectTypeahead.tsx index a5a836afe5d..17c7aecc1c2 100644 --- a/packages/react-core/src/components/Select/SelectTypeahead.tsx +++ b/packages/react-core/src/demos/SelectTypeahead.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { Select, SelectOption, SelectList, SelectOptionProps } from '.'; -import { MenuToggle, MenuToggleElement } from '../MenuToggle'; -import { TextInputGroup, TextInputGroupMain, TextInputGroupUtilities } from '../TextInputGroup'; -import { Button } from '../Button'; +import { Select, SelectOption, SelectList, SelectOptionProps } from '../components/Select'; +import { MenuToggle, MenuToggleElement } from '../components/MenuToggle'; +import { TextInputGroup, TextInputGroupMain, TextInputGroupUtilities } from '../components/TextInputGroup'; +import { Button } from '../components/Button'; import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; export const SelectTypeahead: React.FunctionComponent = ({ diff --git a/packages/react-core/src/demos/index.ts b/packages/react-core/src/demos/index.ts index e10143716db..8665f6284e9 100644 --- a/packages/react-core/src/demos/index.ts +++ b/packages/react-core/src/demos/index.ts @@ -1,2 +1,3 @@ export * from './DashboardHeader'; export * from './DashboardWrapper'; +export * from './SelectTypeahead'; From e9afdb1db758595495b20945d1416f4ee39c77d0 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Tue, 10 Oct 2023 09:54:45 -0400 Subject: [PATCH 4/4] remove import from other loc --- packages/react-core/src/components/Select/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-core/src/components/Select/index.ts b/packages/react-core/src/components/Select/index.ts index ba371183904..5a8bca40468 100644 --- a/packages/react-core/src/components/Select/index.ts +++ b/packages/react-core/src/components/Select/index.ts @@ -2,4 +2,3 @@ export * from './Select'; export * from './SelectGroup'; export * from './SelectList'; export * from './SelectOption'; -export * from '../../demos/SelectTypeahead';