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 23, 2023
1 parent 79f0cb4 commit 236afbc
Showing 1 changed file with 53 additions and 26 deletions.
79 changes: 53 additions & 26 deletions src/Form/FormAutosuggest.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
Expand All @@ -45,7 +48,7 @@ function FormAutosuggest({
onSelected(clickedValue);
}

setState(prevState => ({
setState((prevState) => ({
...prevState,
dropDownItems: [],
displayValue: clickedValue,
Expand All @@ -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;
Expand All @@ -91,7 +99,7 @@ function FormAutosuggest({
newState.errorMessage = '';
}

setState(prevState => ({
setState((prevState) => ({
...prevState,
...newState,
}));
Expand All @@ -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 : '',
Expand All @@ -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 : '',
Expand All @@ -149,7 +163,7 @@ function FormAutosuggest({

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

if (dropDownItems.length > 1) {
setState(prevState => ({
setState((prevState) => ({
...prevState,
dropDownItems,
errorMessage: '',
Expand All @@ -189,19 +203,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: '',
}));

setIsMenuClosed(false);
} else {
setState(prevState => ({
setState((prevState) => ({
...prevState,
dropDownItems: [],
errorMessageText,
Expand All @@ -215,6 +231,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 +243,7 @@ function FormAutosuggest({
autoComplete="off"
value={state.displayValue}
aria-invalid={state.errorMessage}
aria-activedescendant={activeMenuItemId}
onChange={handleOnChange}
onClick={handleClick}
trailingElement={iconToggle}
Expand All @@ -250,16 +270,23 @@ 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)',
arrowKeyNavigationSelector:
'a:not(:disabled),li:not(:disabled, .btn-icon),input:not(:disabled)',
ignoredArrowKeysNames: ['ArrowRight', 'ArrowLeft'],
isLoading: false,
className: null,
Expand All @@ -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),
Expand Down

0 comments on commit 236afbc

Please sign in to comment.