Skip to content

Commit

Permalink
ISPN-15284 Refactor Select: FeaturesSelector
Browse files Browse the repository at this point in the history
  • Loading branch information
karesti committed Oct 31, 2023
1 parent b57f8f5 commit 8c6d8a8
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 26 deletions.
41 changes: 16 additions & 25 deletions src/app/Caches/Create/FeaturesSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Alert, Form, FormAlert, FormGroup, FormSection } from '@patternfly/react-core';
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core/deprecated';
import { Alert, Form, FormAlert, FormGroup, FormSection, SelectOptionProps } from '@patternfly/react-core';
import { CacheFeature, CacheMode } from '@services/infinispanRefData';
import { useTranslation } from 'react-i18next';
import { ConsoleServices } from '@services/ConsoleServices';
Expand All @@ -15,6 +14,7 @@ import { useConnectedUser } from '@app/services/userManagementHook';
import { validFeatures } from '@app/utils/featuresValidation';
import { useFetchProtobufTypes } from '@app/services/protobufHook';
import { ConsoleACL } from '@services/securityService';
import { SelectMultiWithChips } from '@app/Common/SelectMultiWithChips';

const FeaturesSelector = () => {
const { t } = useTranslation();
Expand All @@ -26,7 +26,6 @@ const FeaturesSelector = () => {

const [loadingBackups, setLoadingBackups] = useState(true);
const [isBackups, setIsBackups] = useState(false);
const [isOpenCacheFeature, setIsOpenCacheFeature] = useState(false);

useEffect(() => {
if (loadingBackups) {
Expand All @@ -42,13 +41,12 @@ const FeaturesSelector = () => {
}
}, [loadingBackups]);

const onSelectFeature = (event, selection) => {
const onSelectFeature = (selection) => {
if (configuration.feature.cacheFeatureSelected.includes(selection)) {
removeFeature(selection);
} else {
addFeature(selection);
}
setIsOpenCacheFeature(false);
};

const onClearFeatureSelection = () => {
Expand All @@ -61,11 +59,6 @@ const FeaturesSelector = () => {
}
};
});
setIsOpenCacheFeature(false);
};

const cacheFeatureOptions = () => {
return Object.keys(CacheFeature).map((key) => <SelectOption id={key} key={key} value={CacheFeature[key]} />);
};

const displayAlert = () => {
Expand All @@ -89,6 +82,12 @@ const FeaturesSelector = () => {
return !notSecured && ConsoleServices.security().hasConsoleACL(ConsoleACL.ADMIN, connectedUser);
};

const featuresOptions = () : SelectOptionProps[] => {
const selectOptions: SelectOptionProps[] = [];
Object.keys(CacheFeature).forEach((key) => selectOptions.push({value: CacheFeature[key], children: CacheFeature[key]}));
return selectOptions;
}

return (
<Form
isWidthLimited
Expand All @@ -98,21 +97,13 @@ const FeaturesSelector = () => {
>
<FormSection title={t('caches.create.configurations.feature.cache-feature-list', { brandname: brandname })}>
<FormGroup fieldId="cache-feature">
<Select
variant={SelectVariant.typeaheadMulti}
typeAheadAriaLabel={t('caches.create.configurations.feature.cache-feature-list-typeahead')}
onToggle={() => setIsOpenCacheFeature(!isOpenCacheFeature)}
onSelect={onSelectFeature}
onClear={onClearFeatureSelection}
selections={configuration.feature.cacheFeatureSelected}
isOpen={isOpenCacheFeature}
aria-labelledby="cache-feature"
placeholderText={t('caches.create.configurations.feature.cache-feature-list-placeholder')}
toggleId="featuresSelect"
chipGroupProps={{ numChips: 6 }}
>
{cacheFeatureOptions()}
</Select>
<SelectMultiWithChips id="featuresSelect"
placeholder={t('caches.create.configurations.feature.cache-feature-list-placeholder')}
options={featuresOptions()}
onSelect={onSelectFeature}
onClear={onClearFeatureSelection}
selection={configuration.feature.cacheFeatureSelected}
/>
</FormGroup>
</FormSection>
{displayAlert()}
Expand Down
214 changes: 214 additions & 0 deletions src/app/Common/SelectMultiWithChips.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import React, { useEffect, useState } from 'react';
import {
Button,
Chip,
ChipGroup,
MenuToggle,
MenuToggleElement,
Select,
SelectList,
SelectOption,
SelectOptionProps,
TextInputGroup,
TextInputGroupMain,
TextInputGroupUtilities
} from '@patternfly/react-core';
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';

const SelectMultiWithChips = (props: {id: string, placeholder:string,
options: SelectOptionProps[],
onSelect: (selection) => void,
onClear: () => void,
selection: string[]
}
) => {
const [isOpen, setIsOpen] = useState(false);
const [inputValue, setInputValue] = useState<string>('');
const [selected, setSelected] = useState<string[]>(props.selection);
const [selectOptions, setSelectOptions] = useState<SelectOptionProps[]>(props.options);
const [focusedItemIndex, setFocusedItemIndex] = useState<number | null>(null);
const [activeItem, setActiveItem] = useState<string | null>(null);
const textInputRef = React.useRef<HTMLInputElement>();

useEffect(() => {
setSelected(props.selection);
}, [props.selection])

useEffect(() => {
let newSelectOptions: SelectOptionProps[] = props.options;

// Filter menu items based on the text input value when one exists
if (inputValue) {
newSelectOptions = props.options.filter((menuItem) =>
String(menuItem.children).toLowerCase().includes(inputValue.toLowerCase())
);

// When no options are found after filtering, display 'No results found'
if (!newSelectOptions.length) {
newSelectOptions = [
{ isDisabled: false, children: `No results found for "${inputValue}"`, value: 'no results' }
];
}

// Open the menu when the input value changes and the new value is not empty
if (!isOpen) {
setIsOpen(true);
}
}

setSelectOptions(newSelectOptions);
setFocusedItemIndex(null);
setActiveItem(null);
}, [inputValue]);

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-multi-typeahead-${focusedItem.value.replace(' ', '-')}`);
}
};

const onInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
const enabledMenuItems = selectOptions.filter((menuItem) => !menuItem.isDisabled);
const [firstMenuItem] = enabledMenuItems;
const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;

switch (event.key) {
// Select the first available option
case 'Enter':
if (!isOpen) {
setIsOpen((prevIsOpen) => !prevIsOpen);
} else if (isOpen && focusedItem.value !== 'no results') {
onSelect(focusedItem.value as string);
}
break;
case 'Tab':
case 'Escape':
setIsOpen(false);
setActiveItem(null);
break;
case 'ArrowUp':
case 'ArrowDown':
event.preventDefault();
handleMenuArrowKeys(event.key);
break;
}
};

const onToggleClick = () => {
setIsOpen(!isOpen);
};

const onTextInputChange = (_event: React.FormEvent<HTMLInputElement>, value: string) => {
setInputValue(value);
};

const onSelect = (value: string) => {
if (value && value !== 'no results') {
setSelected(
selected.includes(value) ? selected.filter((selection) => selection !== value) : [...selected, value]
);
}

textInputRef.current?.focus();
props.onSelect(value);
};

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

return (
<Select
id={props.id + 'multi-typeahead-select'}
isOpen={isOpen}
selected={selected}
onSelect={(ev, selection) => onSelect(selection as string)}
onOpenChange={() => setIsOpen(false)}
toggle={toggle}
>
<SelectList isAriaMultiselectable id={props.id + 'select-multi-typeahead-listbox' }>
{selectOptions.map((option, index) => (
<SelectOption
key={option.value || option.children}
isFocused={focusedItemIndex === index}
className={option.className}
id={`select-multi-typeahead-${option.value.replace(' ', '-')}`}
{...option}
ref={null}
/>
))}
</SelectList>
</Select>
);
};

export { SelectMultiWithChips }
1 change: 0 additions & 1 deletion src/app/assets/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,6 @@
"cache-feature-list": "Add {{brandname}} capabilities",
"cache-feature-list-tooltip": "Add capabilities to your cache.",
"cache-feature-list-placeholder": "Select capabilities",
"cache-feature-list-typeahead": "Select capabilities",
"bounded": "Bounded",
"bounded-tooltip": "To restrict the size of the cache, configure {{brandname}} to evict entries. You can set an eviction threshold based on the maximum amount of memory or based on the total number of entries that a cache can hold.",
"radio-max-size": "Maximum amount of memory",
Expand Down

0 comments on commit 8c6d8a8

Please sign in to comment.