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 } from "downshift";
|
2023-10-06 16:54:55 +02:00
|
|
|
import classNames from "classnames";
|
2023-06-12 14:27:40 +02:00
|
|
|
import { useCunningham } from ":/components/Provider";
|
|
|
|
|
import {
|
|
|
|
|
getOptionsFilter,
|
|
|
|
|
optionToString,
|
2023-09-15 11:00:44 +02:00
|
|
|
optionToValue,
|
2023-06-12 14:27:40 +02:00
|
|
|
SelectMonoAux,
|
|
|
|
|
SubProps,
|
|
|
|
|
} from ":/components/Forms/Select/mono-common";
|
2023-10-04 15:48:22 +02:00
|
|
|
import { SelectHandle } from ":/components/Forms/Select";
|
2023-10-06 16:54:55 +02:00
|
|
|
import { isOptionWithRender } from ":/components/Forms/Select/utils";
|
2023-06-12 14:27:40 +02:00
|
|
|
|
2023-10-03 17:02:23 +02:00
|
|
|
export const SelectMonoSearchable = forwardRef<SelectHandle, SubProps>(
|
2023-10-06 16:54:55 +02:00
|
|
|
({ showLabelWhenSelected = true, ...props }, ref) => {
|
2023-10-03 17:02:23 +02:00
|
|
|
const { t } = useCunningham();
|
|
|
|
|
const [optionsToDisplay, setOptionsToDisplay] = useState(props.options);
|
|
|
|
|
const [hasInputFocused, setHasInputFocused] = useState(false);
|
|
|
|
|
const [inputFilter, setInputFilter] = useState<string>();
|
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
const downshiftReturn = useCombobox({
|
|
|
|
|
...props.downshiftProps,
|
|
|
|
|
items: optionsToDisplay,
|
|
|
|
|
itemToString: optionToString,
|
|
|
|
|
onInputValueChange: (e) => {
|
|
|
|
|
setInputFilter(e.inputValue);
|
|
|
|
|
if (!e.inputValue) {
|
|
|
|
|
downshiftReturn.selectItem(null);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const [labelAsPlaceholder, setLabelAsPlaceholder] = useState(
|
|
|
|
|
!downshiftReturn.selectedItem,
|
|
|
|
|
);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (hasInputFocused || downshiftReturn.inputValue) {
|
|
|
|
|
setLabelAsPlaceholder(false);
|
|
|
|
|
return;
|
2023-06-12 14:27:40 +02:00
|
|
|
}
|
2023-10-03 17:02:23 +02:00
|
|
|
setLabelAsPlaceholder(!downshiftReturn.selectedItem);
|
|
|
|
|
}, [
|
|
|
|
|
downshiftReturn.selectedItem,
|
|
|
|
|
hasInputFocused,
|
|
|
|
|
downshiftReturn.inputValue,
|
|
|
|
|
]);
|
2023-06-12 14:27:40 +02:00
|
|
|
|
2023-10-03 17:02:23 +02:00
|
|
|
// When component is controlled, this useEffect will update the local selected item.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (inputFilter) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-09-25 16:03:54 +02:00
|
|
|
|
2023-10-03 17:02:23 +02:00
|
|
|
const selectedItem = downshiftReturn.selectedItem
|
|
|
|
|
? optionToValue(downshiftReturn.selectedItem)
|
|
|
|
|
: undefined;
|
2023-09-25 16:03:54 +02:00
|
|
|
|
2023-10-03 17:02:23 +02:00
|
|
|
const optionToSelect = props.options.find(
|
|
|
|
|
(option) => optionToValue(option) === props.value,
|
|
|
|
|
);
|
2023-09-25 16:03:54 +02:00
|
|
|
|
2023-10-03 17:02:23 +02:00
|
|
|
// Already selected
|
|
|
|
|
if (optionToSelect && selectedItem === props.value) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-09-25 16:03:54 +02:00
|
|
|
|
2023-10-03 17:02:23 +02:00
|
|
|
downshiftReturn.selectItem(optionToSelect ?? null);
|
|
|
|
|
}, [props.value, props.options, inputFilter]);
|
2023-09-15 11:00:44 +02:00
|
|
|
|
2023-10-03 17:02:23 +02:00
|
|
|
// Even there is already a value selected, when opening the combobox menu we want to display all available choices.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (downshiftReturn.isOpen) {
|
|
|
|
|
setOptionsToDisplay(
|
|
|
|
|
inputFilter
|
|
|
|
|
? props.options.filter(getOptionsFilter(inputFilter))
|
|
|
|
|
: props.options,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
setInputFilter(undefined);
|
|
|
|
|
}
|
|
|
|
|
}, [downshiftReturn.isOpen, props.options, inputFilter]);
|
|
|
|
|
|
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
|
|
|
blur: () => {
|
|
|
|
|
downshiftReturn.closeMenu();
|
|
|
|
|
inputRef.current?.blur();
|
|
|
|
|
},
|
|
|
|
|
}));
|
2023-09-19 17:15:58 +02:00
|
|
|
|
2024-02-26 14:41:15 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
props.onSearchInputChange?.({ target: { value: inputFilter } });
|
|
|
|
|
}, [inputFilter]);
|
|
|
|
|
|
2023-10-03 17:02:23 +02:00
|
|
|
const onInputBlur = () => {
|
|
|
|
|
setHasInputFocused(false);
|
|
|
|
|
if (downshiftReturn.selectedItem) {
|
|
|
|
|
// Here the goal is to make sure that when the input in blurred then the input value
|
|
|
|
|
// has exactly the selectedItem label. Which is not the case by default.
|
|
|
|
|
downshiftReturn.selectItem(downshiftReturn.selectedItem);
|
|
|
|
|
} else {
|
|
|
|
|
// We want the input to be empty when no item is selected.
|
|
|
|
|
downshiftReturn.setInputValue("");
|
|
|
|
|
}
|
|
|
|
|
};
|
2023-09-19 17:15:58 +02:00
|
|
|
|
2023-10-03 17:02:23 +02:00
|
|
|
const inputProps = downshiftReturn.getInputProps({
|
|
|
|
|
ref: inputRef,
|
|
|
|
|
disabled: props.disabled,
|
|
|
|
|
});
|
2023-06-12 14:27:40 +02:00
|
|
|
|
2023-10-06 16:54:55 +02:00
|
|
|
const renderCustomSelectedOption = !showLabelWhenSelected;
|
|
|
|
|
|
2023-10-03 17:02:23 +02:00
|
|
|
return (
|
|
|
|
|
<SelectMonoAux
|
|
|
|
|
{...props}
|
|
|
|
|
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({
|
|
|
|
|
disabled: props.disabled,
|
|
|
|
|
"aria-label": t("components.forms.select.toggle_button_aria_label"),
|
|
|
|
|
}),
|
2023-06-12 14:27:40 +02:00
|
|
|
}}
|
2023-10-03 17:02:23 +02:00
|
|
|
labelAsPlaceholder={labelAsPlaceholder}
|
|
|
|
|
options={optionsToDisplay}
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
{...inputProps}
|
2023-10-06 16:54:55 +02:00
|
|
|
className={classNames({
|
|
|
|
|
"c__select__inner__value__input--hidden":
|
|
|
|
|
renderCustomSelectedOption && !hasInputFocused,
|
|
|
|
|
})}
|
2023-10-03 17:02:23 +02:00
|
|
|
onFocus={() => {
|
|
|
|
|
setHasInputFocused(true);
|
|
|
|
|
}}
|
|
|
|
|
onBlur={() => {
|
|
|
|
|
onInputBlur();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2023-10-06 16:54:55 +02:00
|
|
|
|
|
|
|
|
{renderCustomSelectedOption &&
|
|
|
|
|
!hasInputFocused &&
|
|
|
|
|
downshiftReturn.selectedItem &&
|
|
|
|
|
isOptionWithRender(downshiftReturn.selectedItem) &&
|
|
|
|
|
downshiftReturn.selectedItem.render()}
|
2023-10-03 17:02:23 +02:00
|
|
|
</SelectMonoAux>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|