From e165fa3a870db121f4f55ad85d34aed9b30185cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malinowski?=
Date: Tue, 26 Nov 2024 17:18:16 +0100 Subject: [PATCH 1/5] test(multi-select): add test suite --- tests/MultiSelect/MultiSelect.test.ts | 124 ++++++++++++++++++++++++++ tests/setup-tests.ts | 3 + 2 files changed, 127 insertions(+) create mode 100644 tests/MultiSelect/MultiSelect.test.ts diff --git a/tests/MultiSelect/MultiSelect.test.ts b/tests/MultiSelect/MultiSelect.test.ts new file mode 100644 index 0000000000..aed26b4a52 --- /dev/null +++ b/tests/MultiSelect/MultiSelect.test.ts @@ -0,0 +1,124 @@ +import { render, screen } from "@testing-library/svelte"; +import { MultiSelect } from "carbon-components-svelte"; +import { user } from "../setup-tests"; + +describe("MultiSelect sorts items correctly", () => { + /** Opens the dropdown. */ + const openMenu = async () => + await user.click( + await screen.findByLabelText("Open menu", { + selector: `[role="button"]`, + }), + ); + + /** Closes the dropdown. */ + const closeMenu = async () => + await user.click( + await screen.findByLabelText("Close menu", { + selector: `[role="button"]`, + }), + ); + + /** Toggles an option, identifying it by its `text` value. */ + const toggleOption = async (optionText: string) => + await user.click( + await screen.findByText((text) => text.trim() === optionText), + ); + + /** Fetches the `text` value of the nth option in the MultiSelect component. */ + const nthRenderedOptionText = (index: number) => + screen.queryAllByRole("option").at(index)?.textContent?.trim(); + + it("initially sorts items alphabetically", async () => { + render(MultiSelect, { + items: [ + { id: "1", text: "C" }, + { id: "3", text: "A" }, + { id: "2", text: "B" }, + ], + }); + + await openMenu(); + expect(nthRenderedOptionText(0)).toBe("A"); + expect(nthRenderedOptionText(1)).toBe("B"); + expect(nthRenderedOptionText(2)).toBe("C"); + }); + + it("immediately moves selected items to the top (with selectionFeedback: top)", async () => { + render(MultiSelect, { + items: [ + { id: "3", text: "C" }, + { id: "1", text: "A" }, + { id: "2", text: "B" }, + ], + selectionFeedback: "top", + }); + + await openMenu(); + expect(nthRenderedOptionText(0)).toBe("A"); + + await toggleOption("C"); + expect(nthRenderedOptionText(0)).toBe("C"); + + await toggleOption("C"); + expect(nthRenderedOptionText(0)).toBe("A"); + }); + + it("sorts newly-toggled items only after the dropdown is reoponed (with selectionFeedback: top-after-reopen)", async () => { + render(MultiSelect, { + items: [ + { id: "3", text: "C" }, + { id: "1", text: "A" }, + { id: "2", text: "B" }, + ], + }); + + // Initially, items should be sorted alphabetically. + await openMenu(); + expect(nthRenderedOptionText(0)).toBe("A"); + + // While the menu is still open, a newly-selected item should not move. + await toggleOption("C"); + expect(nthRenderedOptionText(0)).toBe("A"); + + // The newly-selected item should move after the menu is closed and + // re-opened. + await closeMenu(); + await openMenu(); + expect(nthRenderedOptionText(0)).toBe("C"); + + // A deselected item should not move while the dropdown is still open. + await toggleOption("C"); + expect(nthRenderedOptionText(0)).toBe("C"); + + // The deselected item should move after closing and re-opening the dropdown. + await closeMenu(); + await openMenu(); + expect(nthRenderedOptionText(0)).toBe("A"); + }); + + it("never moves selected items to the top (with selectionFeedback: fixed)", async () => { + render(MultiSelect, { + items: [ + { id: "3", text: "C" }, + { id: "1", text: "A" }, + { id: "2", text: "B" }, + ], + selectionFeedback: "fixed", + }); + + // Items should be sorted alphabetically. + await openMenu(); + expect(nthRenderedOptionText(0)).toBe("A"); + + // A newly-selected item should not move after the selection is made. + await toggleOption("C"); + expect(nthRenderedOptionText(0)).toBe("A"); + + // The newly-selected item also shouldn’t move after the dropdown is closed + // and reopened. + await closeMenu(); + await openMenu(); + expect(nthRenderedOptionText(0)).toBe("A"); + }); +}); diff --git a/tests/setup-tests.ts b/tests/setup-tests.ts index d494d2af52..c69af71048 100644 --- a/tests/setup-tests.ts +++ b/tests/setup-tests.ts @@ -3,4 +3,7 @@ import "@testing-library/jest-dom/vitest"; import { userEvent } from "@testing-library/user-event"; import "../css/all.css"; +// Mock scrollIntoView since it's not implemented in JSDOM +Element.prototype.scrollIntoView = vi.fn(); + export const user = userEvent.setup(); From c3a390f3fef072c6b736e33a85a2ae772df12e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malinowski?=
Date: Thu, 28 Nov 2024 20:50:15 +0100
Subject: [PATCH 2/5] fix(multi-select): fix sorting behavior
- Menu items are sorted when the component renders.
- With selectionFeedback: top, selected items are immediately pinned to
the top.
- With selectionFeedback: top-after-reopen, selected items are pinned to the top only
after the dropdown is closed.
- With selectionFeedback: fixed, selected items are never pinned to the
top.
Fixes #2066
---
src/MultiSelect/MultiSelect.svelte | 25 +++++++++++++++++++------
1 file changed, 19 insertions(+), 6 deletions(-)
diff --git a/src/MultiSelect/MultiSelect.svelte b/src/MultiSelect/MultiSelect.svelte
index abbdf7721f..8fa8abed54 100644
--- a/src/MultiSelect/MultiSelect.svelte
+++ b/src/MultiSelect/MultiSelect.svelte
@@ -243,11 +243,11 @@
}
});
- $: menuId = `menu-${id}`;
- $: inline = type === "inline";
- $: ariaLabel = $$props["aria-label"] || "Choose an item";
- $: sortedItems = (() => {
- if (selectionFeedback === "top" && selectedIds.length > 0) {
+ function sort() {
+ if (
+ selectionFeedback === "top" ||
+ selectionFeedback === "top-after-reopen"
+ ) {
const checkedItems = items
.filter((item) => selectedIds.includes(item.id))
.map((item) => ({ ...item, checked: true }));
@@ -269,7 +269,20 @@
checked: selectedIds.includes(item.id),
}))
.sort(sortItem);
- })();
+ }
+
+ let sortedItems = sort();
+
+ $: menuId = `menu-${id}`;
+ $: inline = type === "inline";
+ $: ariaLabel = $$props["aria-label"] || "Choose an item";
+ $: if (
+ selectedIds &&
+ (selectionFeedback === "top" ||
+ (selectionFeedback === "top-after-reopen" && open === false))
+ ) {
+ sortedItems = sort();
+ }
$: checked = sortedItems.filter(({ checked }) => checked);
$: unchecked = sortedItems.filter(({ checked }) => !checked);
$: filteredItems = sortedItems.filter((item) => filterItem(item, value));
From 765ffc88eb39a02dbe0b78cc9e4f1cc7b0c4077c Mon Sep 17 00:00:00 2001
From: Eric Liu