(react) add ref to Select

We encountered a use-case where we needed to blur the select programatically
but the component wasn't offering any way to do that.
This commit is contained in:
Nathan Vasse
2023-10-03 17:02:23 +02:00
committed by NathanVss
parent d647a77c58
commit 1c7a114b6e
12 changed files with 869 additions and 474 deletions

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { forwardRef, useImperativeHandle, useRef } from "react";
import { useMultipleSelection, useSelect } from "downshift";
import {
getMultiOptionsFilter,
@@ -6,95 +6,110 @@ import {
SubProps,
} from ":/components/Forms/Select/multi-common";
import { optionToString } from ":/components/Forms/Select/mono-common";
import { SelectHandle } from ":/components/Forms/Select/index";
export const SelectMultiSimple = (props: SubProps) => {
const options = React.useMemo(
() => props.options.filter(getMultiOptionsFilter(props.selectedItems, "")),
[props.selectedItems],
);
export const SelectMultiSimple = forwardRef<SelectHandle, SubProps>(
(props, ref) => {
const options = React.useMemo(
() =>
props.options.filter(getMultiOptionsFilter(props.selectedItems, "")),
[props.selectedItems],
);
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 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 = useSelect({
items: options,
itemToString: optionToString,
selectedItem: null, // Important, without this we are not able to re-select the last removed option.
defaultHighlightedIndex: 0, // after selection, highlight the first item.
stateReducer: (state, actionAndChanges) => {
const { changes, type } = actionAndChanges;
switch (type) {
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
case useSelect.stateChangeTypes.ItemClick:
return {
...changes,
isOpen: true, // keep the menu open after selection.
highlightedIndex: 0, // with the first option highlighted.
};
}
return changes;
},
onStateChange: ({ type, selectedItem: newSelectedItem }) => {
switch (type) {
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
case useSelect.stateChangeTypes.ItemClick:
if (newSelectedItem) {
props.onSelectedItemsChange([
...props.selectedItems,
newSelectedItem,
]);
}
break;
default:
break;
}
},
isItemDisabled: (item) => !!item.disabled,
});
return (
<SelectMultiAux
{...props}
options={options}
labelAsPlaceholder={props.selectedItems.length === 0}
selectedItems={props.selectedItems}
downshiftReturn={{
...downshiftReturn,
wrapperProps: {
onClick: () => {
if (!props.disabled) {
downshiftReturn.toggleMenu();
const downshiftReturn = useSelect({
items: options,
itemToString: optionToString,
selectedItem: null, // Important, without this we are not able to re-select the last removed option.
defaultHighlightedIndex: 0, // after selection, highlight the first item.
stateReducer: (state, actionAndChanges) => {
const { changes, type } = actionAndChanges;
switch (type) {
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
case useSelect.stateChangeTypes.ItemClick:
return {
...changes,
isOpen: true, // keep the menu open after selection.
highlightedIndex: 0, // with the first option highlighted.
};
}
return changes;
},
onStateChange: ({ type, selectedItem: newSelectedItem }) => {
switch (type) {
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
case useSelect.stateChangeTypes.ItemClick:
if (newSelectedItem) {
props.onSelectedItemsChange([
...props.selectedItems,
newSelectedItem,
]);
}
break;
default:
break;
}
},
isItemDisabled: (item) => !!item.disabled,
});
const toggleRef = useRef<HTMLElement>(null);
useImperativeHandle(ref, () => ({
blur: () => {
downshiftReturn.closeMenu();
toggleRef.current?.blur();
},
}));
return (
<SelectMultiAux
{...props}
options={options}
labelAsPlaceholder={props.selectedItems.length === 0}
selectedItems={props.selectedItems}
downshiftReturn={{
...downshiftReturn,
wrapperProps: {
onClick: () => {
if (!props.disabled) {
downshiftReturn.toggleMenu();
}
},
},
},
toggleButtonProps: downshiftReturn.getToggleButtonProps({
...useMultipleSelectionReturn.getDropdownProps({
preventKeyAction: downshiftReturn.isOpen,
toggleButtonProps: downshiftReturn.getToggleButtonProps({
...useMultipleSelectionReturn.getDropdownProps({
preventKeyAction: downshiftReturn.isOpen,
ref: toggleRef,
}),
disabled: props.disabled,
onClick: (e: React.MouseEvent): void => {
// As the wrapper also has an onClick handler, we need to stop the event propagation here on it will toggle
// twice the menu opening which will ... do nothing :).
e.stopPropagation();
},
}),
disabled: props.disabled,
onClick: (e: React.MouseEvent): void => {
// As the wrapper also has an onClick handler, we need to stop the event propagation here on it will toggle
// twice the menu opening which will ... do nothing :).
e.stopPropagation();
},
}),
}}
useMultipleSelectionReturn={useMultipleSelectionReturn}
/>
);
};
}}
useMultipleSelectionReturn={useMultipleSelectionReturn}
/>
);
},
);