Implementing validation for the Multi Input Crayons Component (#18185)

* WIP: add the validation

* feat/WIP: set the color of the button to red

* feat: change color temporarily

* feat: add accessibility for validation

* feat: add the styling changes for the multi input

* feat: add focus visible styling as well

* fix: add comma to scss file to separate attributes

* chore: rename regex to inputRegex

* refactor: move out validation into a function

* chore: rename regex to inputRegex

* feat: add a validation Regex as a prop

* feat: update the text description

* chore: update spelling oops

* feat: remove comment

* remove comments

* rename error

* feat: add showLabel and labelText to the stories component

* cater for when we don't pass in a validationRegex

* some small tweaks

Co-authored-by: Suzanne Aitchison <suzanne@forem.com>
This commit is contained in:
Ridhwana 2022-07-22 13:51:06 +02:00 committed by GitHub
parent 2b23d4f6ed
commit ffa5e023bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 124 additions and 38 deletions

View file

@ -27,7 +27,7 @@
&:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border: solid var(--base-10);
border: 1px solid var(--base-10);
border-right: none;
color: var(--base-90);
}
@ -35,7 +35,7 @@
&:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border: solid var(--base-10);
border: 1px solid var(--base-10);
border-left: none;
}
@ -50,4 +50,29 @@
background: none;
color: var(--accent-danger);
}
&-invalid {
&:first-child,
&:last-child {
background-color: var(--accent-danger-a10);
color: var(--accent-danger);
border: 1px solid var(--accent-danger);
}
&:first-child {
border-right: none;
}
&:last-child {
border-left: none;
}
&:first-child:hover,
&:first-child:focus-visible,
&:last-child:hover,
&:last-child:focus-visible {
background-color: var(--accent-danger-a10);
color: var(--accent-danger);
}
}
}

View file

