Skip to content

Commit

Permalink
feat: add attributes to input and wrapper component
Browse files Browse the repository at this point in the history
add aria-activedescendent attribute to input and aria-live=”assertive” to wrapper component
  • Loading branch information
httpsmenahassan committed Aug 22, 2023
1 parent 79f0cb4 commit 3d11100
Showing 1 changed file with 87 additions and 58 deletions.
145 changes: 87 additions & 58 deletions src/Form/FormAutosuggest.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import React, {
useEffect, useState,
} from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { KeyboardArrowUp, KeyboardArrowDown } from '../../icons';
import Icon from '../Icon';
import FormGroup from './FormGroup';
import FormControl from './FormControl';
import FormControlFeedback from './FormControlFeedback';
import IconButton from '../IconButton';
import Spinner from '../Spinner';
import useArrowKeyNavigation from '../hooks/useArrowKeyNavigation';
import messages from './messages';
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { useIntl } from "react-intl";
import { KeyboardArrowUp, KeyboardArrowDown } from "../../icons";
import Icon from "../Icon";
import FormGroup from "./FormGroup";
import FormControl from "./FormControl";
import FormControlFeedback from "./FormControlFeedback";
import IconButton from "../IconButton";
import Spinner from "../Spinner";
import useArrowKeyNavigation from "../hooks/useArrowKeyNavigation";
import messages from "./messages";

function FormAutosuggest({
children,
Expand All @@ -33,19 +31,24 @@ function FormAutosuggest({
});
const [isMenuClosed, setIsMenuClosed] = useState(true);
const [state, setState] = useState({
displayValue: value || '',
errorMessage: '',
displayValue: value || "",
errorMessage: "",
dropDownItems: [],
});
const [activeMenuItemId, setActiveMenuItemId] = useState(null);

const handleMenuItemFocus = (menuItemId) => {
setActiveMenuItemId(menuItemId);
};

const handleItemClick = (e, onClick) => {
const clickedValue = e.currentTarget.getAttribute('data-value');
const clickedValue = e.currentTarget.getAttribute("data-value");

if (onSelected && clickedValue !== value) {
onSelected(clickedValue);
}

setState(prevState => ({
setState((prevState) => ({
...prevState,
dropDownItems: [],
displayValue: clickedValue,
Expand All @@ -58,22 +61,29 @@ function FormAutosuggest({
}
};

function getItems(strToFind = '') {
let childrenOpt = React.Children.map(children, (child) => {
function getItems(strToFind = "") {
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,
"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;
Expand All @@ -88,10 +98,10 @@ function FormAutosuggest({

if (isMenuClosed) {
newState.dropDownItems = getItems(state.displayValue);
newState.errorMessage = '';
newState.errorMessage = "";
}

setState(prevState => ({
setState((prevState) => ({
...prevState,
...newState,
}));
Expand All @@ -104,52 +114,58 @@ 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 : '',
errorMessage: !state.displayValue ? errorMessageText : "",
}));

setIsMenuClosed(true);
}
};

const keyDownHandler = e => {
if (e.key === 'Escape') {
const keyDownHandler = (e) => {
if (e.key === "Escape") {
e.preventDefault();

setState(prevState => ({
setState((prevState) => ({
...prevState,
dropDownItems: [],
errorMessage: !state.displayValue ? errorMessageText : '',
errorMessage: !state.displayValue ? errorMessageText : "",
}));

setIsMenuClosed(true);
}
};

useEffect(() => {
document.addEventListener('keydown', keyDownHandler);
document.addEventListener('click', handleClickOutside, true);
document.addEventListener("keydown", keyDownHandler);
document.addEventListener("click", handleClickOutside, true);

return () => {
document.removeEventListener('click', handleClickOutside, true);
document.removeEventListener('keydown', keyDownHandler);
document.removeEventListener("click", handleClickOutside, true);
document.removeEventListener("keydown", keyDownHandler);
};
});

useEffect(() => {
if (value || value === '') {
setState(prevState => ({
if (value || value === "") {
setState((prevState) => ({
...prevState,
displayValue: value,
}));
Expand All @@ -159,14 +175,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,
}));
Expand All @@ -176,10 +192,10 @@ function FormAutosuggest({
const dropDownItems = getItems(e.target.value);

if (dropDownItems.length > 1) {
setState(prevState => ({
setState((prevState) => ({
...prevState,
dropDownItems,
errorMessage: '',
errorMessage: "",
}));

setIsMenuClosed(false);
Expand All @@ -189,19 +205,21 @@ 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: '',
errorMessage: "",
}));

setIsMenuClosed(false);
} else {
setState(prevState => ({
setState((prevState) => ({
...prevState,
dropDownItems: [],
errorMessageText,
Expand All @@ -215,6 +233,9 @@ function FormAutosuggest({

return (
<div className="pgn__form-autosuggest__wrapper" ref={parentRef}>
<div aria-live="assertive" className="sr-only">
{`${state.dropDownItems.length} options found`}
</div>
<FormGroup isInvalid={!!state.errorMessage}>
<FormControl
aria-expanded={(state.dropDownItems.length > 0).toString()}
Expand All @@ -224,6 +245,7 @@ function FormAutosuggest({
autoComplete="off"
value={state.displayValue}
aria-invalid={state.errorMessage}
aria-activedescendent={activeMenuItemId}
onChange={handleOnChange}
onClick={handleClick}
trailingElement={iconToggle}
Expand All @@ -250,37 +272,44 @@ function FormAutosuggest({
>
{isLoading ? (
<div className="pgn__form-autosuggest__dropdown-loading">
<Spinner animation="border" variant="dark" screenReaderText={screenReaderText} />
<Spinner
animation="border"
variant="dark"
screenReaderText={screenReaderText}
/>
</div>
) : state.dropDownItems.length > 0 && state.dropDownItems}
) : (
state.dropDownItems.length > 0 && state.dropDownItems
)}
</ul>
</div>
);
}

FormAutosuggest.defaultProps = {
arrowKeyNavigationSelector: 'a:not(:disabled),li:not(:disabled, .btn-icon),input:not(:disabled)',
ignoredArrowKeysNames: ['ArrowRight', 'ArrowLeft'],
arrowKeyNavigationSelector:
"a:not(:disabled),li:not(:disabled, .btn-icon),input:not(:disabled)",
ignoredArrowKeysNames: ["ArrowRight", "ArrowLeft"],
isLoading: false,
className: null,
floatingLabel: null,
onChange: null,
onSelected: null,
helpMessage: '',
placeholder: '',
helpMessage: "",
placeholder: "",
value: null,
errorMessageText: null,
readOnly: false,
children: null,
name: 'form-autosuggest',
screenReaderText: 'loading',
name: "form-autosuggest",
screenReaderText: "loading",
};

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),
Expand Down

0 comments on commit 3d11100

Please sign in to comment.