+ {loading ? (
+
+ ) : null}
+
+ {virtual
+ ? virtualizer.getVirtualItems().map((item) => (
+
+ {renderRow(item.index)}
+
+ ))
+ : visible.map((_, index) => renderRow(index))}
+
+ {!loading && error ? (
+
+ ) : null}
+ {!loading && !error && visible.length === 0 ? (
+
+ ) : null}
+
+ );
+}
diff --git a/frontend/src/v1/components/ui/select/MultiSelect.test.tsx b/frontend/src/v1/components/ui/select/MultiSelect.test.tsx
new file mode 100644
index 0000000000..9c33d688d1
--- /dev/null
+++ b/frontend/src/v1/components/ui/select/MultiSelect.test.tsx
@@ -0,0 +1,396 @@
+import { act, screen, waitFor } from "@testing-library/react";
+import { expectNoA11yViolations, renderV1 } from "@v1/test";
+import { expect, it, vi } from "vitest";
+import { MultiSelect, type MultiSelectProps } from "./MultiSelect";
+import type { SelectOption } from "./options";
+import type { LoadOptions } from "./useAsyncOptions";
+
+const TAGS: readonly SelectOption[] = [
+ { value: "invoice", label: "Invoice" },
+ { value: "receipt", label: "Receipt", description: "Paid" },
+ { value: "archived", label: "Archived", disabled: true },
+ { value: "contract", label: "Contract", keywords: ["agreement"] },
+];
+
+function input(): HTMLInputElement {
+ return screen.getByRole("combobox", { name: "Tags" }) as HTMLInputElement;
+}
+
+const chipNames = () =>
+ [...document.querySelectorAll('[data-slot="chip"]')].map(
+ (chip) => chip.textContent,
+ );
+
+const option = (name: string | RegExp) => screen.getByRole("option", { name });
+
+function setup(props: Partial