@ -14,14 +14,20 @@ const KEYS = {
* Component allowing users to add multiple entries for a given input field that get displayed as destructive pills
*
* @param {Object} props
* @param {string} props.labelText The text for the input's label
* @param {boolean} props.showLabel Whether the label text should be visible or hidden (for assistive tech users only)
* @param {string} props.placeholder Input placeholder text
* @param {string} props.regex Optional regular expression used to restrict the input
* @param {string} props.inputRegex Optional regular expression used to restrict the input
* @param {string} props.validationRegex Optional regular expression used to validate the value of the input
* @param {Function} props.SelectionTemplate Optional Preact component to render selected items
*/
export const MultiInput = ({
placeholder,
regex,
inputRegex,
validationRegex,
showLabel = true,
labelText,
SelectionTemplate = DefaultSelectionTemplate,
}) => {
const inputRef = useRef(null);
@ -84,20 +90,22 @@ export const MultiInput = ({
}
break;
default:
if (regex && !regex.test(e.key)) {
if (inputRegex && !inputRegex.test(e.key)) {
e.preventDefault();
}
}
};
const addItemToList = (value) => {
// TODO: we will want to do some validation here based on a prop
if (value.trim().length > 0) {
// If an item was edited, we want to keep it in the same position in the list
const insertIndex = inputPosition !== null ? inputPosition : items.length;
// if we do not pass in a validationRegex we can assume that anything is valid
const valid = validationRegex ? checkValidity(value) : true;
const newSelections = [
...items.slice(0, insertIndex),
value,
{ value, valid },
...items.slice(insertIndex),
];
@ -111,6 +119,10 @@ export const MultiInput = ({
}
};
const checkValidity = (value) => {
return validationRegex.test(value);
};
const clearInput = () => {
inputRef.current.value = '';
};
@ -124,7 +136,7 @@ export const MultiInput = ({
};
const deselectItem = (clickedItem) => {
const newArr = items.filter((item) => item !== clickedItem);
const newArr = items.filter((item) => item.value !== clickedItem);
setItems(newArr);
// We also update the hidden selected items list, so removals are announced to screen reader users
@ -142,8 +154,7 @@ export const MultiInput = ({
inputPosition !== null ? inputPosition - 1 : items.length - 1;
const item = items[nextEditIndex];
deselectItem(item);
enterEditState(item, nextEditIndex);
enterEditState(item.value, nextEditIndex);
}
};
@ -184,10 +195,14 @@ export const MultiInput = ({
style={{ order: position }}
>
<SelectionTemplate
name={item}
className="c-input--multi__selected"
onEdit={() => enterEditState(item, index)}
onDeselect={() => deselectItem(item)}
name={item.value}
className={`c-input--multi__selected ${
!item.valid ? 'c-input--multi__selected-invalid' : ''
}`}
enableValidation={true}
valid={item.valid}
onEdit={() => enterEditState(item.value, index)}
onDeselect={() => deselectItem(item.value)}
/>
</li>
);
@ -200,6 +215,12 @@ export const MultiInput = ({
aria-hidden="true"
className="absolute pointer-events-none opacity-0 p-2"
/>
<label
id="multi-select-label"
className={showLabel ? '' : 'screen-reader-only'}
>
{labelText}
</label>
{/* A visually hidden list provides confirmation messages to screen reader users as an item is selected or removed */}
<div className="screen-reader-only">
@ -228,6 +249,7 @@ export const MultiInput = ({
autocomplete="off"
class="c-input--multi__input"
type="text"
aria-labelledby="multi-select-label"
onBlur={handleInputBlur}
onKeyDown={handleKeyDown}
placeholder={inputPosition === null ? placeholder : null}
@ -243,7 +265,10 @@ export const MultiInput = ({
};
MultiInput.propTypes = {
labelText: PropTypes.string.isRequired,
showLabel: PropTypes.bool,
placeholder: PropTypes.string,
regex: PropTypes.string,
inputRegex: PropTypes.string,
validationRegex: PropTypes.string,
SelectionTemplate: PropTypes.func,
};

View file

@ -14,8 +14,18 @@ export default {
description:
'Placeholder text, shown when no selections have been made yet',
},
regex: {
description: 'A regular expression used to validate the input',
inputRegex: {
description: 'A regular expression used to restrict the input',
},
validationRegex: {
description: 'A regular expression used to validate the value of input',
},
labelText: {
description: 'The label for the input',
},
showLabel: {
description:
'Should the label text be visible (it will always be available to assistive technology regardless)',
},
},
},
@ -27,7 +37,10 @@ export const Default = (args) => {
Default.args = {
placeholder: 'Add an email address...',
regex: /([a-zA-Z0-9@.])/,
inputRegex: /([a-zA-Z0-9@_.+-])/,
validationRegex: /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/,
labelText: 'Example multi input',
showLabel: true,
};
Default.story = {

View file

@ -14,30 +14,53 @@ import { Close } from '@images/x.svg';
*/
export const DefaultSelectionTemplate = ({
name,
enableValidation = false,
valid = true,
buttonVariant = 'default',
className = 'c-autocomplete--multi__selected',
onEdit,
onDeselect,
}) => (
<div role="group" aria-label={name} className="flex mr-1 mb-1 w-max">
<Button
variant={buttonVariant}
className={`${className} p-1 cursor-text`}
aria-label={`Edit ${name}`}
onClick={onEdit}
>
{name}
</Button>
<Button
variant={buttonVariant}
className={`${className} p-1`}
aria-label={`Remove ${name}`}
onClick={onDeselect}
>
<Icon src={Close} />
</Button>
</div>
);
}) => {
const conditionalAttributes = () => {
if (enableValidation) {
return { 'aria-describedby': `invalid-item-${name}` };
}
return {};
};
return (
<>
{enableValidation && (
<div
id={`invalid-item-${name}`}
className="screen-reader-only"
aria-live="assertive"
>
{!valid ? 'Invalid entry' : ''}
</div>
)}
<div role="group" aria-label={name} className="flex mr-1 mb-1 w-max">
<Button
variant={buttonVariant}
className={`${className} p-1 cursor-text`}
aria-label={`Edit ${name}`}
{...conditionalAttributes()}
onClick={onEdit}
>
{name}
</Button>
<Button
variant={buttonVariant}
className={`${className} p-1`}
aria-label={`Remove ${name}`}
onClick={onDeselect}
>
<Icon src={Close} />
</Button>
</div>
</>
);
};
DefaultSelectionTemplate.propTypes = {
name: PropTypes.string.isRequired,