From 2cc30d292d987b24a643a486b5d765077a191c08 Mon Sep 17 00:00:00 2001 From: Cindy Nguyen Date: Wed, 20 Sep 2023 15:50:58 -0400 Subject: [PATCH] feat: autosuggest error handling Added logic to set error handling when clicking or tabing outside of the component. Created logic to allow consumer to use freeform functionality. Created option values and seperate display versus data value logic. --- src/Form/FormAutosuggest.jsx | 113 ++++++++++++++++++++++++---------- src/Form/form-autosuggest.mdx | 26 +++++--- 2 files changed, 97 insertions(+), 42 deletions(-) diff --git a/src/Form/FormAutosuggest.jsx b/src/Form/FormAutosuggest.jsx index 5fb689b2e84..a2f4b3d0fdc 100644 --- a/src/Form/FormAutosuggest.jsx +++ b/src/Form/FormAutosuggest.jsx @@ -2,6 +2,7 @@ import React, { useEffect, useState, } from 'react'; import PropTypes from 'prop-types'; +import { v4 as uuidv4 } from 'uuid'; import { useIntl } from 'react-intl'; import { KeyboardArrowUp, KeyboardArrowDown } from '../../icons'; import Icon from '../Icon'; @@ -13,17 +14,21 @@ import Spinner from '../Spinner'; import useArrowKeyNavigation from '../hooks/useArrowKeyNavigation'; import messages from './messages'; +//this is the function that is being called in the mdx file function FormAutosuggest({ children, arrowKeyNavigationSelector, ignoredArrowKeysNames, screenReaderText, value, + freeformValue, isLoading, - errorMessageText, + errorMessageText, //create more, more specific error no selected value + errorNoMatchingText, // error no matching value onChange, onSelected, helpMessage, + allowFreeFormInput, ...props }) { const intl = useIntl(); @@ -34,40 +39,46 @@ function FormAutosuggest({ const [isMenuClosed, setIsMenuClosed] = useState(true); const [isActive, setIsActive] = useState(false); const [state, setState] = useState({ - displayValue: value || '', + displayValue: freeformValue || '', errorMessage: '', dropDownItems: [], }); + console.log("VALUE:", value) const handleItemClick = (e, onClick) => { - const clickedValue = e.currentTarget.getAttribute('data-value'); - - if (onSelected && clickedValue !== value) { - onSelected(clickedValue); + const clickedDataValue = e.currentTarget.getAttribute('data-value'); + const clickedDisplayValue = e.currentTarget.innerText + + if (onSelected && clickedDataValue !== value) { + onSelected({dataValue: clickedDataValue, displayValue: clickedDisplayValue, }); + // call on selected and passes the object as an argument + // doc site example, selected will now be a data object + //then passes that in and passes that in as the value of the autosuggest component + // component believes its jsut eh display value, so whereever its value it should be displayvalue } setState(prevState => ({ ...prevState, dropDownItems: [], - displayValue: clickedValue, + displayValue: clickedDisplayValue, })); setIsMenuClosed(true); if (onClick) { onClick(e); - } - }; + } + }; function getItems(strToFind = '') { let childrenOpt = React.Children.map(children, (child) => { // eslint-disable-next-line no-shadow - const { children, onClick, ...rest } = child.props; + const { children, onClick, value, ...rest } = child.props; return React.cloneElement(child, { ...rest, children, - 'data-value': children, + 'data-value': value ?? children, onClick: (e) => handleItemClick(e, onClick), }); }); @@ -103,6 +114,7 @@ function FormAutosuggest({ handleExpand(e, isMenuClosed)} /> ); - - const handleDocumentClick = (e) => { - if (parentRef.current && !parentRef.current.contains(e.target) && isActive) { + const leaveControl = () => { setIsActive(false); + + let errorMessage = "" + + if (!state.displayValue){ + console.log("Nothing typed:", errorMessageText) + errorMessage = errorMessageText + }else if(!allowFreeFormInput){ //not allowing for freeform, meaning must exactly match + console.log("Freeform not allowed:", errorNoMatchingText) + const dropDownItems = getItems(state.displayValue) + let inputMatchesDropDown = false + React.Children.forEach(children, (child) => { + console.log("item", child.props.children) + console.log(child.props) + if (state.displayValue === child.props.children){ + onSelected({displayValue: state.displayValue, dataValue: child.props.value}) // freeform input is not allowed, HAS to match option values + + inputMatchesDropDown = true + } + }) + if (!inputMatchesDropDown){ + onSelected({displayValue: "" , dataValue: ""}) //setting components parent value back to default + errorMessage = errorNoMatchingText + console.log("Freeform not allowed and doesn't match:", state.displayValue, dropDownItems, errorMessage) + } + }else{ + onSelected({displayValue: state.displayValue, dataValue: state.displayValue}) // freeform IS allowed, therefore we are assigning the parent component value to be what the user types if there's no matching option + } setState(prevState => ({ ...prevState, dropDownItems: [], - errorMessage: !state.displayValue ? errorMessageText : '', + errorMessage: errorMessage, })); - + setIsMenuClosed(true); + } + + const handleDocumentClick = (e) => { + if (parentRef.current && !parentRef.current.contains(e.target) && isActive) { + leaveControl(); } }; @@ -139,6 +181,9 @@ function FormAutosuggest({ setIsMenuClosed(true); } + if (e.key === 'Tab' && isActive){ + leaveControl(); + } }; useEffect(() => { @@ -151,28 +196,29 @@ function FormAutosuggest({ }; }); - useEffect(() => { - if (value || value === '') { - setState(prevState => ({ - ...prevState, - displayValue: value, - })); - } - }, [value]); + // Not clear what this was used for + // useEffect(() => { + // if (value || value === '') { + // setState(prevState => ({ + // ...prevState, + // displayValue: freeformValue, + // })); + // } + // }, [value]); - const setDisplayValue = (itemValue) => { - const optValue = []; + const setDisplayValue = (typedValue) => { + const optValues = []; children.forEach(opt => { - optValue.push(opt.props.children); + optValues.push({dataValue: opt.props.value, displayValue: opt.props.children, }); }); - const normalized = itemValue.toLowerCase(); - const opt = optValue.find((o) => o.toLowerCase() === normalized); + const normalized = typedValue.toLowerCase(); + const opt = optValues.find((o) => o.displayValue.toLowerCase() === normalized); setState(prevState => ({ ...prevState, - displayValue: opt || itemValue, + displayValue: opt ? opt.displayValue : typedValue, })); }; @@ -193,9 +239,9 @@ function FormAutosuggest({ const handleOnChange = (e) => { const findStr = e.target.value; - + if (onChange) { onChange(findStr); } - + if (findStr.length) { const filteredItems = getItems(findStr); setState(prevState => ({ @@ -279,7 +325,8 @@ FormAutosuggest.defaultProps = { helpMessage: '', placeholder: '', value: null, - errorMessageText: null, + errorMessageText: "Error, no selected value", + errorNoMatchingText: "Error, no matching value", readOnly: false, children: null, name: 'form-autosuggest', diff --git a/src/Form/form-autosuggest.mdx b/src/Form/form-autosuggest.mdx index 9da8579ba20..73360e91449 100644 --- a/src/Form/form-autosuggest.mdx +++ b/src/Form/form-autosuggest.mdx @@ -19,7 +19,15 @@ Form auto-suggest enables users to manually select or type to find matching opti ```jsx live () => { - const [selected, setSelected] = useState(''); + const [selectedDisplay, setSelectedDisplay] = useState(''); + const [selectedData, setSelectedData] = useState(''); + const handleSelected = (selectedObject) => { + setSelectedDisplay(selectedObject.displayValue) + setSelectedData(selectedObject.dataValue) + } + + console.log("SELECTEDDISPLAY:", selectedDisplay) + console.log("SELECTEDDATA:", selectedData) return ( setSelected(value)} + errorNoMatchingText="Error, no matching value" + value={selectedData} + freeformValue={selectedDisplay} + onSelected={(value) => handleSelected(value)} + allowFreeFormInput={true} > - JavaScript - Python - Rube - alert(e.currentTarget.getAttribute('data-value'))}> - Option with custom onClick - + JavaScript + Python + Ruby ); }