diff --git a/src/Form/FormAutosuggest.jsx b/src/Form/FormAutosuggest.jsx index ba4c0a975ce..2e82403d944 100644 --- a/src/Form/FormAutosuggest.jsx +++ b/src/Form/FormAutosuggest.jsx @@ -1,6 +1,4 @@ -import React, { - useEffect, useState, -} from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; import { KeyboardArrowUp, KeyboardArrowDown } from '../../icons'; @@ -37,6 +35,11 @@ function FormAutosuggest({ errorMessage: '', dropDownItems: [], }); + const [activeMenuItemId, setActiveMenuItemId] = useState(null); + + const handleMenuItemFocus = (menuItemId) => { + setActiveMenuItemId(menuItemId); + }; const handleItemClick = (e, onClick) => { const clickedValue = e.currentTarget.getAttribute('data-value'); @@ -45,7 +48,7 @@ function FormAutosuggest({ onSelected(clickedValue); } - setState(prevState => ({ + setState((prevState) => ({ ...prevState, dropDownItems: [], displayValue: clickedValue, @@ -59,21 +62,26 @@ function FormAutosuggest({ }; function getItems(strToFind = '') { - let childrenOpt = React.Children.map(children, (child) => { + let childrenOpt = React.Children.map(children, (child, index) => { // eslint-disable-next-line no-shadow const { children, onClick, ...rest } = child.props; + // Generate a unique ID for each menu item + const menuItemId = `pgn__form-autosuggest__menuItem-${index}`; return React.cloneElement(child, { ...rest, children, 'data-value': children, onClick: (e) => handleItemClick(e, onClick), + // set ID for the option + id: menuItemId, + // set onFocus behavior + onFocus: () => handleMenuItemFocus(menuItemId), }); }); if (strToFind.length > 0) { - childrenOpt = childrenOpt - .filter((opt) => (opt.props.children.toLowerCase().includes(strToFind.toLowerCase()))); + childrenOpt = childrenOpt.filter((opt) => opt.props.children.toLowerCase().includes(strToFind.toLowerCase())); } return childrenOpt; @@ -91,7 +99,7 @@ function FormAutosuggest({ newState.errorMessage = ''; } - setState(prevState => ({ + setState((prevState) => ({ ...prevState, ...newState, })); @@ -104,16 +112,22 @@ function FormAutosuggest({ iconAs={Icon} size="sm" variant="secondary" - alt={isMenuClosed - ? intl.formatMessage(messages.iconButtonOpened) - : intl.formatMessage(messages.iconButtonClosed)} + alt={ + isMenuClosed + ? intl.formatMessage(messages.iconButtonOpened) + : intl.formatMessage(messages.iconButtonClosed) + } onClick={(e) => handleExpand(e, isMenuClosed)} /> ); const handleClickOutside = (e) => { - if (parentRef.current && !parentRef.current.contains(e.target) && state.dropDownItems.length > 0) { - setState(prevState => ({ + if ( + parentRef.current + && !parentRef.current.contains(e.target) + && state.dropDownItems.length > 0 + ) { + setState((prevState) => ({ ...prevState, dropDownItems: [], errorMessage: !state.displayValue ? errorMessageText : '', @@ -123,11 +137,11 @@ function FormAutosuggest({ } }; - const keyDownHandler = e => { + const keyDownHandler = (e) => { if (e.key === 'Escape') { e.preventDefault(); - setState(prevState => ({ + setState((prevState) => ({ ...prevState, dropDownItems: [], errorMessage: !state.displayValue ? errorMessageText : '', @@ -149,7 +163,7 @@ function FormAutosuggest({ useEffect(() => { if (value || value === '') { - setState(prevState => ({ + setState((prevState) => ({ ...prevState, displayValue: value, })); @@ -159,14 +173,14 @@ function FormAutosuggest({ const setDisplayValue = (itemValue) => { const optValue = []; - children.forEach(opt => { + children.forEach((opt) => { optValue.push(opt.props.children); }); const normalized = itemValue.toLowerCase(); const opt = optValue.find((o) => o.toLowerCase() === normalized); - setState(prevState => ({ + setState((prevState) => ({ ...prevState, displayValue: opt || itemValue, })); @@ -176,7 +190,7 @@ function FormAutosuggest({ const dropDownItems = getItems(e.target.value); if (dropDownItems.length > 1) { - setState(prevState => ({ + setState((prevState) => ({ ...prevState, dropDownItems, errorMessage: '', @@ -189,11 +203,13 @@ function FormAutosuggest({ const handleOnChange = (e) => { const findStr = e.target.value; - if (onChange) { onChange(findStr); } + if (onChange) { + onChange(findStr); + } if (findStr.length) { const filteredItems = getItems(findStr); - setState(prevState => ({ + setState((prevState) => ({ ...prevState, dropDownItems: filteredItems, errorMessage: '', @@ -201,7 +217,7 @@ function FormAutosuggest({ setIsMenuClosed(false); } else { - setState(prevState => ({ + setState((prevState) => ({ ...prevState, dropDownItems: [], errorMessageText, @@ -215,6 +231,9 @@ function FormAutosuggest({ return (
+
+ {`${state.dropDownItems.length} options found`} +
0).toString()} @@ -224,6 +243,7 @@ function FormAutosuggest({ autoComplete="off" value={state.displayValue} aria-invalid={state.errorMessage} + aria-activedescendant={activeMenuItemId} onChange={handleOnChange} onClick={handleClick} trailingElement={iconToggle} @@ -250,16 +270,23 @@ function FormAutosuggest({ > {isLoading ? (
- +
- ) : state.dropDownItems.length > 0 && state.dropDownItems} + ) : ( + state.dropDownItems.length > 0 && state.dropDownItems + )}
); } FormAutosuggest.defaultProps = { - arrowKeyNavigationSelector: 'a:not(:disabled),li:not(:disabled, .btn-icon),input:not(:disabled)', + arrowKeyNavigationSelector: + 'a:not(:disabled),li:not(:disabled, .btn-icon),input:not(:disabled)', ignoredArrowKeysNames: ['ArrowRight', 'ArrowLeft'], isLoading: false, className: null, @@ -280,7 +307,7 @@ FormAutosuggest.propTypes = { /** * Specifies the CSS selector string that indicates to which elements * the user can navigate using the arrow keys - */ + */ arrowKeyNavigationSelector: PropTypes.string, /** Specifies ignored hook keys. */ ignoredArrowKeysNames: PropTypes.arrayOf(PropTypes.string),