From ffa5e023bb3a6b77d501c3abcb42cc6f937a1cc8 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 22 Jul 2022 13:51:06 +0200 Subject: [PATCH] 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 --- .../stylesheets/components/multi-input.scss | 29 ++++++++- .../crayons/MultiInput/MultiInput.jsx | 51 +++++++++++---- .../__stories__/MultiInput.stories.jsx | 19 +++++- .../components/defaultSelectionTemplate.jsx | 63 +++++++++++++------ 4 files changed, 124 insertions(+), 38 deletions(-) diff --git a/app/assets/stylesheets/components/multi-input.scss b/app/assets/stylesheets/components/multi-input.scss index de8b5b79f..16945f59b 100644 --- a/app/assets/stylesheets/components/multi-input.scss +++ b/app/assets/stylesheets/components/multi-input.scss @@ -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); + } + } } diff --git a/app/javascript/crayons/MultiInput/MultiInput.jsx b/app/javascript/crayons/MultiInput/MultiInput.jsx index 368b9f036..47c196370 100644 --- a/app/javascript/crayons/MultiInput/MultiInput.jsx +++ b/app/javascript/crayons/MultiInput/MultiInput.jsx @@ -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 }} > 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)} /> ); @@ -200,6 +215,12 @@ export const MultiInput = ({ aria-hidden="true" className="absolute pointer-events-none opacity-0 p-2" /> + {/* A visually hidden list provides confirmation messages to screen reader users as an item is selected or removed */}
@@ -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, }; diff --git a/app/javascript/crayons/MultiInput/__stories__/MultiInput.stories.jsx b/app/javascript/crayons/MultiInput/__stories__/MultiInput.stories.jsx index 58c382be7..785cfa6d8 100644 --- a/app/javascript/crayons/MultiInput/__stories__/MultiInput.stories.jsx +++ b/app/javascript/crayons/MultiInput/__stories__/MultiInput.stories.jsx @@ -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 = { diff --git a/app/javascript/shared/components/defaultSelectionTemplate.jsx b/app/javascript/shared/components/defaultSelectionTemplate.jsx index 289a73175..779be1c27 100644 --- a/app/javascript/shared/components/defaultSelectionTemplate.jsx +++ b/app/javascript/shared/components/defaultSelectionTemplate.jsx @@ -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, -}) => ( -
- - -
-); +}) => { + const conditionalAttributes = () => { + if (enableValidation) { + return { 'aria-describedby': `invalid-item-${name}` }; + } + return {}; + }; + + return ( + <> + {enableValidation && ( +
+ {!valid ? 'Invalid entry' : ''} +
+ )} +
+ + +
+ + ); +}; DefaultSelectionTemplate.propTypes = { name: PropTypes.string.isRequired,