Spaces:
Runtime error
Runtime error
import './Dropdown.scss'; | |
import ChevronIcon from '@assets/images/chevron.svg?react'; | |
import { ForwardedRef, forwardRef, useEffect, useRef, useState } from 'react'; | |
const useIsOpened = () => { | |
const [isOpened, setIsOpened] = useState<boolean>(false); | |
const ref = useRef<HTMLButtonElement>(null); | |
useEffect(() => { | |
const handleClickOutside = (event: MouseEvent) => { | |
if (!ref.current?.contains(event.target as Node)) { | |
setIsOpened(!isOpened); | |
} | |
}; | |
document.addEventListener('click', handleClickOutside, !isOpened); | |
return () => { | |
document.removeEventListener('click', handleClickOutside, !isOpened); | |
}; | |
}, [isOpened]); | |
return { ref, isOpened, setIsOpened }; | |
}; | |
const sparkClassNames = { | |
dropdown: 'spark-dropdown spark-dropdown-primary spark-dropdown-size-m', | |
dropdownButton: | |
'spark-button spark-button-action spark-button-size-m spark-focus-visible spark-focus-visible-self spark-focus-visible-snap spark-dropdown-button spark-focus-visible spark-focus-visible-self spark-focus-visible-snap', | |
dropdownButtonContent: 'spark-button-content', | |
dropdownButtonLabel: 'spark-dropdown-button-label', | |
dropdownButtonIcon: 'spark-icon spark-icon-regular spark-dropdown-arrow-icon', | |
popover: 'spark-popover spark-shadow', | |
dropdownListWrapper: | |
'spark-scrollbar spark-scrollbar-y spark-focus-visible spark-focus-visible-self spark-focus-visible-snap spark-dropdown-list-box-scroll', | |
dropdownList: 'spark-list spark-list-size-m spark-dropdown-list-box spark-dropdown-primary', | |
dropdownListItem: 'spark-list-item', | |
dropdownListActiveItem: 'spark-list-is-focused', | |
dropdownListItemText: 'spark-list-item-text', | |
} as const; | |
type DropdownPopoverProps = { | |
items: { text: string; onClick: () => void }[]; | |
direction: DropdownProps['direction']; | |
selectedOption: string | null; | |
}; | |
const DropdownPopover = forwardRef(function DropdownPopover( | |
{ items, selectedOption, direction = 'bottom' }: DropdownPopoverProps, | |
ref: ForwardedRef<HTMLDivElement> | |
): JSX.Element { | |
const directionClassName = `dropdown-popover-${direction}`; | |
return ( | |
<div ref={ref} className={`${sparkClassNames.popover} ${directionClassName}`}> | |
<div tabIndex={-1} className={sparkClassNames.dropdownListWrapper} role="group"> | |
<ul className={sparkClassNames.dropdownList} role="listbox" tabIndex={-1}> | |
{items.map(({ text, onClick }, i) => ( | |
<li | |
key={`dropdown-item-${i}-${text}`} | |
className={`${sparkClassNames.dropdownListItem} ${ | |
text === selectedOption ? sparkClassNames.dropdownListActiveItem : '' | |
}`} | |
role="option" | |
tabIndex={0} | |
onClick={onClick} | |
> | |
<span className={sparkClassNames.dropdownListItemText}>{text}</span> | |
</li> | |
))} | |
</ul> | |
</div> | |
</div> | |
); | |
}); | |
type DropdownProps = { | |
options: string[]; | |
selectedOption: string | null; | |
onSelect: (value: string) => void; | |
placeholder?: string; | |
selectedPrefix?: string; | |
className?: string; | |
direction?: 'bottom' | 'top'; | |
}; | |
export const Dropdown = ({ | |
options, | |
selectedOption, | |
onSelect, | |
placeholder = 'Select an Option', | |
selectedPrefix = '', | |
className = '', | |
direction = 'bottom', | |
}: DropdownProps): JSX.Element => { | |
const { ref, isOpened, setIsOpened } = useIsOpened(); | |
const selectedOptionText = selectedPrefix ? `${selectedPrefix}: ${selectedOption}` : selectedOption; | |
return ( | |
<> | |
<div className={`${sparkClassNames.dropdown} ${className}`}> | |
<button | |
ref={ref} | |
className={sparkClassNames.dropdownButton} | |
type="button" | |
aria-haspopup="listbox" | |
aria-expanded={isOpened} | |
onClick={() => setIsOpened(!isOpened)} | |
> | |
<span className={sparkClassNames.dropdownButtonContent}> | |
<span className={sparkClassNames.dropdownButtonLabel}> | |
{selectedOption ? selectedOptionText : placeholder} | |
</span> | |
<span aria-hidden="true" role="img" className={sparkClassNames.dropdownButtonIcon}> | |
<ChevronIcon /> | |
</span> | |
</span> | |
</button> | |
{isOpened && ( | |
<DropdownPopover | |
direction={direction} | |
selectedOption={selectedOption} | |
items={options.map((option) => ({ | |
text: option, | |
onClick: () => { | |
onSelect(option); | |
setIsOpened(false); | |
}, | |
}))} | |
/> | |
)} | |
</div> | |
</> | |
); | |
}; | |