demarches-normaliennes/app/javascript/components/ComboSearch.jsx
2021-05-24 11:57:00 +02:00

143 lines
3.8 KiB
JavaScript

import React, { useState, useMemo, useCallback, useRef } from 'react';
import { useDebounce } from 'use-debounce';
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 { fire } from '@utils';
function defaultTransformResults(_, results) {
return results;
}
function ComboSearch({
placeholder,
required,
hiddenFieldId,
onChange,
scope,
minimumInputLength,
transformResult,
allowInputValues = false,
transformResults = defaultTransformResults,
className
}) {
const label = scope;
const hiddenValueField = useMemo(
() => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`),
[hiddenFieldId]
);
const hiddenIdField = useMemo(
() =>
document.querySelector(
`input[data-uuid="${hiddenFieldId}"] + input[data-reference]`
),
[hiddenFieldId]
);
const initialValue = hiddenValueField && hiddenValueField.value;
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm] = useDebounce(searchTerm, 300);
const [value, setValue] = useState(initialValue);
const resultsMap = useRef({});
const setExternalValue = useCallback((value) => {
if (hiddenValueField) {
hiddenValueField.setAttribute('value', value);
fire(hiddenValueField, 'autosave:trigger');
}
});
const setExternalId = useCallback((key) => {
if (hiddenIdField) {
hiddenIdField.setAttribute('value', key);
}
});
const setExternalValueAndId = useCallback((value) => {
const [key, result] = resultsMap.current[value];
setExternalId(key);
setExternalValue(value);
if (onChange) {
onChange(value, result);
}
});
const handleOnChange = useCallback(
({ target: { value } }) => {
setValue(value);
if (value.length >= minimumInputLength) {
setSearchTerm(value.trim());
if (allowInputValues) {
setExternalId('');
setExternalValue(value);
}
}
},
[minimumInputLength]
);
const handleOnSelect = useCallback((value) => {
setExternalValueAndId(value);
setValue(value);
});
const { isSuccess, data } = useQuery([scope, debouncedSearchTerm], {
enabled: !!debouncedSearchTerm,
notifyOnStatusChange: false,
refetchOnMount: false
});
const results = isSuccess ? transformResults(debouncedSearchTerm, data) : [];
return (
<Combobox aria-label={label} onSelect={handleOnSelect}>
<ComboboxInput
className={className}
placeholder={placeholder}
onChange={handleOnChange}
value={value}
required={required}
/>
{isSuccess && (
<ComboboxPopover className="shadow-popup">
{results.length > 0 ? (
<ComboboxList>
{results.map((result) => {
const [key, str] = transformResult(result);
resultsMap.current[str] = [key, result];
return (
<ComboboxOption
key={key}
value={str}
data-option-value={str}
/>
);
})}
</ComboboxList>
) : (
<span style={{ display: 'block', margin: 8 }}>
Aucun résultat trouvé
</span>
)}
</ComboboxPopover>
)}
</Combobox>
);
}
ComboSearch.propTypes = {
placeholder: PropTypes.string,
required: PropTypes.bool,
hiddenFieldId: PropTypes.string,
scope: PropTypes.string,
minimumInputLength: PropTypes.number,
transformResult: PropTypes.func,
transformResults: PropTypes.func,
allowInputValues: PropTypes.bool,
onChange: PropTypes.func,
className: PropTypes.string
};
export default ComboSearch;