Skip to content

Commit

Permalink
Wizard: Extract ChippingInput to reusable component
Browse files Browse the repository at this point in the history
This extracts input with chips from the NTP servers into a separate reusable ChippingInput component.
  • Loading branch information
regexowl authored and lucasgarfield committed Dec 16, 2024
1 parent 8e046e3 commit ae795c3
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 87 deletions.
118 changes: 118 additions & 0 deletions src/Components/CreateImageWizard/ChippingInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React, { useState } from 'react';

import {
Button,
Chip,
ChipGroup,
HelperText,
HelperTextItem,
TextInputGroup,
TextInputGroupMain,
TextInputGroupUtilities,
} from '@patternfly/react-core/dist/esm';
import { PlusCircleIcon, TimesIcon } from '@patternfly/react-icons';
import { UnknownAction } from 'redux';

import { useAppDispatch } from '../../store/hooks';

type ChippingInputProps = {
ariaLabel: string;
placeholder: string;
validator: (value: string) => boolean;
list: string[] | undefined;
item: string;
addAction: (value: string) => UnknownAction;
removeAction: (value: string) => UnknownAction;
};

const ChippingInput = ({
ariaLabel,
placeholder,
validator,
list,
item,
addAction,
removeAction,
}: ChippingInputProps) => {
const dispatch = useAppDispatch();

const [inputValue, setInputValue] = useState('');
const [errorText, setErrorText] = useState('');

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

const handleKeyDown = (e: React.KeyboardEvent, value: string) => {
if (e.key === ' ' || e.key === 'Enter' || e.key === ',') {
e.preventDefault();

if (validator(value) && !list?.includes(value)) {
dispatch(addAction(value));
setInputValue('');
setErrorText('');
}

if (list?.includes(value)) {
setErrorText(`${item} already exists.`);
}

if (!validator(value)) {
setErrorText('Invalid format.');
}
}
};

const handleAddItem = (e: React.MouseEvent, value: string) => {
dispatch(addAction(value));
setInputValue('');
};

return (
<>
<TextInputGroup>
<TextInputGroupMain
placeholder={placeholder}
onChange={onTextInputChange}
value={inputValue}
onKeyDown={(e) => handleKeyDown(e, inputValue)}
/>
<TextInputGroupUtilities>
<Button
variant="plain"
onClick={(e) => handleAddItem(e, inputValue)}
isDisabled={!inputValue}
aria-label={ariaLabel}
>
<PlusCircleIcon className="pf-v5-u-primary-color-100" />
</Button>
<Button
variant="plain"
onClick={() => setInputValue('')}
isDisabled={!inputValue}
aria-label="Clear input"
>
<TimesIcon />
</Button>
</TextInputGroupUtilities>
</TextInputGroup>
{errorText && (
<HelperText>
<HelperTextItem variant={'error'}>{errorText}</HelperTextItem>
</HelperText>
)}
<ChipGroup numChips={5} className="pf-v5-u-mt-sm pf-v5-u-w-100">
{list?.map((item) => (
<Chip key={item} onClick={() => dispatch(removeAction(item))}>
{item}
</Chip>
))}
</ChipGroup>
</>
);
};

export default ChippingInput;
Original file line number Diff line number Diff line change
@@ -1,104 +1,30 @@
import React, { useState } from 'react';
import React from 'react';

import {
Button,
Chip,
ChipGroup,
FormGroup,
HelperText,
HelperTextItem,
TextInputGroup,
TextInputGroupMain,
TextInputGroupUtilities,
} from '@patternfly/react-core';
import { PlusCircleIcon, TimesIcon } from '@patternfly/react-icons';
import { FormGroup } from '@patternfly/react-core';

import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import { useAppSelector } from '../../../../../store/hooks';
import {
addNtpServer,
removeNtpServer,
selectNtpServers,
} from '../../../../../store/wizardSlice';
import ChippingInput from '../../../ChippingInput';
import { isNtpServerValid } from '../../../validators';

const NtpServersInput = () => {
const dispatch = useAppDispatch();
const ntpServers = useAppSelector(selectNtpServers);
const [inputValue, setInputValue] = useState('');
const [errorText, setErrorText] = useState('');

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

const handleKeyDown = (e: React.KeyboardEvent, value: string) => {
if (e.key === ' ' || e.key === 'Enter' || e.key === ',') {
e.preventDefault();

if (isNtpServerValid(value) && !ntpServers?.includes(value)) {
dispatch(addNtpServer(value));
setInputValue('');
setErrorText('');
}

if (ntpServers?.includes(value)) {
setErrorText('NTP server already exists.');
}

if (!isNtpServerValid(value)) {
setErrorText('Invalid format.');
}
}
};

const handleAddServer = (e: React.MouseEvent, value: string) => {
dispatch(addNtpServer(value));
setInputValue('');
};

return (
<FormGroup isRequired={false} label="NTP servers">
<TextInputGroup>
<TextInputGroupMain
placeholder="Add NTP servers"
onChange={onTextInputChange}
value={inputValue}
onKeyDown={(e) => handleKeyDown(e, inputValue)}
/>
<TextInputGroupUtilities>
<Button
variant="plain"
onClick={(e) => handleAddServer(e, inputValue)}
isDisabled={!inputValue}
aria-label="Add NTP server"
>
<PlusCircleIcon />
</Button>
<Button
variant="plain"
onClick={() => setInputValue('')}
isDisabled={!inputValue}
aria-label="Clear input"
>
<TimesIcon />
</Button>
</TextInputGroupUtilities>
</TextInputGroup>
{errorText && (
<HelperText>
<HelperTextItem variant={'error'}>{errorText}</HelperTextItem>
</HelperText>
)}
<ChipGroup numChips={5} className="pf-v5-u-mt-sm pf-v5-u-w-100">
{ntpServers?.map((server) => (
<Chip key={server} onClick={() => dispatch(removeNtpServer(server))}>
{server}
</Chip>
))}
</ChipGroup>
<ChippingInput
ariaLabel="Add NTP server"
placeholder="Add NTP servers"
validator={isNtpServerValid}
list={ntpServers}
item="NTP server"
addAction={addNtpServer}
removeAction={removeNtpServer}
/>
</FormGroup>
);
};
Expand Down

0 comments on commit ae795c3

Please sign in to comment.