Skip to content

Commit

Permalink
Update depricated PF multiselect
Browse files Browse the repository at this point in the history
  • Loading branch information
ashley-o0o committed Jul 15, 2024
1 parent af0d946 commit 413e886
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ class GroupSettingSection extends Contextual<HTMLElement> {
}

clearMultiChipItem() {
this.find().findByRole('button', { name: 'Clear all' }).click();
this.find().findByRole('button', { name: 'Clear input value' }).click();
}

findMultiGroupInput() {
return this.find().find('input');
}

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) {
Expand All @@ -29,7 +29,8 @@ class GroupSettingSection extends Contextual<HTMLElement> {

removeChipItem(name: string) {
this.findChipGroup()
.findByRole('button', { name: `Remove ${name}` })
.find('li')
.findByRole('button', { name: `close ${name}` })
.click();
}

Expand Down
225 changes: 190 additions & 35 deletions frontend/src/components/MultiSelection.tsx
Original file line number Diff line number Diff line change
@@ -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<MultiSelectionProps> = ({ value, setValue, ariaLabel }) => {
const [showMenu, setShowMenu] = React.useState(false);
export const MultiSelection: React.FC<MultiSelectionProps> = ({ value, setValue }) => {
const [isOpen, setIsOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState<string>('');
const [focusedItemIndex, setFocusedItemIndex] = React.useState<number | null>(null);
const [activeItem, setActiveItem] = React.useState<string | null>(null);
const textInputRef = React.useRef<HTMLInputElement>();
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<boolean>) => {
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<HTMLInputElement>) => {
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<HTMLInputElement>, valueOfInput: string) => {
setInputValue(valueOfInput);
};
const onSelect = (menuItem?: SelectionOptions) => {
if (menuItem) {
setValue(
selected.includes(menuItem)
? value.map((option) => (option === menuItem ? { ...option, selected: false } : option))
: value.map((option) => (option === menuItem ? { ...option, selected: true } : option)),
);
}
textInputRef.current?.focus();
};

const noSelectedItems = value.filter((option) => option.selected).length === 0;

const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
variant="typeahead"
aria-label="Options menu"
onClick={onToggleClick}
innerRef={toggleRef}
isExpanded={isOpen}
isFullWidth
>
<TextInputGroup isPlain>
<TextInputGroupMain
value={inputValue}
onClick={onToggleClick}
onChange={onTextInputChange}
onKeyDown={onInputKeyDown}
autoComplete="off"
innerRef={textInputRef}
{...(activeItem && { 'aria-activedescendant': activeItem })}
role="combobox"
isExpanded={isOpen}
aria-controls="select-multi-typeahead-listbox"
>
<ChipGroup aria-label="Current selections">
{selected.map((selection, index) => (
<Chip
key={index}
onClick={(ev) => {
ev.stopPropagation();
onSelect(selection);
}}
>
{selection.name}
</Chip>
))}
</ChipGroup>
</TextInputGroupMain>
<TextInputGroupUtilities>
{selected.length > 0 && (
<Button
variant="plain"
onClick={() => {
setInputValue('');
setValue(value.map((option) => ({ ...option, selected: false })));
textInputRef.current?.focus();
}}
aria-label="Clear input value"
>
<TimesIcon aria-hidden />
</Button>
)}
</TextInputGroupUtilities>
</TextInputGroup>
</MenuToggle>
);

return (
<>
<Select
data-testid="multi-group-selection"
variant={SelectVariant.typeaheadMulti}
onToggle={(e, isOpen: React.SetStateAction<boolean>) => toggleMenu(isOpen)}
onSelect={(e, newValue) => {
if (value.filter((option) => option.name === newValue).length) {
const newState = value.map((element) =>
element.name === newValue ? { ...element, enabled: !element.enabled } : element,
);
setValue(newState);
}
}}
onClear={clearSelection}
selections={value.filter((element) => element.enabled).map((element) => element.name)}
isOpen={showMenu}
aria-label="Select groups menu"
typeAheadAriaLabel={ariaLabel}
isCreatable={false}
onCreateOption={undefined}
validated={noSelectedItems ? 'error' : 'default'}
isOpen={isOpen}
selected={selected}
onSelect={(ev, selection) => onSelect(value.find((option) => option.name === selection))}
onOpenChange={() => setIsOpen(false)}
toggle={toggle}
>
{value.map((option, index) => (
<SelectOption isDisabled={false} key={index} value={option.name} />
))}
<SelectList isAriaMultiselectable>
{selectOptions.length === 0 && inputValue ? (
<SelectOption isDisabled>No results found</SelectOption>
) : (
selectOptions.map((option, index) => (
<SelectOption
key={option.name}
isFocused={focusedItemIndex === index}
id={`select-multi-typeahead-${option.name.replace(' ', '-')}`}
value={option.name}
ref={null}
>
{option.name}
</SelectOption>
))
)}
</SelectList>
</Select>
{noSelectedItems && (
<HelperText>
Expand Down
36 changes: 29 additions & 7 deletions frontend/src/pages/groupSettings/GroupSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -78,7 +92,11 @@ const GroupSettings: React.FC = () => {
>
<MultiSelection
ariaLabel={adminDesc}
value={groupSettings.adminGroups}
value={groupSettings.adminGroups.map((g) => ({
id: g.id,
name: g.name,
selected: g.enabled,
}))}
setValue={(newState) => handleMenuItemSelection(newState, GroupsConfigField.ADMIN)}
/>
{groupSettings.errorAdmin ? (
Expand Down Expand Up @@ -111,7 +129,11 @@ const GroupSettings: React.FC = () => {
>
<MultiSelection
ariaLabel={userDesc}
value={groupSettings.allowedGroups}
value={groupSettings.allowedGroups.map((g) => ({
id: g.id,
name: g.name,
selected: g.enabled,
}))}
setValue={(newState) => handleMenuItemSelection(newState, GroupsConfigField.USER)}
/>
{groupSettings.errorUser ? (
Expand Down
6 changes: 0 additions & 6 deletions frontend/src/pages/groupSettings/groupTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,3 @@ export type GroupStatus = {
name: string;
enabled: boolean;
};

export type MenuItemStatus = {
id: number;
name: string;
enabled: boolean;
};

0 comments on commit 413e886

Please sign in to comment.