demarches-normaliennes/app/javascript/components/ComboSearch.jsx

154 lines
4.2 KiB
React
Raw Normal View History

import React, { useState, useCallback, useRef } from 'react';
import { useDebounce } from 'use-debounce';
2020-10-07 17:41:20 +02:00
import { useQuery } from 'react-query';
import PropTypes from 'prop-types';
import {
Combobox,
ComboboxInput,
ComboboxPopover,
ComboboxList,
ComboboxOption
} from '@reach/combobox';
import '@reach/combobox/styles.css';
import invariant from 'tiny-invariant';
2020-10-07 17:41:20 +02:00
import { useDeferredSubmit, useHiddenField, groupId } from './shared/hooks';
2021-06-09 17:13:26 +02:00
2020-10-07 17:41:20 +02:00
function defaultTransformResults(_, results) {
return results;
}
function ComboSearch({
onChange,
value: controlledValue,
2020-10-07 17:41:20 +02:00
scope,
scopeExtra,
2020-10-07 17:41:20 +02:00
minimumInputLength,
transformResult,
allowInputValues = false,
transformResults = defaultTransformResults,
id,
describedby,
...props
2020-10-07 17:41:20 +02:00
}) {
invariant(id || onChange, 'ComboSearch: `id` or `onChange` are required');
const group = !onChange ? groupId(id) : null;
const [externalValue, setExternalValue, hiddenField] = useHiddenField(group);
const [, setExternalId] = useHiddenField(group, 'external_id');
const initialValue = externalValue ? externalValue : controlledValue;
2020-10-07 17:41:20 +02:00
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm] = useDebounce(searchTerm, 300);
2020-10-07 17:41:20 +02:00
const [value, setValue] = useState(initialValue);
const resultsMap = useRef({});
const getLabel = (result) => {
const [, value, label] = transformResult(result);
return label ?? value;
};
const setExternalValueAndId = useCallback((label) => {
const { key, value, result } = resultsMap.current[label];
2020-10-07 17:41:20 +02:00
if (onChange) {
onChange(value, result);
} else {
setExternalId(key);
setExternalValue(value);
2020-10-07 17:41:20 +02:00
}
2021-06-09 17:09:07 +02:00
}, []);
const awaitFormSubmit = useDeferredSubmit(hiddenField);
2020-10-07 17:41:20 +02:00
const handleOnChange = useCallback(
({ target: { value } }) => {
setValue(value);
if (!value) {
if (onChange) {
onChange(null);
} else {
setExternalId('');
setExternalValue('');
}
} else if (value.length >= minimumInputLength) {
2020-10-07 17:41:20 +02:00
setSearchTerm(value.trim());
if (allowInputValues) {
setExternalId('');
2020-10-07 17:41:20 +02:00
setExternalValue(value);
}
}
},
[minimumInputLength]
);
const handleOnSelect = useCallback((value) => {
setExternalValueAndId(value);
2020-10-07 17:41:20 +02:00
setValue(value);
2021-06-09 17:13:26 +02:00
setSearchTerm('');
awaitFormSubmit.done();
2021-06-09 17:09:07 +02:00
}, []);
2020-10-07 17:41:20 +02:00
const { isSuccess, data } = useQuery(
[scope, debouncedSearchTerm, scopeExtra],
{
enabled: !!debouncedSearchTerm,
notifyOnStatusChange: false,
refetchOnMount: false
}
);
2020-10-07 17:41:20 +02:00
const results = isSuccess ? transformResults(debouncedSearchTerm, data) : [];
2021-06-09 17:13:26 +02:00
const onBlur = useCallback(() => {
if (!allowInputValues && isSuccess && results[0]) {
const label = getLabel(results[0]);
2021-06-09 17:13:26 +02:00
awaitFormSubmit(() => {
handleOnSelect(label);
2021-06-09 17:13:26 +02:00
});
}
}, [data]);
2020-10-07 17:41:20 +02:00
return (
<Combobox onSelect={handleOnSelect}>
2020-10-07 17:41:20 +02:00
<ComboboxInput
{...props}
2020-10-07 17:41:20 +02:00
onChange={handleOnChange}
2021-06-09 17:13:26 +02:00
onBlur={onBlur}
2020-10-07 17:41:20 +02:00
value={value}
autocomplete={false}
id={id}
aria-describedby={describedby}
2020-10-07 17:41:20 +02:00
/>
{isSuccess && (
<ComboboxPopover className="shadow-popup">
{results.length > 0 ? (
<ComboboxList>
{results.map((result, index) => {
const label = getLabel(result);
const [key, value] = transformResult(result);
resultsMap.current[label] = { key, value, result };
return <ComboboxOption key={`${key}-${index}`} value={label} />;
2020-10-07 17:41:20 +02:00
})}
</ComboboxList>
) : (
<span style={{ display: 'block', margin: 8 }}>
Aucun résultat trouvé
</span>
)}
</ComboboxPopover>
)}
</Combobox>
);
}
ComboSearch.propTypes = {
value: PropTypes.string,
2020-10-07 17:41:20 +02:00
scope: PropTypes.string,
minimumInputLength: PropTypes.number,
transformResult: PropTypes.func,
transformResults: PropTypes.func,
allowInputValues: PropTypes.bool,
onChange: PropTypes.func,
scopeExtra: PropTypes.string,
mandatory: PropTypes.bool,
id: PropTypes.string,
describedby: PropTypes.string
2020-10-07 17:41:20 +02:00
};
export default ComboSearch;