create ComboMultipleDropdownList component
This commit is contained in:
parent
1b43cfb678
commit
e048f48241
2 changed files with 222 additions and 0 deletions
217
app/javascript/components/ComboMultipleDropdownList.js
Normal file
217
app/javascript/components/ComboMultipleDropdownList.js
Normal file
|
@ -0,0 +1,217 @@
|
|||
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 '@reach/combobox/styles.css';
|
||||
import matchSorter from 'match-sorter';
|
||||
import { fire } from '@utils';
|
||||
|
||||
const Context = createContext();
|
||||
|
||||
function ComboMultipleDropdownList({
|
||||
options,
|
||||
hiddenFieldId,
|
||||
selected,
|
||||
label
|
||||
}) {
|
||||
if (label == undefined) {
|
||||
label = 'Choisir une option';
|
||||
}
|
||||
if (Array.isArray(options[0]) == false) {
|
||||
options = options.map((o) => [o, o]);
|
||||
}
|
||||
const inputRef = useRef();
|
||||
const [term, setTerm] = useState('');
|
||||
const [selections, setSelections] = useState(selected);
|
||||
const results = useMemo(
|
||||
() =>
|
||||
(term
|
||||
? matchSorter(
|
||||
options.filter((o) => !o[0].startsWith('--')),
|
||||
term
|
||||
)
|
||||
: options
|
||||
).filter((o) => o[0] && !selections.includes(o[1])),
|
||||
[term, selections]
|
||||
);
|
||||
const hiddenField = useMemo(
|
||||
() => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`),
|
||||
[hiddenFieldId]
|
||||
);
|
||||
|
||||
const handleChange = (event) => {
|
||||
setTerm(event.target.value);
|
||||
};
|
||||
|
||||
const saveSelection = (selections) => {
|
||||
setSelections(selections);
|
||||
if (hiddenField) {
|
||||
hiddenField.setAttribute('value', JSON.stringify(selections));
|
||||
fire(hiddenField, 'autosave:trigger');
|
||||
}
|
||||
};
|
||||
|
||||
const onSelect = (value) => {
|
||||
let sel = options.find((o) => o[0] == value)[1];
|
||||
saveSelection([...selections, sel]);
|
||||
setTerm('');
|
||||
};
|
||||
|
||||
const onRemove = (value) => {
|
||||
saveSelection(
|
||||
selections.filter((s) => s !== options.find((o) => o[0] == value)[1])
|
||||
);
|
||||
inputRef.current.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<Combobox openOnFocus={true} onSelect={onSelect} aria-label={label}>
|
||||
<ComboboxTokenLabel onRemove={onRemove}>
|
||||
<ul
|
||||
aria-live="polite"
|
||||
aria-atomic={true}
|
||||
data-reach-combobox-token-list
|
||||
>
|
||||
{selections.map((selection) => (
|
||||
<ComboboxToken
|
||||
key={selection}
|
||||
value={options.find((o) => o[1] == selection)[0]}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<ComboboxInput
|
||||
ref={inputRef}
|
||||
value={term}
|
||||
onChange={handleChange}
|
||||
autocomplete={false}
|
||||
/>
|
||||
</ComboboxTokenLabel>
|
||||
{results && (
|
||||
<ComboboxPopover portal={false}>
|
||||
{results.length === 0 && (
|
||||
<p>
|
||||
Aucun résultat{' '}
|
||||
<button
|
||||
onClick={() => {
|
||||
setTerm('');
|
||||
}}
|
||||
>
|
||||
Effacer
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
<ComboboxList>
|
||||
{results.map((value, index) => {
|
||||
if (value[0].startsWith('--')) {
|
||||
return <ComboboxSeparator key={index} value={value[0]} />;
|
||||
}
|
||||
return <ComboboxOption key={index} value={value[0]} />;
|
||||
})}
|
||||
</ComboboxList>
|
||||
</ComboboxPopover>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxTokenLabel({ onRemove, ...props }) {
|
||||
const selectionsRef = useRef([]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
selectionsRef.current = [];
|
||||
return () => (selectionsRef.current = []);
|
||||
});
|
||||
|
||||
const context = {
|
||||
onRemove,
|
||||
selectionsRef
|
||||
};
|
||||
|
||||
return (
|
||||
<Context.Provider value={context}>
|
||||
<div data-combobox-token-label {...props} />
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
ComboboxTokenLabel.propTypes = {
|
||||
onRemove: PropTypes.func
|
||||
};
|
||||
|
||||
function ComboboxSeparator({ value }) {
|
||||
return (
|
||||
<li aria-disabled="true" role="option" data-combobox-separator>
|
||||
{value.slice(2, -2)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
ComboboxSeparator.propTypes = {
|
||||
value: PropTypes.string
|
||||
};
|
||||
|
||||
function ComboboxToken({ value, ...props }) {
|
||||
const { selectionsRef, onRemove } = useContext(Context);
|
||||
useEffect(() => {
|
||||
selectionsRef.current.push(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<li
|
||||
data-reach-combobox-token
|
||||
tabIndex="0"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Backspace') {
|
||||
onRemove(value);
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
role="presentation"
|
||||
data-combobox-remove-token
|
||||
onClick={() => {
|
||||
onRemove(value);
|
||||
}}
|
||||
>
|
||||
x
|
||||
</span>
|
||||
{value}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
ComboboxToken.propTypes = {
|
||||
value: PropTypes.string,
|
||||
label: PropTypes.string
|
||||
};
|
||||
|
||||
ComboMultipleDropdownList.propTypes = {
|
||||
options: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
)
|
||||
)
|
||||
]),
|
||||
hiddenFieldId: PropTypes.string,
|
||||
selected: PropTypes.arrayOf(PropTypes.string),
|
||||
arraySelected: PropTypes.arrayOf(PropTypes.array),
|
||||
label: PropTypes.string
|
||||
};
|
||||
|
||||
export default ComboMultipleDropdownList;
|
5
app/javascript/loaders/ComboMultipleDropdownList.js
Normal file
5
app/javascript/loaders/ComboMultipleDropdownList.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Loadable from '../components/Loadable';
|
||||
|
||||
export default Loadable(() =>
|
||||
import('../components/ComboMultipleDropdownList')
|
||||
);
|
Loading…
Add table
Reference in a new issue