Skip to content

Commit

Permalink
fix(dropdown): adding accessibility (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
hcafaq committed Feb 15, 2023
1 parent 5e63f99 commit 5e8cbec
Show file tree
Hide file tree
Showing 16 changed files with 314 additions and 152 deletions.
30 changes: 17 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/dropdown/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"react": "^17.0.1"
},
"dependencies": {
"@heycar-uikit/icons": "^4.0.0",
"@heycar-uikit/input": "^1.1.9",
"classnames": "^2.3.1"
},
"author": "HeyCar Team",
Expand Down
111 changes: 76 additions & 35 deletions packages/dropdown/src/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
/* eslint-disable prettier/prettier */
import React, { useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import cn from 'classnames';

import { DropdownProps, SelectOptions } from './Dropdown.types';
import { ChevronDown, ChevronTop } from '@heycar-uikit/icons';
import Input from '@heycar-uikit/input';

import DropdownOption from './components/DropdownOption';
import { DropdownOptionProps } from './components/DropdownOption.types';
import { DropdownProps } from './Dropdown.types';

import styles from './styles/default.module.css';

Expand All @@ -16,22 +21,68 @@ function Dropdown({
onBlur,
onClick,
fullWidth,
placeholder,
...restProps
}: DropdownProps) {
const [stateValue, setStateValue] = useState<SelectOptions | undefined>(
const [stateValue, setStateValue] = useState<DropdownOptionProps | undefined>(
value,
);

const [isOpen, setIsOpen] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);

const selectOption = (option: SelectOptions) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const selectOption = (option: DropdownOptionProps) => {
if (onChange) {
if (option !== stateValue) onChange(option);
}
setStateValue(option);
};

const isOptionSelection = (option: SelectOptions) => {
useEffect(() => {
if (isOpen) setHighlightedIndex(0);
}, [isOpen]);

useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.target != containerRef.current) return;
switch (e.code) {
case 'Enter':
case 'Space':
setIsOpen(prev => !prev);
if (isOpen) selectOption(options[highlightedIndex]);
break;
case 'ArrowUp':
case 'ArrowDown': {
if (!isOpen) {
setIsOpen(true);
break;
}

const newValue = highlightedIndex + (e.code === 'ArrowDown' ? 1 : -1);

if (newValue >= 0 && newValue < options.length) {
setHighlightedIndex(newValue);
}
break;
}
case 'Escape':
setIsOpen(false);
break;
}
};

containerRef.current?.addEventListener('keydown', handler);

return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
containerRef.current?.removeEventListener('keydown', handler);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [highlightedIndex, options, selectOption]);

const isOptionSelection = (option: DropdownOptionProps) => {
return option?.value === stateValue?.value;
};

Expand All @@ -50,55 +101,45 @@ function Dropdown({
fullWidth && styles.fullWidth,
);

const arrowClassNames = cn(
disabled && styles.caret_disabled,
isOpen && styles.caret_up,
!isOpen && styles.caret_down,
);

const valueClassNames = cn(
disabled && 'disabled',
styles.value,
);
const valueClassNames = cn(disabled && 'disabled', styles.value);

return (
<Component
className={classNames}
onBlur={onBlurHandler}
onClick={onClickHandler}
ref={containerRef}
tabIndex={0}
{...restProps}
onBlur={onBlurHandler}
>
<span className={valueClassNames}>
{stateValue?.label}
</span>
<div className={arrowClassNames}></div>
<Input
className={valueClassNames}
disabled={disabled}
fullWidth={true}
onChange={() => {
setStateValue(options[highlightedIndex]);
}}
placeholder={placeholder}
rightIcon={isOpen ? <ChevronTop /> : <ChevronDown />}
value={stateValue?.label}
/>
<ul
className={`${styles.options} ${isOpen ? styles.show : ''}`}
data-test-id={dataTestId}
>
{options.map(option => (
{options.map((option, index) => (
<li
className={`${styles.option} ${isOptionSelection(option) ? styles.selected : ''
}`}
} ${index === highlightedIndex ? styles.highlighted : ''}`}
key={option.value}
onClick={e => {
e.stopPropagation();
onMouseDown={e => {
selectOption(option);
e.stopPropagation();
setIsOpen(false);
}}
onMouseEnter={() => setHighlightedIndex(index)}
>
<div className={styles.contentContainer}>
{option.leftContent && (
<span className={styles.leftContent}>{option.leftContent}</span>
)}
<span className={styles.optionContent}>{option.label}</span>
{option.rightContent && (
<span className={styles.rightContent}>
{option.rightContent}
</span>
)}
</div>
<DropdownOption {...option} />
</li>
))}
</ul>
Expand Down
29 changes: 8 additions & 21 deletions packages/dropdown/src/Dropdown.types.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,12 @@
import React from 'react';

export type SelectOptions = {
/**
* labels of options
*/
label: string;
/**
* value of options
*/
value: string;
/**
* Element placed before the children.
*/
leftContent?: React.ReactNode;
/**
* Element placed after the children
*/
rightContent?: React.ReactNode;
};
import { DropdownOptionProps } from './components/DropdownOption.types';

export type DropdownProps = {
/**
* list of options to show in dropdown
*/
options: SelectOptions[];
options: DropdownOptionProps[];
/**
* onClick method callback upon dropdown click
*/
Expand All @@ -35,11 +18,11 @@ export type DropdownProps = {
/**
* onChange method callback upon option change
*/
onChange?: (value: SelectOptions | undefined) => void;
onChange?: (value: DropdownOptionProps | undefined) => void;
/**
* for setting default value.
*/
value?: SelectOptions;
value?: DropdownOptionProps;
/**
* boolean prop to set disabled state.
*/
Expand All @@ -56,4 +39,8 @@ export type DropdownProps = {
* The component used for the root node. Either a string to use a HTML element or a component
*/
Component?: React.ElementType;
/**
* The string as a placeholder
*/
placeholder?: string;
};
6 changes: 3 additions & 3 deletions packages/dropdown/src/__tests__/Dropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('Dropdown', () => {
/>,
);

expect(container.querySelector('span')).toHaveTextContent('Mango');
expect(container.querySelector('span')).toHaveTextContent('Pomelo');
});

it('should set options', () => {
Expand Down Expand Up @@ -90,7 +90,7 @@ describe('Dropdown', () => {
/>,
);

expect(container.querySelector('span')).toHaveClass('disabled');
expect(container.querySelector('input')).toBeDisabled();
});

it('should set `full width` class', () => {
Expand Down Expand Up @@ -154,7 +154,7 @@ describe('Dropdown', () => {

const ul = getByTestId(dataTestId) as HTMLUListElement;

if (ul.firstChild) fireEvent.click(ul.firstChild);
if (ul.firstChild) fireEvent.mouseDown(ul.firstChild);

expect(cb).toBeCalledTimes(1);
});
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

1 comment on commit 5e8cbec

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 97.72% 1029/1053
🟢 Branches 84.32% 199/236
🟢 Functions 92.86% 65/70
🟢 Lines 98.02% 941/960

Test suite run success

250 tests passing in 37 suites.

Report generated by 🧪jest coverage report action from 5e8cbec

Please sign in to comment.