2023-10-03 17:02:23 +02:00
|
|
|
import React, {
|
|
|
|
|
forwardRef,
|
|
|
|
|
useEffect,
|
|
|
|
|
useImperativeHandle,
|
|
|
|
|
useRef,
|
|
|
|
|
useState,
|
|
|
|
|
} from "react";
|
2023-06-12 14:27:40 +02:00
|
|
|
import { useCombobox, useMultipleSelection } from "downshift";
|
|
|
|
|
import { optionToString } from ":/components/Forms/Select/mono-common";
|
|
|
|
|
import {
|
|
|
|
|
getMultiOptionsFilter,
|
|
|
|
|
SelectMultiAux,
|
|
|
|
|
SubProps,
|
|
|
|
|
} from ":/components/Forms/Select/multi-common";
|
2023-10-03 17:02:23 +02:00
|
|
|
import { SelectHandle } from ":/components/Forms/Select/index";
|
2023-06-12 14:27:40 +02:00
|
|
|
|
2023-10-03 17:02:23 +02:00
|
|
|
export const SelectMultiSearchable = forwardRef<SelectHandle, SubProps>(
|
|
|
|
|
(props, ref) => {
|
2024-02-26 14:41:15 +01:00
|
|
|
const [inputValue, setInputValue] = React.useState<string>();
|
2023-10-03 17:02:23 +02:00
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
const options = React.useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
props.options.filter(
|
|
|
|
|
getMultiOptionsFilter(props.selectedItems, inputValue),
|
|
|
|
|
),
|
|
|
|
|
[props.selectedItems, inputValue],
|
|
|
|
|
);
|
|
|
|
|
const [hasInputFocused, setHasInputFocused] = useState(false);
|
|
|
|
|
const useMultipleSelectionReturn = useMultipleSelection({
|
|
|
|
|
selectedItems: props.selectedItems,
|
|
|
|
|
onStateChange({ selectedItems: newSelectedItems, type }) {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case useMultipleSelection.stateChangeTypes
|
|
|
|
|
.SelectedItemKeyDownBackspace:
|
|
|
|
|
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
|
|
|
|
|
case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
|
|
|
|
|
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
|
|
|
|
|
props.onSelectedItemsChange(newSelectedItems ?? []);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const downshiftReturn = useCombobox({
|
|
|
|
|
items: options,
|
|
|
|
|
itemToString: optionToString,
|
|
|
|
|
defaultHighlightedIndex: 0, // after selection, highlight the first item.
|
|
|
|
|
selectedItem: null, // Important, without this we are not able to re-select the last removed option.
|
|
|
|
|
stateReducer: (state, actionAndChanges) => {
|
|
|
|
|
const { changes, type } = actionAndChanges;
|
|
|
|
|
switch (type) {
|
|
|
|
|
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
|
|
|
|
case useCombobox.stateChangeTypes.ItemClick:
|
|
|
|
|
return {
|
|
|
|
|
...changes,
|
|
|
|
|
isOpen: true, // keep the menu open after selection.
|
|
|
|
|
highlightedIndex: 0, // with the first option highlighted.
|
|
|
|
|
};
|
|
|
|
|
default:
|
|
|
|
|
return changes;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onStateChange: ({
|
|
|
|
|
inputValue: newInputValue,
|
|
|
|
|
type,
|
|
|
|
|
selectedItem: newSelectedItem,
|
|
|
|
|
}) => {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
|
|
|
|
case useCombobox.stateChangeTypes.ItemClick:
|
|
|
|
|
case useCombobox.stateChangeTypes.InputBlur:
|
|
|
|
|
if (newSelectedItem && !newSelectedItem.disabled) {
|
|
|
|
|
props.onSelectedItemsChange([
|
|
|
|
|
...props.selectedItems,
|
|
|
|
|
newSelectedItem,
|
|
|
|
|
]);
|
2024-02-26 14:41:15 +01:00
|
|
|
setInputValue(undefined);
|
2023-10-03 17:02:23 +02:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case useCombobox.stateChangeTypes.InputChange:
|
2024-02-26 14:41:15 +01:00
|
|
|
setInputValue(newInputValue);
|
2023-10-03 17:02:23 +02:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
isItemDisabled: (item) => !!item.disabled,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const inputProps = downshiftReturn.getInputProps({
|
|
|
|
|
...useMultipleSelectionReturn.getDropdownProps({
|
|
|
|
|
preventKeyAction: downshiftReturn.isOpen,
|
|
|
|
|
ref: inputRef,
|
|
|
|
|
disabled: props.disabled,
|
|
|
|
|
}),
|
|
|
|
|
value: inputValue,
|
|
|
|
|
});
|
|
|
|
|
// We want to extend the default behavior of the input onKeyDown.
|
|
|
|
|
const { onKeyDown } = inputProps;
|
|
|
|
|
inputProps.onKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
|
|
|
|
switch (event.code) {
|
|
|
|
|
case "Backspace":
|
|
|
|
|
if (!inputValue) {
|
|
|
|
|
props.onSelectedItemsChange(props.selectedItems.slice(0, -1));
|
2023-06-12 14:27:40 +02:00
|
|
|
}
|
|
|
|
|
}
|
2023-10-03 17:02:23 +02:00
|
|
|
onKeyDown?.(event);
|
|
|
|
|
};
|
2023-06-12 14:27:40 +02:00
|
|
|
|
2023-10-03 17:02:23 +02:00
|
|
|
const [labelAsPlaceholder, setLabelAsPlaceholder] = useState(true);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (hasInputFocused || inputValue) {
|
|
|
|
|
setLabelAsPlaceholder(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setLabelAsPlaceholder(props.selectedItems.length === 0);
|
|
|
|
|
}, [props.selectedItems, hasInputFocused, inputValue]);
|
2023-06-12 14:27:40 +02:00
|
|
|
|
2023-10-03 17:02:23 +02:00
|
|
|
useImperativeHandle(ref, () => ({
|
|
|
|
|
blur: () => {
|
|
|
|
|
downshiftReturn.closeMenu();
|
|
|
|
|
inputRef.current?.blur();
|
|
|
|
|
},
|
|
|
|
|
}));
|
2023-06-12 14:27:40 +02:00
|
|
|
|
2024-02-26 14:41:15 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
props.onSearchInputChange?.({ target: { value: inputValue } });
|
|
|
|
|
}, [inputValue]);
|
|
|
|
|
|
2023-10-03 17:02:23 +02:00
|
|
|
return (
|
|
|
|
|
<SelectMultiAux
|
|
|
|
|
{...props}
|
2023-11-21 16:51:32 +01:00
|
|
|
monoline={false}
|
2023-10-03 17:02:23 +02:00
|
|
|
options={options}
|
|
|
|
|
labelAsPlaceholder={labelAsPlaceholder}
|
|
|
|
|
selectedItems={props.selectedItems}
|
|
|
|
|
downshiftReturn={{
|
|
|
|
|
...downshiftReturn,
|
|
|
|
|
wrapperProps: {
|
|
|
|
|
onClick: () => {
|
|
|
|
|
inputRef.current?.focus();
|
2023-11-28 16:28:00 +01:00
|
|
|
// This is important because if we don't check that: when clicking on the toggle button
|
|
|
|
|
// when the menu is open, it will close and reopen immediately.
|
|
|
|
|
if (!downshiftReturn.isOpen) {
|
|
|
|
|
downshiftReturn.openMenu();
|
|
|
|
|
}
|
2023-10-03 17:02:23 +02:00
|
|
|
},
|
2023-06-12 14:27:40 +02:00
|
|
|
},
|
2023-10-03 17:02:23 +02:00
|
|
|
toggleButtonProps: downshiftReturn.getToggleButtonProps(),
|
|
|
|
|
}}
|
|
|
|
|
useMultipleSelectionReturn={useMultipleSelectionReturn}
|
|
|
|
|
>
|
|
|
|
|
<span
|
|
|
|
|
className="c__select__inner__value__input"
|
|
|
|
|
data-value={inputValue}
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
{...inputProps}
|
|
|
|
|
onFocus={() => {
|
|
|
|
|
setHasInputFocused(true);
|
2024-01-31 17:55:41 +01:00
|
|
|
downshiftReturn.openMenu();
|
2023-10-03 17:02:23 +02:00
|
|
|
}}
|
|
|
|
|
onBlur={() => {
|
|
|
|
|
setHasInputFocused(false);
|
|
|
|
|
}}
|
|
|
|
|
size={4}
|
|
|
|
|
/>
|
|
|
|
|
</span>
|
|
|
|
|
</SelectMultiAux>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|