import React, { Fragment, useState, useCallback, useEffect, useRef } from 'react' import { Transition } from '@headlessui/react' import { ChevronDownIcon } from '@heroicons/react/20/solid' import classNames from 'classnames' import { useMountedEffect, useDebounce } from '../custom-hooks' function Option({ isHighlighted, onClick, onMouseEnter, text, id }) { const className = classNames( 'relative cursor-pointer select-none py-2 px-3 text-gray-900 dark:text-gray-300', { 'bg-gray-100 dark:bg-gray-700': isHighlighted } ) return ( // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
  • {text}
  • ) } function scrollTo(wrapper, id) { if (wrapper) { const el = wrapper.querySelector('#' + id) if (el) { el.scrollIntoView({ block: 'center' }) } } } function optionId(index) { return `plausible-combobox-option-${index}` } export default function PlausibleCombobox({ values, fetchOptions, singleOption, isDisabled, autoFocus, freeChoice, disabledOptions, onSelect, placeholder, forceLoading, className, boxClass }) { const isEmpty = values.length === 0 const [options, setOptions] = useState([]) const [isLoading, setLoading] = useState(false) const [isOpen, setOpen] = useState(false) const [search, setSearch] = useState('') const [highlightedIndex, setHighlightedIndex] = useState(0) const searchRef = useRef(null) const containerRef = useRef(null) const listRef = useRef(null) const loading = isLoading || !!forceLoading const visibleOptions = [...options] if ( freeChoice && search.length > 0 && options.every((option) => option.value !== search) ) { visibleOptions.push({ value: search, label: search, freeChoice: true }) } const afterFetchOptions = useCallback((loadedOptions) => { setLoading(false) setHighlightedIndex(0) setOptions(loadedOptions) }, []) const initialFetchOptions = useCallback(() => { setLoading(true) fetchOptions('').then(afterFetchOptions) }, [fetchOptions, afterFetchOptions]) const searchOptions = useCallback(() => { if (isOpen) { setLoading(true) fetchOptions(search).then(afterFetchOptions) } }, [search, isOpen, fetchOptions, afterFetchOptions]) const debouncedSearchOptions = useDebounce(searchOptions) useEffect(() => { if (isOpen) { initialFetchOptions() } }, [isOpen, initialFetchOptions]) useMountedEffect(() => { debouncedSearchOptions() }, [search]) function highLight(index) { let newIndex = index if (index < 0) { newIndex = visibleOptions.length - 1 } else if (index >= visibleOptions.length) { newIndex = 0 } setHighlightedIndex(newIndex) scrollTo(listRef.current, optionId(newIndex)) } function onKeyDown(e) { if (e.key === 'Enter') { if (!isOpen || loading || visibleOptions.length === 0) return null selectOption(visibleOptions[highlightedIndex]) e.preventDefault() } if (e.key === 'Escape') { if (!isOpen || loading) return null setOpen(false) searchRef.current?.focus() e.preventDefault() } if (e.key === 'ArrowDown') { if (isOpen) { highLight(highlightedIndex + 1) } else { setOpen(true) } } if (e.key === 'ArrowUp') { if (isOpen) { highLight(highlightedIndex - 1) } else { setOpen(true) } } } function isOptionDisabled(option) { const optionAlreadySelected = values.some( (val) => val.value === option.value ) const optionDisabled = (disabledOptions || []).some( (val) => val?.value === option.value ) return optionAlreadySelected || optionDisabled } function onInput(e) { if (!isOpen) { setOpen(true) } setSearch(e.target.value) } function toggleOpen() { if (!isOpen) { setOpen(true) searchRef.current.focus() } else { setSearch('') setOpen(false) } } function selectOption(option) { if (singleOption) { onSelect([option]) } else { searchRef.current.focus() onSelect([...values, option]) } setOpen(false) setSearch('') } function removeOption(option, e) { e.stopPropagation() const newValues = values.filter((val) => val.value !== option.value) onSelect(newValues) searchRef.current.focus() setOpen(false) } const handleClick = useCallback((e) => { if (containerRef.current && containerRef.current.contains(e.target)) { return } setSearch('') setOpen(false) }, []) useEffect(() => { document.addEventListener('mousedown', handleClick, false) return () => { document.removeEventListener('mousedown', handleClick, false) } }, [handleClick]) useEffect(() => { if (singleOption && isEmpty && autoFocus) { searchRef.current.focus() } }, [isEmpty, singleOption, autoFocus]) const searchBoxClass = 'border-none py-1 px-0 w-full inline-block rounded-md focus:outline-hidden focus:ring-0 text-sm' const containerClass = classNames('relative w-full', { [className]: !!className, 'opacity-30 cursor-default pointer-events-none': isDisabled }) function renderSingleOptionContent() { const itemSelected = values.length === 1 return (
    {itemSelected && renderSingleSelectedItem()}
    ) } function renderSingleSelectedItem() { if (search === '') { return ( {values[0].label} ) } } function renderMultiOptionContent() { return ( <> {values.map((value) => { return (
    {value.label} removeOption(value, e)} className="cursor-pointer font-bold ml-1" > ×
    ) })} ) } function renderDropDownContent() { const matchesFound = visibleOptions.length > 0 && visibleOptions.some((option) => !isOptionDisabled(option)) if (loading) { return (
    Loading options...
    ) } if (matchesFound) { return visibleOptions .filter((option) => !isOptionDisabled(option)) .map((option, i) => { const text = option.freeChoice ? `Filter by '${option.label}'` : option.label return (