🐛(react) fix Select mono selected item update label
When we were updating the label from the options array of the selected item, the field was still showing this old value. Fixes #316
This commit is contained in:
5
.changeset/breezy-phones-repeat.md
Normal file
5
.changeset/breezy-phones-repeat.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@openfun/cunningham-react": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix Select mono selected item update label
|
||||||
@@ -6,7 +6,7 @@ import { FieldProps } from ":/components/Forms/Field";
|
|||||||
export * from ":/components/Forms/Select/mono";
|
export * from ":/components/Forms/Select/mono";
|
||||||
export * from ":/components/Forms/Select/multi";
|
export * from ":/components/Forms/Select/multi";
|
||||||
|
|
||||||
type BaseOption = {
|
export type BaseOption = {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
render: () => ReactNode;
|
render: () => ReactNode;
|
||||||
|
|||||||
@@ -51,25 +51,17 @@ export const SelectMonoSearchable = forwardRef<SelectHandle, SubProps>(
|
|||||||
downshiftReturn.inputValue,
|
downshiftReturn.inputValue,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// When component is controlled, this useEffect will update the local selected item.
|
// Similar to: useKeepSelectedItemInSyncWithOptions ( see docs )
|
||||||
|
// The only difference is that it does not apply when there is an inputFilter. ( See below why )
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// If there is an inputFilter, using selectItem will trigger onInputValueChange that will sets inputFilter to
|
||||||
|
// empty, and then ignoring the existing filter and displaying all options.
|
||||||
if (inputFilter) {
|
if (inputFilter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedItem = downshiftReturn.selectedItem
|
|
||||||
? optionToValue(downshiftReturn.selectedItem)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const optionToSelect = props.options.find(
|
const optionToSelect = props.options.find(
|
||||||
(option) => optionToValue(option) === props.value,
|
(option) => optionToValue(option) === props.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Already selected
|
|
||||||
if (optionToSelect && selectedItem === props.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
downshiftReturn.selectItem(optionToSelect ?? null);
|
downshiftReturn.selectItem(optionToSelect ?? null);
|
||||||
}, [props.value, props.options, inputFilter]);
|
}, [props.value, props.options, inputFilter]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useSelect } from "downshift";
|
import { useSelect, UseSelectReturnValue } from "downshift";
|
||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -11,9 +11,27 @@ import {
|
|||||||
SelectMonoAux,
|
SelectMonoAux,
|
||||||
SubProps,
|
SubProps,
|
||||||
} from ":/components/Forms/Select/mono-common";
|
} from ":/components/Forms/Select/mono-common";
|
||||||
import { SelectHandle } from ":/components/Forms/Select";
|
import { Option, SelectHandle, SelectProps } from ":/components/Forms/Select";
|
||||||
import { SelectedOption } from ":/components/Forms/Select/utils";
|
import { SelectedOption } from ":/components/Forms/Select/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here we ensure that the selected item is always in sync with the options.
|
||||||
|
* Ex: If the selected options changes label we want to reflect that.
|
||||||
|
* @param downshiftReturn
|
||||||
|
* @param props
|
||||||
|
*/
|
||||||
|
const useKeepSelectedItemInSyncWithOptions = (
|
||||||
|
downshiftReturn: UseSelectReturnValue<Option>,
|
||||||
|
props: Pick<SelectProps, "value" | "options">,
|
||||||
|
) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const optionToSelect = props.options.find(
|
||||||
|
(option) => optionToValue(option) === props.value,
|
||||||
|
);
|
||||||
|
downshiftReturn.selectItem(optionToSelect ?? null);
|
||||||
|
}, [props.value, props.options]);
|
||||||
|
};
|
||||||
|
|
||||||
export const SelectMonoSimple = forwardRef<SelectHandle, SubProps>(
|
export const SelectMonoSimple = forwardRef<SelectHandle, SubProps>(
|
||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
const downshiftReturn = useSelect({
|
const downshiftReturn = useSelect({
|
||||||
@@ -22,23 +40,7 @@ export const SelectMonoSimple = forwardRef<SelectHandle, SubProps>(
|
|||||||
itemToString: optionToString,
|
itemToString: optionToString,
|
||||||
});
|
});
|
||||||
|
|
||||||
// When component is controlled, this useEffect will update the local selected item.
|
useKeepSelectedItemInSyncWithOptions(downshiftReturn, props);
|
||||||
useEffect(() => {
|
|
||||||
const selectedItem = downshiftReturn.selectedItem
|
|
||||||
? optionToValue(downshiftReturn.selectedItem)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const optionToSelect = props.options.find(
|
|
||||||
(option) => optionToValue(option) === props.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Already selected
|
|
||||||
if (optionToSelect && selectedItem === props.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
downshiftReturn.selectItem(optionToSelect ?? null);
|
|
||||||
}, [props.value, props.options]);
|
|
||||||
|
|
||||||
const wrapperRef = useRef<HTMLElement>(null);
|
const wrapperRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -756,7 +756,6 @@ describe("<Select/>", () => {
|
|||||||
expectMenuToBeOpen(menu);
|
expectMenuToBeOpen(menu);
|
||||||
|
|
||||||
expectOptions(["Paris", "Panama"]);
|
expectOptions(["Paris", "Panama"]);
|
||||||
|
|
||||||
myOptions.shift();
|
myOptions.shift();
|
||||||
|
|
||||||
// Rerender the select with the options mutated
|
// Rerender the select with the options mutated
|
||||||
@@ -1017,6 +1016,75 @@ describe("<Select/>", () => {
|
|||||||
await user.click(option);
|
await user.click(option);
|
||||||
expect(searchTerm).toBeUndefined();
|
expect(searchTerm).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updates the selected value label if the option label changes", async () => {
|
||||||
|
const myOptions = [
|
||||||
|
{
|
||||||
|
label: "Paris",
|
||||||
|
value: "paris",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Panama",
|
||||||
|
value: "panama",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "London",
|
||||||
|
value: "london",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Wrapper = ({ options }: { options: Option[] }) => {
|
||||||
|
const [value, setValue] = useState<string | number | undefined>(
|
||||||
|
"paris",
|
||||||
|
);
|
||||||
|
const [onChangeCounts, setOnChangeCounts] = useState(0);
|
||||||
|
return (
|
||||||
|
<CunninghamProvider>
|
||||||
|
<div>
|
||||||
|
<div>Value = {value}|</div>
|
||||||
|
<div>onChangeCounts = {onChangeCounts}|</div>
|
||||||
|
<Select
|
||||||
|
label="City"
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
setValue(e.target.value as string);
|
||||||
|
setOnChangeCounts(onChangeCounts + 1);
|
||||||
|
}}
|
||||||
|
searchable={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CunninghamProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { rerender } = render(<Wrapper options={myOptions} />, {
|
||||||
|
wrapper: CunninghamProvider,
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = screen.getByRole("combobox", {
|
||||||
|
name: "City",
|
||||||
|
});
|
||||||
|
expect(input).toHaveValue("Paris");
|
||||||
|
screen.getByText("Value = paris|");
|
||||||
|
screen.getByText("onChangeCounts = 0|");
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<Wrapper
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "Paname",
|
||||||
|
value: "paris",
|
||||||
|
},
|
||||||
|
...myOptions.slice(1),
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(input).toHaveValue("Paname"));
|
||||||
|
screen.getByText("Value = paris|");
|
||||||
|
screen.getByText("onChangeCounts = 0|");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Simple", () => {
|
describe("Simple", () => {
|
||||||
@@ -1842,6 +1910,72 @@ describe("<Select/>", () => {
|
|||||||
screen.getByText("onChangeCounts = 2|");
|
screen.getByText("onChangeCounts = 2|");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updates the selected value label if the option label changes", async () => {
|
||||||
|
const myOptions = [
|
||||||
|
{
|
||||||
|
label: "Paris",
|
||||||
|
value: "paris",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Panama",
|
||||||
|
value: "panama",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "London",
|
||||||
|
value: "london",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Wrapper = ({ options }: { options: Option[] }) => {
|
||||||
|
const [value, setValue] = useState<string | number | undefined>(
|
||||||
|
"paris",
|
||||||
|
);
|
||||||
|
const [onChangeCounts, setOnChangeCounts] = useState(0);
|
||||||
|
return (
|
||||||
|
<CunninghamProvider>
|
||||||
|
<div>
|
||||||
|
<div>Value = {value}|</div>
|
||||||
|
<div>onChangeCounts = {onChangeCounts}|</div>
|
||||||
|
<Select
|
||||||
|
label="City"
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
setValue(e.target.value as string);
|
||||||
|
setOnChangeCounts(onChangeCounts + 1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CunninghamProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { rerender } = render(<Wrapper options={myOptions} />, {
|
||||||
|
wrapper: CunninghamProvider,
|
||||||
|
});
|
||||||
|
|
||||||
|
const valueRendered = document.querySelector(".c__select__inner__value");
|
||||||
|
expect(valueRendered).toHaveTextContent("Paris");
|
||||||
|
screen.getByText("Value = paris|");
|
||||||
|
screen.getByText("onChangeCounts = 0|");
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<Wrapper
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "Paname",
|
||||||
|
value: "paris",
|
||||||
|
},
|
||||||
|
...myOptions.slice(1),
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(valueRendered).toHaveTextContent("Paname");
|
||||||
|
screen.getByText("Value = paris|");
|
||||||
|
screen.getByText("onChangeCounts = 0|");
|
||||||
|
});
|
||||||
|
|
||||||
it("blurs from ref", async () => {
|
it("blurs from ref", async () => {
|
||||||
const ref = createRef<SelectHandle>();
|
const ref = createRef<SelectHandle>();
|
||||||
render(
|
render(
|
||||||
|
|||||||
@@ -108,6 +108,35 @@ export const Controlled = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ControlledEdit = () => {
|
||||||
|
const [value, setValue] = useState(OPTIONS[0].value);
|
||||||
|
const [options, setOptions] = useState(OPTIONS);
|
||||||
|
|
||||||
|
const edit = () => {
|
||||||
|
setOptions([{ value: "woodbury", label: "EDITTED" }, ...OPTIONS.slice(1)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
Value: <span>{value}</span>
|
||||||
|
<Button onClick={edit}>Edit</Button>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
label="Select a city"
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
multi={false}
|
||||||
|
searchable={true}
|
||||||
|
onChange={(e) => {
|
||||||
|
setValue(e.target.value as string);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button onClick={() => setValue("")}>Reset</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const Overflow = {
|
export const Overflow = {
|
||||||
render: Template,
|
render: Template,
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user