From 3d1d41a518e0748ec2a3e8e17210585ae980f1c4 Mon Sep 17 00:00:00 2001 From: Ashley McEntee Date: Tue, 2 Jul 2024 15:35:15 -0400 Subject: [PATCH] Update depricated PF multiselect --- .../cypress/cypress/pages/userManagement.ts | 9 +- frontend/src/components/MultiSelection.tsx | 225 +++++++++++++++--- .../src/pages/groupSettings/GroupSettings.tsx | 24 +- .../src/pages/groupSettings/groupTypes.ts | 6 - 4 files changed, 214 insertions(+), 50 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/pages/userManagement.ts b/frontend/src/__tests__/cypress/cypress/pages/userManagement.ts index 491510a95c..1dd1ef0041 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/userManagement.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/userManagement.ts @@ -8,7 +8,7 @@ class GroupSettingSection extends Contextual { } clearMultiChipItem() { - this.find().findByRole('button', { name: 'Clear all' }).click(); + this.find().findByRole('button', { name: 'Clear input value' }).click(); } findMultiGroupInput() { @@ -16,11 +16,11 @@ class GroupSettingSection extends Contextual { } findMultiGroupOptions(name: string) { - return this.find().findByTestId('multi-group-selection').findByRole('option', { name }); + return this.find().findByRole('option', { name }); } private findChipGroup() { - return this.find().findByRole('list', { name: 'Chip group category' }); + return this.find().findByRole('list', { name: 'Current selections' }); } findChipItem(name: string | RegExp) { @@ -29,7 +29,8 @@ class GroupSettingSection extends Contextual { removeChipItem(name: string) { this.findChipGroup() - .findByRole('button', { name: `Remove ${name}` }) + .find('li') + .findByRole('button', { name: `close ${name}` }) .click(); } diff --git a/frontend/src/components/MultiSelection.tsx b/frontend/src/components/MultiSelection.tsx index 123c38652e..f5cf67f414 100644 --- a/frontend/src/components/MultiSelection.tsx +++ b/frontend/src/components/MultiSelection.tsx @@ -1,54 +1,209 @@ import * as React from 'react'; -import { HelperText, HelperTextItem } from '@patternfly/react-core'; -import { Select, SelectOption, SelectVariant } from '@patternfly/react-core/deprecated'; -import { MenuItemStatus } from '~/pages/groupSettings/groupTypes'; +import { + Select, + SelectOption, + SelectList, + MenuToggle, + MenuToggleElement, + TextInputGroup, + TextInputGroupMain, + TextInputGroupUtilities, + ChipGroup, + Chip, + Button, + HelperText, + HelperTextItem, +} from '@patternfly/react-core'; +import { TimesIcon } from '@patternfly/react-icons/dist/esm/icons/times-icon'; + +export type SelectionOptions = { + id: number | string; + name: string; + selected?: boolean; +}; type MultiSelectionProps = { - value: MenuItemStatus[]; - setValue: (itemSelection: MenuItemStatus[]) => void; + value: SelectionOptions[]; + setValue: (itemSelection: SelectionOptions[]) => void; ariaLabel: string; }; -export const MultiSelection: React.FC = ({ value, setValue, ariaLabel }) => { - const [showMenu, setShowMenu] = React.useState(false); +export const MultiSelection: React.FC = ({ value, setValue }) => { + const [isOpen, setIsOpen] = React.useState(false); + const [inputValue, setInputValue] = React.useState(''); + const [focusedItemIndex, setFocusedItemIndex] = React.useState(null); + const [activeItem, setActiveItem] = React.useState(null); + const textInputRef = React.useRef(); + const selected = React.useMemo(() => value.filter((v) => v.selected), [value]); + const selectOptions = React.useMemo( + () => + value.filter((v) => !inputValue || v.name.toLowerCase().includes(inputValue.toLowerCase())), + [inputValue, value], + ); + + React.useEffect(() => { + if (inputValue) { + setIsOpen(true); + } + setFocusedItemIndex(null); + setActiveItem(null); + }, [inputValue]); + + const handleMenuArrowKeys = (key: string) => { + let indexToFocus; + if (!isOpen) { + setIsOpen(true); + return; + } + + if (key === 'ArrowUp') { + if (focusedItemIndex === null || focusedItemIndex === 0) { + indexToFocus = selectOptions.length - 1; + } else { + indexToFocus = focusedItemIndex - 1; + } + } + + if (key === 'ArrowDown') { + if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) { + indexToFocus = 0; + } else { + indexToFocus = focusedItemIndex + 1; + } + } - const toggleMenu = (isOpen: React.SetStateAction) => { - setShowMenu(isOpen); + if (indexToFocus != null) { + setFocusedItemIndex(indexToFocus); + const focusedItem = selectOptions[indexToFocus]; + setActiveItem(`select-multi-typeahead-${focusedItem.name.replace(' ', '-')}`); + } }; - const clearSelection = () => { - const newState = value.map((element) => ({ ...element, enabled: false })); - setValue(newState); + const onInputKeyDown = (event: React.KeyboardEvent) => { + const focusedItem = focusedItemIndex !== null ? selectOptions[focusedItemIndex] : null; + switch (event.key) { + case 'Enter': + if (isOpen && focusedItem) { + onSelect(focusedItem); + } + if (!isOpen) { + setIsOpen(true); + } + break; + case 'Tab': + case 'Escape': + setIsOpen(false); + setActiveItem(null); + break; + case 'ArrowUp': + case 'ArrowDown': + event.preventDefault(); + handleMenuArrowKeys(event.key); + break; + } }; - const noSelectedItems = value.filter((option) => option.enabled).length === 0; + const onToggleClick = () => { + setIsOpen(!isOpen); + setTimeout(() => textInputRef.current?.focus(), 100); + }; + const onTextInputChange = (_event: React.FormEvent, valueOfInput: string) => { + setInputValue(valueOfInput); + }; + const onSelect = (menuItem?: SelectionOptions) => { + if (menuItem) { + setValue( + selected.includes(menuItem) + ? value.map((option) => (option === menuItem ? { ...option, enabled: false } : option)) + : value.map((option) => (option === menuItem ? { ...option, enabled: true } : option)), + ); + } + textInputRef.current?.focus(); + }; + + const noSelectedItems = value.filter((option) => option.selected).length === 0; + + const toggle = (toggleRef: React.Ref) => ( + + + + + {selected.map((selection, index) => ( + { + ev.stopPropagation(); + onSelect(selection); + }} + > + {selection.name} + + ))} + + + + {selected.length > 0 && ( + + )} + + + + ); return ( <> {noSelectedItems && ( diff --git a/frontend/src/pages/groupSettings/GroupSettings.tsx b/frontend/src/pages/groupSettings/GroupSettings.tsx index 362d4fa61b..6cf0acfda2 100644 --- a/frontend/src/pages/groupSettings/GroupSettings.tsx +++ b/frontend/src/pages/groupSettings/GroupSettings.tsx @@ -11,9 +11,9 @@ import { import ApplicationsPage from '~/pages/ApplicationsPage'; import { isGroupEmpty } from '~/utilities/utils'; import SettingSection from '~/components/SettingSection'; -import { MultiSelection } from '~/components/MultiSelection'; +import { MultiSelection, SelectionOptions } from '~/components/MultiSelection'; import { useWatchGroups } from '~/utilities/useWatchGroups'; -import { GroupsConfigField, MenuItemStatus } from './groupTypes'; +import { GroupsConfigField } from './groupTypes'; const GroupSettings: React.FC = () => { const { @@ -37,13 +37,27 @@ const GroupSettings: React.FC = () => { updateGroups(groupSettings); }; - const handleMenuItemSelection = (newState: MenuItemStatus[], field: GroupsConfigField) => { + const handleMenuItemSelection = (newState: SelectionOptions[], field: GroupsConfigField) => { switch (field) { case GroupsConfigField.ADMIN: - setGroupSettings({ ...groupSettings, adminGroups: newState }); + setGroupSettings({ + ...groupSettings, + adminGroups: newState.map((opt) => ({ + id: Number(opt.id), + name: opt.name, + enabled: opt.selected || false, + })), + }); break; case GroupsConfigField.USER: - setGroupSettings({ ...groupSettings, allowedGroups: newState }); + setGroupSettings({ + ...groupSettings, + allowedGroups: newState.map((opt) => ({ + id: Number(opt.id), + name: opt.name, + enabled: opt.selected || false, + })), + }); break; } setIsGroupSettingsChanged(true); diff --git a/frontend/src/pages/groupSettings/groupTypes.ts b/frontend/src/pages/groupSettings/groupTypes.ts index c98bf07acd..e399469ffd 100644 --- a/frontend/src/pages/groupSettings/groupTypes.ts +++ b/frontend/src/pages/groupSettings/groupTypes.ts @@ -15,9 +15,3 @@ export type GroupStatus = { name: string; enabled: boolean; }; - -export type MenuItemStatus = { - id: number; - name: string; - enabled: boolean; -};