(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,10 @@
import React, { useEffect, useRef, useState } from "react";
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import { useCombobox } from "downshift";
import { useCunningham } from ":/components/Provider";
import {
@@ -8,119 +14,129 @@ import {
SelectMonoAux,
SubProps,
} from ":/components/Forms/Select/mono-common";
import { SelectHandle } from ":/components/Forms/Select/index";
export const SelectMonoSearchable = (props: SubProps) => {
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;
}
setLabelAsPlaceholder(!downshiftReturn.selectedItem);
}, [
downshiftReturn.selectedItem,
hasInputFocused,
downshiftReturn.inputValue,
]);
// When component is controlled, this useEffect will update the local selected item.
useEffect(() => {
if (inputFilter) {
return;
}
const selectedItem = downshiftReturn.selectedItem
? optionToValue(downshiftReturn.selectedItem)
: undefined;
const optionToSelect = props.options.find(
(option) => optionToValue(option) === props.value,
export const SelectMonoSearchable = forwardRef<SelectHandle, SubProps>(
(props, ref) => {
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;
}
setLabelAsPlaceholder(!downshiftReturn.selectedItem);
}, [
downshiftReturn.selectedItem,
hasInputFocused,
downshiftReturn.inputValue,
]);
// Already selected
if (optionToSelect && selectedItem === props.value) {
return;
}
// When component is controlled, this useEffect will update the local selected item.
useEffect(() => {
if (inputFilter) {
return;
}
downshiftReturn.selectItem(optionToSelect ?? null);
}, [props.value, props.options, inputFilter]);
const selectedItem = downshiftReturn.selectedItem
? optionToValue(downshiftReturn.selectedItem)
: undefined;
// 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,
const optionToSelect = props.options.find(
(option) => optionToValue(option) === props.value,
);
} else {
setInputFilter(undefined);
}
}, [downshiftReturn.isOpen, props.options, inputFilter]);
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("");
}
};
// Already selected
if (optionToSelect && selectedItem === props.value) {
return;
}
const inputProps = downshiftReturn.getInputProps({
ref: inputRef,
disabled: props.disabled,
});
downshiftReturn.selectItem(optionToSelect ?? null);
}, [props.value, props.options, inputFilter]);
return (
<SelectMonoAux
{...props}
downshiftReturn={{
...downshiftReturn,
wrapperProps: {
onClick: () => {
inputRef.current?.focus();
downshiftReturn.openMenu();
// 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();
},
}));
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("");
}
};
const inputProps = downshiftReturn.getInputProps({
ref: inputRef,
disabled: props.disabled,
});
return (
<SelectMonoAux
{...props}
downshiftReturn={{
...downshiftReturn,
wrapperProps: {
onClick: () => {
inputRef.current?.focus();
downshiftReturn.openMenu();
},
},
},
toggleButtonProps: downshiftReturn.getToggleButtonProps({
disabled: props.disabled,
"aria-label": t("components.forms.select.toggle_button_aria_label"),
}),
}}
labelAsPlaceholder={labelAsPlaceholder}
options={optionsToDisplay}
>
<input
{...inputProps}
onFocus={() => {
setHasInputFocused(true);
toggleButtonProps: downshiftReturn.getToggleButtonProps({
disabled: props.disabled,
"aria-label": t("components.forms.select.toggle_button_aria_label"),
}),
}}
onBlur={() => {
onInputBlur();
}}
/>
</SelectMonoAux>
);
};
labelAsPlaceholder={labelAsPlaceholder}
options={optionsToDisplay}
>
<input
{...inputProps}
onFocus={() => {
setHasInputFocused(true);
}}
onBlur={() => {
onInputBlur();
}}
/>
</SelectMonoAux>
);
},
);