ShareImageModal: update the typeahead selector to pf5

This dropdown needed to be update as part of the overall pf5 upgrade.
Each of the components in the select now need to be declared and
handled. Also, some of the Select and SelectOption props come from the
Menu props since Select inherits Menu.
This commit is contained in:
jkozol 2023-10-09 00:41:50 +02:00 committed by Lucas Garfield
parent a5b1b1f775
commit cf1d477544
3 changed files with 164 additions and 103 deletions

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import {
ActionGroup,
@ -9,14 +9,23 @@ import {
HelperTextItem,
FormHelperText,
Popover,
ValidatedOptions,
} from '@patternfly/react-core';
import {
Select,
SelectOption,
SelectVariant,
} from '@patternfly/react-core/deprecated';
import { ExclamationCircleIcon, HelpIcon } from '@patternfly/react-icons';
SelectList,
ValidatedOptions,
MenuToggle,
MenuToggleElement,
TextInputGroup,
TextInputGroupMain,
TextInputGroupUtilities,
ChipGroup,
Chip,
} from '@patternfly/react-core';
import {
ExclamationCircleIcon,
HelpIcon,
TimesIcon,
} from '@patternfly/react-icons';
import { useNavigate } from 'react-router-dom';
import { AWS_REGIONS } from '../../constants';
@ -55,28 +64,72 @@ const generateRequests = (
type RegionsSelectPropTypes = {
composeId: string;
handleClose: any;
handleToggle: any;
isOpen: boolean;
setIsOpen: any;
};
const RegionsSelect = ({
composeId,
handleClose,
handleToggle,
isOpen,
setIsOpen,
}: RegionsSelectPropTypes) => {
const RegionsSelect = ({ composeId, handleClose }: RegionsSelectPropTypes) => {
const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [selected, setSelected] = useState<string[]>([]);
const titleId = 'Clone this image';
const [validated, setValidated] = useState<ValidatedOptions>(
ValidatedOptions.default
);
const [helperTextInvalid] = useState(
'Select at least one region to share to.'
);
const initialRegions = AWS_REGIONS;
const [inputValue, setInputValue] = useState<string>('');
const [selected, setSelected] = useState<string[]>([]);
const [selectOptions, setSelectOptions] = useState(initialRegions);
// Filter dropdown items when there is a typed input
useEffect(() => {
let newSelectOptions = initialRegions;
if (inputValue) {
newSelectOptions = initialRegions.filter((region) =>
region.value.toLowerCase().includes(inputValue.toLowerCase())
);
// When no options are found after filtering, display 'No results found'
if (!newSelectOptions.length) {
newSelectOptions = [
{
disableRegion: false,
description: `No results found for "${inputValue}"`,
value: 'empty',
},
];
}
// Open the menu when the input value changes and the new value is not empty
if (!isOpen) {
setIsOpen(true);
}
}
setSelectOptions(newSelectOptions);
}, [inputValue]);
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]
);
setValidated(ValidatedOptions.success);
} else {
setValidated(ValidatedOptions.error);
}
};
const [cloneCompose] = useCloneComposeMutation();
const { data: composeStatus, isSuccess } = useGetComposeStatusQuery({
@ -87,33 +140,6 @@ const RegionsSelect = ({
return undefined;
}
const options = AWS_REGIONS;
const handleSelect = (
event: React.MouseEvent<Element, MouseEvent> | React.ChangeEvent<Element>,
selection: string
): void => {
let nextSelected;
if (selected.includes(selection)) {
nextSelected = selected.filter((region) => region !== selection);
setSelected(nextSelected);
setIsOpen(false);
} else {
nextSelected = [...selected, selection];
setSelected(nextSelected);
setIsOpen(false);
}
nextSelected.length === 0
? setValidated(ValidatedOptions.error)
: setValidated(ValidatedOptions.default);
};
const handleClear = () => {
setSelected([]);
setIsOpen(false);
setValidated(ValidatedOptions.error);
};
const handleSubmit = async () => {
setIsSaving(true);
const requests = generateRequests(composeId, composeStatus, selected);
@ -121,9 +147,63 @@ const RegionsSelect = ({
navigate(resolveRelPath(''));
};
const handleToggle = () => {
if (!selected.length) setValidated(ValidatedOptions.error);
setIsOpen(!isOpen);
};
const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
variant="typeahead"
onClick={handleToggle}
innerRef={toggleRef}
isExpanded={isOpen}
isFullWidth
>
<TextInputGroup isPlain>
<TextInputGroupMain
value={inputValue}
onClick={handleToggle}
onChange={onTextInputChange}
placeholder="Select region"
isExpanded={isOpen}
>
<ChipGroup aria-label="Selected regions">
{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([]);
setValidated(ValidatedOptions.error);
}}
aria-label="Clear input value"
>
<TimesIcon aria-hidden />
</Button>
)}
</TextInputGroupUtilities>
</TextInputGroup>
</MenuToggle>
);
return (
<Form>
<span id={titleId} hidden>
<span id="Clone this image" hidden>
Select a region
</span>
<FormGroup
@ -141,40 +221,37 @@ const RegionsSelect = ({
</div>
}
>
<button
type="button"
aria-label="More info for name field"
onClick={(e) => e.preventDefault()}
aria-describedby="simple-form-name-01"
className="pf-c-form__group-label-help"
<Button
variant="plain"
aria-label="About regions"
className="pf-u-pl-sm header-button"
isInline
>
<HelpIcon />
</button>
</Button>
</Popover>
}
>
<Select
variant={SelectVariant.typeaheadMulti}
typeAheadAriaLabel="Select a region"
onToggle={handleToggle}
onSelect={handleSelect}
onClear={handleClear}
selections={selected}
isScrollable
isOpen={isOpen}
aria-labelledby={titleId}
placeholderText="Select a region"
menuAppendTo="parent"
validated={validated}
maxHeight="25rem"
selected={selected}
onSelect={(ev, selection) => onSelect(selection as string)}
onOpenChange={handleToggle}
toggle={toggle}
>
{options.map((option, index) => (
<SelectOption
isDisabled={option.disableRegion}
key={index}
value={option.value}
{...(option.description && { description: option.description })}
/>
))}
<SelectList isAriaMultiselectable>
{selectOptions.map((option) => (
<SelectOption
isDisabled={option.disableRegion}
key={option.value}
description={option.value}
value={option.value}
>
{option.description}
</SelectOption>
))}
</SelectList>
</Select>
{validated !== 'success' && (
<FormHelperText>
@ -183,7 +260,7 @@ const RegionsSelect = ({
icon={<ExclamationCircleIcon />}
variant={validated}
>
{helperTextInvalid}
Select at least one region to share to.
</HelperTextItem>
</HelperText>
</FormHelperText>

View file

@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import { Modal } from '@patternfly/react-core';
import { useNavigate, useParams } from 'react-router-dom';
@ -11,20 +11,9 @@ import { resolveRelPath } from '../../Utilities/path';
const ShareToRegionsModal = () => {
const navigate = useNavigate();
const handleClose = () => navigate(resolveRelPath(''));
const [isOpen, setIsOpen] = useState(false);
const { composeId } = useParams();
const handleToggle = (isOpen: boolean) => setIsOpen(isOpen);
const handleEscapePress = () => {
if (isOpen) {
handleToggle(isOpen);
} else {
handleClose();
}
};
const appendTo = useMemo(() => {
const modalAnchor = document.querySelector(MODAL_ANCHOR);
return modalAnchor === null ? undefined : (modalAnchor as HTMLElement);
@ -44,16 +33,9 @@ const ShareToRegionsModal = () => {
title="Share to new region"
description="Configure new regions for this image that will run on your AWS. All the
regions will launch with the same configuration."
onEscapePress={handleEscapePress}
appendTo={appendTo}
>
<RegionsSelect
composeId={composeId}
handleClose={handleClose}
handleToggle={handleToggle}
isOpen={isOpen}
setIsOpen={setIsOpen}
/>
<RegionsSelect composeId={composeId} handleClose={handleClose} />
</Modal>
);
};

View file

@ -40,18 +40,20 @@ describe('Create Share To Regions Modal', () => {
const shareButton = await screen.findByRole('button', { name: /share/i });
expect(shareButton).toBeDisabled();
const selectToggle = screen.getByRole('button', { name: /options menu/i });
const selectToggle = screen.getByRole('button', { name: /menu toggle/i });
// eslint-disable-next-line testing-library/no-unnecessary-act
user.click(selectToggle);
const usEast2 = await screen.findByRole('option', {
name: /us-east-2 us east \(ohio\)/i,
name: /us east \(ohio\) us\-east\-2/i,
});
expect(usEast2).not.toHaveClass('pf-m-disabled');
user.click(usEast2);
await waitFor(() => expect(shareButton).toBeEnabled());
const clearAllButton = screen.getByRole('button', { name: /clear all/i });
const clearAllButton = screen.getByRole('button', {
name: /clear input value/i,
});
user.click(clearAllButton);
await waitFor(() => expect(shareButton).toBeDisabled());
@ -97,16 +99,16 @@ describe('Create Share To Regions Modal', () => {
renderCustomRoutesWithReduxRouter(`share/${composeId}`, {}, routes);
const selectToggle = await screen.findByRole('button', {
name: /options menu/i,
name: /menu toggle/i,
});
// eslint-disable-next-line testing-library/no-unnecessary-act
user.click(selectToggle);
// parent region disabled
const usEast1 = await screen.findByRole('option', {
name: /us-east-1 us east \(n. virginia\)/i,
name: /us east \(n. virginia\) us-east-1/i,
});
expect(usEast1).toHaveClass('pf-m-disabled');
expect(usEast1).toBeDisabled();
// close the select again to avoid state update
// eslint-disable-next-line testing-library/no-unnecessary-act