Skip to content

Commit c8c987e

Browse files
Fix MultiSelect component to not set newlySelected incorrectly; also add complementing newlyRemoved
1 parent 62011cf commit c8c987e

3 files changed

Lines changed: 129 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
3333
- new icons:
3434
- `state-confirmed-all`
3535
- `state-declined-all`
36+
- `<MultiSuggestField />`
37+
- `MultiSuggestFieldSelectionProps` provides `newlyRemoved` for callbacks set when removing a selected item
3638

3739
### Fixed
3840

@@ -57,6 +59,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
5759
- use the latest provided `onChange` function
5860
- `<TextField />`, `<TextArea />`
5961
- fix emoji false-positives in invisible character detection
62+
- `<MultiSuggestField />`
63+
- `onSelection` now sets `newlySelected` only for add actions and no longer sets it to the last element
6064

6165
### Changed
6266

src/components/MultiSelect/MultiSelect.tsx

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222

2323
export interface MultiSuggestFieldSelectionProps<T> {
2424
newlySelected?: T;
25+
newlyRemoved?: T;
2526
selectedItems: T[];
2627
createdItems: Partial<T>[];
2728
}
@@ -178,6 +179,11 @@ export function MultiSuggestField<T>({
178179
intent,
179180
...otherMultiSelectProps
180181
}: MultiSuggestFieldProps<T>) {
182+
type SelectionChange =
183+
| { type: "selected"; item: T }
184+
| { type: "removed"; item: T }
185+
| { type: "none" };
186+
181187
// Options created by a user
182188
const createdItems = useRef<T[]>([]);
183189
// Options passed ouside (f.e. from the backend)
@@ -199,6 +205,7 @@ export function MultiSuggestField<T>({
199205
query?: string;
200206
timeoutId?: number;
201207
}>({});
208+
const selectionChange = useRef<SelectionChange>({ type: "none" });
202209

203210
/** Update external items when they change
204211
* e.g for auto-complete when query change
@@ -209,11 +216,21 @@ export function MultiSuggestField<T>({
209216
}, [items.map((item) => itemId(item)).join("|")]);
210217

211218
React.useEffect(() => {
212-
onSelection?.({
213-
newlySelected: selectedItems.slice(-1)[0],
219+
const selectionParams: MultiSuggestFieldSelectionProps<T> = {
214220
createdItems: createdItems.current,
215221
selectedItems,
216-
});
222+
};
223+
224+
if (selectionChange.current.type === "selected") {
225+
selectionParams.newlySelected = selectionChange.current.item;
226+
}
227+
228+
if (selectionChange.current.type === "removed") {
229+
selectionParams.newlyRemoved = selectionChange.current.item;
230+
}
231+
232+
onSelection?.(selectionParams);
233+
selectionChange.current = { type: "none" };
217234
}, [
218235
onSelection,
219236
selectedItems.map((item) => itemId(item)).join("|"),
@@ -228,6 +245,7 @@ export function MultiSuggestField<T>({
228245
return;
229246
}
230247

248+
selectionChange.current = { type: "none" };
231249
setSelectedItems(externalSelectedItems);
232250
}, [externalSelectedItems?.map((item) => itemId(item)).join("|")]);
233251

@@ -268,8 +286,13 @@ export function MultiSuggestField<T>({
268286
* @param matcher
269287
*/
270288
const removeItemSelection = (matcher: string) => {
271-
const filteredItems = selectedItems.filter((item) => itemId(item) !== matcher);
272-
setSelectedItems(filteredItems);
289+
setSelectedItems((items) => {
290+
const removedItem = items.find((item) => itemId(item) === matcher);
291+
292+
selectionChange.current = removedItem ? { type: "removed", item: removedItem } : { type: "none" };
293+
294+
return items.filter((item) => itemId(item) !== matcher);
295+
});
273296
};
274297

275298
const defaultFilterPredicate = (item: T, query: string) => {
@@ -286,6 +309,7 @@ export function MultiSuggestField<T>({
286309
if (itemHasBeenSelectedAlready(itemId(item))) {
287310
removeItemSelection(itemId(item));
288311
} else {
312+
selectionChange.current = { type: "selected", item };
289313
setSelectedItems((items) => [...items, item]);
290314
}
291315

@@ -365,6 +389,7 @@ export function MultiSuggestField<T>({
365389
const handleClear = () => {
366390
requestState.current.query = "";
367391

392+
selectionChange.current = { type: "none" };
368393
setSelectedItems([]);
369394
setFilteredItems([...externalItems, ...createdItems.current]);
370395
};
@@ -375,7 +400,13 @@ export function MultiSuggestField<T>({
375400
* @param index
376401
*/
377402
const removeTagFromSelectionViaIndex = (_label: React.ReactNode, index: number) => {
378-
setSelectedItems([...selectedItems.slice(0, index), ...selectedItems.slice(index + 1)]);
403+
setSelectedItems((items) => {
404+
const removedItem = items[index];
405+
406+
selectionChange.current = removedItem ? { type: "removed", item: removedItem } : { type: "none" };
407+
408+
return [...items.slice(0, index), ...items.slice(index + 1)];
409+
});
379410
};
380411

381412
/**

src/components/MultiSuggestField/tests/MultiSuggestField.test.tsx

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,86 @@ describe("MultiSuggestField", () => {
261261
});
262262
});
263263

264+
it("should set newlySelected only when an item is added", async () => {
265+
const onSelection = jest.fn();
266+
const initiallySelected = predefinedNotControlledValues.args.selectedItems;
267+
268+
const { container } = render(
269+
<MultiSuggestField
270+
{...dropdownOnFocus.args}
271+
items={items}
272+
selectedItems={initiallySelected}
273+
onSelection={onSelection}
274+
/>
275+
);
276+
277+
await waitFor(() => {
278+
expect(onSelection).toHaveBeenCalledWith({
279+
createdItems: [],
280+
selectedItems: initiallySelected,
281+
});
282+
});
283+
284+
onSelection.mockClear();
285+
286+
const [inputContainer] = container.getElementsByClassName("eccgui-multiselect");
287+
const [input] = inputContainer.getElementsByTagName("input");
288+
289+
fireEvent.click(input);
290+
291+
await waitFor(() => {
292+
const listbox = screen.getByRole("listbox");
293+
const menuItems = listbox.getElementsByClassName("eccgui-menu__item");
294+
295+
expect(menuItems.length).toBe(dropdownOnFocus.args.items.length);
296+
297+
fireEvent.click(menuItems[2]);
298+
});
299+
300+
await waitFor(() => {
301+
expect(onSelection).toHaveBeenLastCalledWith({
302+
createdItems: [],
303+
newlySelected: items[2],
304+
selectedItems: [...initiallySelected, items[2]],
305+
});
306+
});
307+
});
308+
309+
it("should set newlyRemoved only when an item is removed", async () => {
310+
const onSelection = jest.fn();
311+
const initiallySelected = predefinedNotControlledValues.args.selectedItems;
312+
313+
const { container } = render(
314+
<MultiSuggestField
315+
{...predefinedNotControlledValues.args}
316+
selectedItems={initiallySelected}
317+
onSelection={onSelection}
318+
/>
319+
);
320+
321+
await waitFor(() => {
322+
expect(onSelection).toHaveBeenCalledWith({
323+
createdItems: [],
324+
selectedItems: initiallySelected,
325+
});
326+
});
327+
328+
onSelection.mockClear();
329+
330+
const [firstTag] = Array.from(container.querySelectorAll("span[data-tag-index]"));
331+
const removeTagButton = firstTag.querySelector("button");
332+
333+
fireEvent.click(removeTagButton!);
334+
335+
await waitFor(() => {
336+
expect(onSelection).toHaveBeenLastCalledWith({
337+
createdItems: [],
338+
newlyRemoved: initiallySelected[0],
339+
selectedItems: initiallySelected.slice(1),
340+
});
341+
});
342+
});
343+
264344
it("should filter items by custom search function", async () => {
265345
const { container } = render(<MultiSuggestField {...CustomSearch.args} items={items} />);
266346

@@ -340,6 +420,13 @@ describe("MultiSuggestField", () => {
340420
expect(getByText(firstSelected)).toBeInTheDocument();
341421
expect(getByText(secondSelected)).toBeInTheDocument();
342422
});
423+
424+
await waitFor(() => {
425+
expect(onSelection).toHaveBeenCalledWith({
426+
createdItems: [],
427+
selectedItems: predefinedNotControlledValues.args.selectedItems,
428+
});
429+
});
343430
});
344431

345432
it("should call onSelection function with the selected items", async () => {
@@ -485,7 +572,6 @@ describe("MultiSuggestField", () => {
485572
await waitFor(() => {
486573
const expectedObject = {
487574
createdItems: [],
488-
newlySelected: items.at(-1),
489575
selectedItems: items,
490576
};
491577
expect(onSelection).toHaveBeenCalledWith(expectedObject);
@@ -513,7 +599,6 @@ describe("MultiSuggestField", () => {
513599
await waitFor(() => {
514600
const expectedObject = {
515601
createdItems: [],
516-
newlySelected: items.at(-1),
517602
selectedItems: items,
518603
};
519604
expect(onSelection).toHaveBeenCalledWith(expectedObject);
@@ -536,7 +621,7 @@ describe("MultiSuggestField", () => {
536621

537622
const expectedObject = {
538623
createdItems: [],
539-
newlySelected: selected.at(-1),
624+
newlyRemoved: items[i],
540625
selectedItems: selected,
541626
};
542627

0 commit comments

Comments
 (0)