import React, {
useMemo,
useState,
useRef,
useContext,
createContext,
useEffect,
useLayoutEffect
} from 'react';
import PropTypes from 'prop-types';
import {
Combobox,
ComboboxInput,
ComboboxList,
ComboboxOption,
ComboboxPopover
} from '@reach/combobox';
import { useId } from '@reach/auto-id';
import '@reach/combobox/styles.css';
import { matchSorter } from 'match-sorter';
import { XIcon } from '@heroicons/react/outline';
import isHotkey from 'is-hotkey';
import invariant from 'tiny-invariant';
import { useDeferredSubmit, useHiddenField } from './shared/hooks';
const Context = createContext();
function ComboMultiple({
options,
id,
labelledby,
describedby,
label,
group,
name = 'value',
selected,
acceptNewValues = false
}) {
invariant(id || label, 'ComboMultiple: `id` or a `label` are required');
invariant(group, 'ComboMultiple: `group` is required');
if (!Array.isArray(options[0])) {
options = options.filter((o) => o).map((o) => [o, o]);
}
const inputRef = useRef();
const [term, setTerm] = useState('');
const [selections, setSelections] = useState(selected);
const [newValues, setNewValues] = useState([]);
const inputId = useId(id);
const removedLabelledby = `${inputId}-remove`;
const selectedLabelledby = `${inputId}-selected`;
const optionValueByLabel = (label) => {
const maybeOption = newValues.includes(label)
? [label, label]
: options.find(([optionLabel]) => optionLabel == label);
return maybeOption ? maybeOption[1] : undefined;
};
const optionLabelByValue = (value) => {
const maybeOption = newValues.includes(value)
? [value, value]
: options.find(([, optionValue]) => optionValue == value);
return maybeOption ? maybeOption[0] : undefined;
};
const extraOptions = useMemo(
() =>
acceptNewValues && term && term.length > 2 && !optionLabelByValue(term)
? [[term, term]]
: [],
[acceptNewValues, term, newValues.join(',')]
);
const results = useMemo(
() =>
[
...extraOptions,
...(term
? matchSorter(
options.filter(([label]) => !label.startsWith('--')),
term
)
: options)
].filter(([, value]) => !selections.includes(value)),
[term, selections.join(','), newValues.join(',')]
);
const [, setHiddenFieldValue, hiddenField] = useHiddenField(group, name);
const awaitFormSubmit = useDeferredSubmit(hiddenField);
const handleChange = (event) => {
setTerm(event.target.value);
};
const saveSelection = (fn) => {
setSelections((selections) => {
selections = fn(selections);
setHiddenFieldValue(JSON.stringify(selections));
return selections;
});
};
const onSelect = (value) => {
const maybeValue = [...extraOptions, ...options].find(
([val]) => val == value
);
const selectedValue = maybeValue && maybeValue[1];
if (selectedValue) {
if (
acceptNewValues &&
extraOptions[0] &&
extraOptions[0][0] == selectedValue
) {
setNewValues((newValues) => [...newValues, selectedValue]);
}
saveSelection((selections) => [...selections, selectedValue]);
}
setTerm('');
awaitFormSubmit.done();
};
const onRemove = (label) => {
const optionValue = optionValueByLabel(label);
if (optionValue) {
saveSelection((selections) =>
selections.filter((value) => value != optionValue)
);
setNewValues((newValues) =>
newValues.filter((value) => value != optionValue)
);
}
inputRef.current.focus();
};
const onKeyDown = (event) => {
if (
isHotkey('enter', event) ||
isHotkey(' ', event) ||
isHotkey(',', event) ||
isHotkey(';', event)
) {
if (
term &&
[...extraOptions, ...options].map(([label]) => label).includes(term)
) {
event.preventDefault();
onSelect(term);
}
}
};
const onBlur = () => {
if (
term &&
[...extraOptions, ...options].map(([label]) => label).includes(term)
) {
awaitFormSubmit(() => {
onSelect(term);
});
}
};
return (
{selections.map((selection) => (