Skip to content

Instantly share code, notes, and snippets.

@rafaelrinaldi
Created January 15, 2020 21:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rafaelrinaldi/736dc1ca72d05a8097e841e6dad3f7fa to your computer and use it in GitHub Desktop.
Save rafaelrinaldi/736dc1ca72d05a8097e841e6dad3f7fa to your computer and use it in GitHub Desktop.
import * as React from 'react';
import cx from 'classnames';
import {keyCodes, sizes, svgs} from '../../constants';
import Icon from '../icon/Icon';
import useClickOutside from '../../hooks/useClickOutside';
import useControlScrolling from '../../hooks/useControlScrolling';
import styles from './DropDown.pcss';
export interface DropDownMenuItem {
label: string | React.ReactNode;
value: string;
}
interface Props {
data?: DropDownMenuItem[];
isDisabled?: boolean;
label?: string | React.ReactNode;
labelPrefix?: string | React.ReactNode;
selectedItem?: DropDownMenuItem;
onChange?: (value: DropDownMenuItem) => void;
shouldHighlightSelectedItem?: boolean;
labelClassName?: string;
}
/**
* Based on Inclusive Components' Menus & Menu Buttons:
* https://inclusive-components.design/menus-menu-buttons
*/
const DropDown: React.FC<Props> = ({
data = [],
isDisabled,
label = '',
labelPrefix = '',
onChange,
selectedItem,
shouldHighlightSelectedItem = false
}) => {
const hasDynamicLabel = !label;
const totalMenuItems: number = data.length - 1;
const nextSelectedItem: DropDownMenuItem | undefined =
selectedItem || (data && data[0]) || undefined;
const selectedItemIndex: number = (nextSelectedItem && data.indexOf(nextSelectedItem)) || 0;
const currentLabel: string | React.ReactNode =
hasDynamicLabel && selectedItem ? selectedItem.label : label;
const [tabIndex, setTabIndex] = React.useState<number>(0);
const [isOpen, setIsOpen] = React.useState<boolean>(false);
const setShouldDisableScrolling = useControlScrolling();
const refContainer: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>();
const refMenuToggle: React.RefObject<HTMLButtonElement> = React.createRef<HTMLButtonElement>();
const refMenuItems: React.RefObject<HTMLButtonElement>[] = data.map(() =>
React.createRef<HTMLButtonElement>()
);
const iconClasses: string = cx(styles.icon, {
[styles.iconUp]: isOpen
});
const setFocus = (item: React.RefObject<HTMLButtonElement>): void => {
item.current && item.current.focus();
};
const setMenuItemFocusByIndex = (index: number): void => setFocus(refMenuItems[index]);
const tabIndexNext = (): number => {
if (tabIndex === totalMenuItems) return 0;
return tabIndex + 1;
};
const tabIndexPrevious = (): number => {
if (tabIndex === 0) return totalMenuItems;
return tabIndex - 1;
};
const onButtonKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>): void => {
if (event.keyCode === keyCodes.UP) setIsOpen(false);
if (event.keyCode === keyCodes.DOWN) setIsOpen(true);
};
const onMenuItemKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>): void => {
if (event.keyCode === keyCodes.UP) setTabIndex(tabIndexPrevious());
if (event.keyCode === keyCodes.DOWN) setTabIndex(tabIndexNext());
if (event.keyCode === keyCodes.ESCAPE) {
setIsOpen(false);
setFocus(refMenuToggle);
}
};
React.useEffect(() => {
if (isOpen) {
setTabIndex(0);
setMenuItemFocusByIndex(0);
}
setShouldDisableScrolling(isOpen);
}, [isOpen]);
React.useEffect(() => setMenuItemFocusByIndex(tabIndex), [tabIndex]);
useClickOutside(refContainer, () => setIsOpen(false));
return (
<div className={styles.root} ref={refContainer}>
<button
disabled={isDisabled}
ref={refMenuToggle}
className={cx(styles.toggle, isDisabled && styles.isDisabled)}
aria-haspopup={true}
aria-expanded={isOpen}
onClick={() => setIsOpen(!isOpen)}
onKeyDown={onButtonKeyDown}>
<span className={styles.toggleLabelPrefix}>{labelPrefix}</span>
<span className={styles.toggleLabel}>{currentLabel}</span>
<Icon icon={svgs.CHEVRON_THIN} size={sizes.CUSTOM} className={iconClasses} />
</button>
<div className={styles.menu} role="menu" hidden={!isOpen}>
{data.map((item: DropDownMenuItem, index: number) => {
const isSelected: boolean = selectedItemIndex === index;
const menuItemClasses: string = cx(styles.menuItem, {
[styles.isSelected]: isSelected,
[styles.hasHighlight]: shouldHighlightSelectedItem
});
return (
<button
ref={refMenuItems[index]}
key={`drop-down-menu-item-${index}`}
className={menuItemClasses}
role="menuitemradio"
tabIndex={-1}
aria-checked={isSelected}
onKeyDown={onMenuItemKeyDown}
onClick={() => {
setIsOpen(false);
if (onChange) onChange(item);
}}>
<span className={styles.menuItemLabel}>{item.label}</span>
</button>
);
})}
</div>
</div>
);
};
export default DropDown;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment