Use @reach/combobox instead of select2
This commit is contained in:
parent
2fbf2b8f6a
commit
1b57d94d93
5 changed files with 396 additions and 4 deletions
129
app/javascript/components/ComboSearch.js
Normal file
129
app/javascript/components/ComboSearch.js
Normal file
|
@ -0,0 +1,129 @@
|
|||
import React, { useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
import { useQuery } from 'react-query';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxPopover,
|
||||
ComboboxList,
|
||||
ComboboxOption
|
||||
} from '@reach/combobox';
|
||||
import '@reach/combobox/styles.css';
|
||||
|
||||
function defaultTransformResults(_, results) {
|
||||
return results;
|
||||
}
|
||||
|
||||
function ComboSearch({
|
||||
placeholder,
|
||||
required,
|
||||
hiddenFieldId,
|
||||
onChange,
|
||||
scope,
|
||||
minimumInputLength,
|
||||
transformResult,
|
||||
allowInputValues = false,
|
||||
transformResults = defaultTransformResults
|
||||
}) {
|
||||
const label = scope;
|
||||
const hiddenField = useMemo(
|
||||
() => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`),
|
||||
[hiddenFieldId]
|
||||
);
|
||||
const initialValue = hiddenField && hiddenField.value;
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const resultsMap = useRef({});
|
||||
const setExternalValue = useCallback((value) => {
|
||||
if (hiddenField) {
|
||||
hiddenField.setAttribute('value', value);
|
||||
}
|
||||
if (onChange) {
|
||||
const result = resultsMap.current[value];
|
||||
onChange(value, result);
|
||||
}
|
||||
});
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
setDebouncedSearchTerm(searchTerm);
|
||||
},
|
||||
300,
|
||||
[searchTerm]
|
||||
);
|
||||
|
||||
const handleOnChange = useCallback(
|
||||
({ target: { value } }) => {
|
||||
setValue(value);
|
||||
if (value.length >= minimumInputLength) {
|
||||
setSearchTerm(value.trim());
|
||||
if (allowInputValues) {
|
||||
setExternalValue(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
[minimumInputLength]
|
||||
);
|
||||
|
||||
const handleOnSelect = useCallback((value) => {
|
||||
setExternalValue(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
|
||||
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] = 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
|
||||
};
|
||||
|
||||
export default ComboSearch;
|
39
app/javascript/components/shared/queryCache.js
Normal file
39
app/javascript/components/shared/queryCache.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { QueryCache } from 'react-query';
|
||||
import { isNumeric } from '@utils';
|
||||
|
||||
const { api_geo_url, api_adresse_url } = gon.autocomplete || {};
|
||||
|
||||
export const queryCache = new QueryCache({
|
||||
defaultConfig: {
|
||||
queries: {
|
||||
queryFn: defaultQueryFn
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function buildURL(scope, term) {
|
||||
if (scope === 'adresse') {
|
||||
return `${api_adresse_url}/search?q=${term}&limit=5`;
|
||||
} else if (isNumeric(term)) {
|
||||
const code = term.padStart(2, '0');
|
||||
return `${api_geo_url}/${scope}?code=${code}&limit=5`;
|
||||
}
|
||||
return `${api_geo_url}/${scope}?nom=${term}&limit=5`;
|
||||
}
|
||||
|
||||
function buildOptions() {
|
||||
if (window.AbortController) {
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
return [{ signal }, controller];
|
||||
}
|
||||
return [{}, null];
|
||||
}
|
||||
|
||||
async function defaultQueryFn(scope, term) {
|
||||
const url = buildURL(scope, term);
|
||||
const [options, controller] = buildOptions();
|
||||
const promise = fetch(url, options).then((response) => response.json());
|
||||
promise.cancel = () => controller && controller.abort();
|
||||
return promise;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue