Created
January 15, 2020 21:27
-
-
Save rafaelrinaldi/736dc1ca72d05a8097e841e6dad3f7fa to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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