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:
parent
2b23d4f6ed
commit
ffa5e023bb
4 changed files with 124 additions and 38 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue