import { NavigationChevronDown } from '@repay/cactus-icons' import { border, color, colorStyle, insetBorder, textStyle } from '@repay/cactus-theme' import React from 'react' import styled from 'styled-components' import { keyDownAsClick, LiveRegion } from '../helpers/a11y' import { useScrollTrap } from '../helpers/scroll' import usePopup, { TogglePopup } from '../helpers/usePopup' interface Option { value: number label: string } interface DropDownProps extends React.AriaAttributes { className?: string label: string | number value: number options: number[] | Option[] onSelectOption: (value: number, e: React.SyntheticEvent) => void liveKey?: React.Key } const getFocusHint = (e: React.KeyboardEvent, toggle: TogglePopup) => { let focusHint = 0 switch (e.key) { case 'Home': toggle(true, 0) break case 'End': toggle(true, -1) break case 'PageUp': focusHint = -7 break case 'PageDown': focusHint = 7 break case 'ArrowUp': focusHint = -1 break case 'ArrowDown': focusHint = 1 break default: return } e.preventDefault() e.stopPropagation() return focusHint } const positionPopup = (popup: HTMLElement) => { let header = popup while ((header = header.parentElement as HTMLElement)) { if (header.parentElement?.getAttribute('role') === 'group') { const headerHeight = header.offsetHeight const popupHeight = header.parentElement.clientHeight - headerHeight let item = popup.firstElementChild const itemHeight = item?.clientHeight ?? 35 let itemMiddle = itemHeight >> 1 while (item && item.getAttribute('aria-selected') !== 'true') { item = item.nextElementSibling itemMiddle += itemHeight } popup.style.top = `${headerHeight}px` popup.scrollTop = itemMiddle - popupHeight / 2 return } } } const focusControl = (root: HTMLElement) => Array.from(root.querySelectorAll('li')) const DropDownBase = ({ className, label, value, options, onSelectOption, liveKey, ...props }: DropDownProps) => { const popupRef = React.useRef(null) useScrollTrap(popupRef) const { wrapperProps, popupProps, buttonProps, expanded, toggle } = usePopup('listbox', { focusControl, positionPopup, }) const closeOnEscape = wrapperProps.onKeyDown wrapperProps.onKeyDown = (e) => { closeOnEscape?.(e) keyDownAsClick(e) if (e.isDefaultPrevented()) return const focusHint = getFocusHint(e, toggle) if (focusHint) { if (expanded) { toggle(true, focusHint, { shift: true }) } else { const item = popupRef.current?.querySelector('[aria-selected="true"]') toggle(true, item) } } } buttonProps['aria-describedby'] = props['aria-labelledby'] delete buttonProps.onKeyDown // Handled at the wrapper level. buttonProps.onClick = () => { const selected = popupRef.current?.querySelector('[aria-selected="true"]') toggle(undefined, selected as HTMLElement) } popupProps.children = options.map((opt: number | Option) => { const optProps = typeof opt === 'object' ? { key: opt.value, children: {opt.label} } : { key: opt, children: {opt} } return (
  • ) }) popupProps.onClick = (e: React.MouseEvent) => { let target = e.target as HTMLElement if (!target.matches('[data-value]')) { target = target.parentElement as HTMLElement } if (target.dataset.value) { toggle(false, e.currentTarget.previousElementSibling as HTMLElement) onSelectOption(parseInt(target.dataset.value), e) } } return (
    {liveKey && }
    ) } const OUTLINE = { thin: '2px' } const DropDown = styled(DropDownBase)` button { appearance: none; background-color: transparent; border: none; outline: none; padding: 0 4px; ${textStyle('h4')} &:focus-visible { outline: 2px solid black; } } [aria-expanded] { color: ${color('callToAction')}; svg { transform: scaleY(-1); } } ul { overflow-y: scroll; position: absolute; z-index: 1; top: 62px; bottom: 0; left: 0; right: 0; list-style: none; padding: 0; margin: 0; outline: none; background-color: ${color('lightContrast')}; } [aria-hidden='true'] { display: none; } li { width: 100%; text-align: center; cursor: pointer; padding: 4px 0; outline: none; span { padding: 4px; border-radius: 4px; } &:hover span, &:focus span { background-color: ${color('lightCallToAction')}; } &:focus-visible span { background-color: transparent; border: ${border('callToAction', OUTLINE)}; } &[aria-selected='true'] span { ${colorStyle('callToAction')} } &[aria-selected='true']:focus-visible span { ${insetBorder('white', undefined, OUTLINE)}; } } ` export default DropDown