Files
cunningham/packages/react/src/components/Forms/Select/multi-searchable.tsx
Nathan Vasse 04ab3306e2 (react) add onSearchInputChange callback to searchable select
We want to be able to notify an event when the search term of the
searchable select gets updated.
2024-03-04 14:40:40 +01:00

174 lines
5.6 KiB
TypeScript

import React, {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import { useCombobox, useMultipleSelection } from "downshift";
import { optionToString } from ":/components/Forms/Select/mono-common";
import {
getMultiOptionsFilter,
SelectMultiAux,
SubProps,
} from ":/components/Forms/Select/multi-common";
import { SelectHandle } from ":/components/Forms/Select/index";
export const SelectMultiSearchable = forwardRef<SelectHandle, SubProps>(
(props, ref) => {
const [inputValue, setInputValue] = React.useState<string>();
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,
]);
setInputValue(undefined);
}
break;
case useCombobox.stateChangeTypes.InputChange:
setInputValue(newInputValue);
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));
}
}
onKeyDown?.(event);
};
const [labelAsPlaceholder, setLabelAsPlaceholder] = useState(true);
useEffect(() => {
if (hasInputFocused || inputValue) {
setLabelAsPlaceholder(false);
return;
}
setLabelAsPlaceholder(props.selectedItems.length === 0);
}, [props.selectedItems, hasInputFocused, inputValue]);
useImperativeHandle(ref, () => ({
blur: () => {
downshiftReturn.closeMenu();
inputRef.current?.blur();
},
}));
useEffect(() => {
props.onSearchInputChange?.({ target: { value: inputValue } });
}, [inputValue]);
return (
<SelectMultiAux
{...props}
monoline={false}
options={options}
labelAsPlaceholder={labelAsPlaceholder}
selectedItems={props.selectedItems}
downshiftReturn={{
...downshiftReturn,
wrapperProps: {
onClick: () => {
inputRef.current?.focus();
// 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();
}
},
},
toggleButtonProps: downshiftReturn.getToggleButtonProps(),
}}
useMultipleSelectionReturn={useMultipleSelectionReturn}
>
<span
className="c__select__inner__value__input"
data-value={inputValue}
>
<input
{...inputProps}
onFocus={() => {
setHasInputFocused(true);
downshiftReturn.openMenu();
}}
onBlur={() => {
setHasInputFocused(false);
}}
size={4}
/>
</span>
</SelectMultiAux>
);
},
);