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 9, 2024
1 parent af0d946 commit 0d5475c
Showing 1 changed file with 178 additions and 31 deletions.
209 changes: 178 additions & 31 deletions frontend/src/components/MultiSelection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import * as React from 'react';
import { HelperText, HelperTextItem } from '@patternfly/react-core';
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core/deprecated';
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';
import { MenuItemStatus } from '~/pages/groupSettings/groupTypes';

type MultiSelectionProps = {
Expand All @@ -9,46 +23,179 @@ type MultiSelectionProps = {
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.enabled), [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) {
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;
}
}

if (indexToFocus != null) {
setFocusedItemIndex(indexToFocus);
const focusedItem = selectOptions[indexToFocus];
setActiveItem(`select-multi-typeahead-${focusedItem.name.replace(' ', '-')}`);
}
}
};

const toggleMenu = (isOpen: React.SetStateAction<boolean>) => {
setShowMenu(isOpen);
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':

Check failure on line 90 in frontend/src/components/MultiSelection.tsx

View workflow job for this annotation

GitHub Actions / Tests (18.x)

Delete `··`
case 'ArrowDown':
event.preventDefault();
handleMenuArrowKeys(event.key);
break;
}
};

const clearSelection = () => {
const newState = value.map((element) => ({ ...element, enabled: false }));
setValue(newState);
const onToggleClick = () => {
setIsOpen(!isOpen);
setTimeout(() => textInputRef.current?.focus(), 100);
};
const onTextInputChange = (_event: React.FormEvent<HTMLInputElement>, valueOfInput: string) => {
setInputValue(valueOfInput);
};
const onSelect = (menuItem?: MenuItemStatus) => {
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.enabled).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, enabled: 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

0 comments on commit 0d5475c

Please sign in to comment.