@agilant/toga-blox 1.0.32 → 1.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/components/Dropdown/Dropdown.d.ts +4 -0
  2. package/dist/components/Dropdown/Dropdown.js +20 -0
  3. package/dist/components/Dropdown/Dropdown.stories.d.ts +8 -0
  4. package/dist/components/Dropdown/Dropdown.stories.js +110 -0
  5. package/dist/components/Dropdown/Dropdown.test.d.ts +1 -0
  6. package/dist/components/Dropdown/Dropdown.test.js +43 -0
  7. package/dist/components/Dropdown/Dropdown.types.d.ts +15 -0
  8. package/dist/components/Dropdown/Dropdown.types.js +1 -0
  9. package/dist/components/GenericList/GenericList.d.ts +2 -15
  10. package/dist/components/GenericList/GenericList.js +64 -51
  11. package/dist/components/GenericList/GenericList.stories.d.ts +8 -35
  12. package/dist/components/GenericList/GenericList.stories.js +46 -78
  13. package/dist/components/GenericList/GenericList.test.d.ts +1 -1
  14. package/dist/components/GenericList/GenericList.test.js +112 -22
  15. package/dist/components/GenericList/index.d.ts +2 -0
  16. package/dist/components/GenericList/index.js +2 -0
  17. package/dist/components/GenericList/types.d.ts +16 -0
  18. package/dist/components/GenericList/types.js +1 -0
  19. package/dist/components/Header/Header.stories.js +2 -4
  20. package/dist/components/Input/Input.d.ts +30 -3
  21. package/dist/components/Input/Input.js +70 -48
  22. package/dist/components/Input/Input.stories.js +3 -4
  23. package/dist/components/Input/Input.test.js +74 -42
  24. package/dist/components/InputAndCheck/InputAndCheck.d.ts +47 -0
  25. package/dist/components/InputAndCheck/InputAndCheck.js +74 -0
  26. package/dist/components/InputAndCheck/InputAndCheck.stories.d.ts +9 -0
  27. package/dist/components/InputAndCheck/InputAndCheck.stories.js +201 -0
  28. package/dist/components/InputAndCheck/InputAndCheck.test.d.ts +1 -0
  29. package/dist/components/InputAndCheck/InputAndCheck.test.js +307 -0
  30. package/dist/components/InputAndCheck/index.d.ts +0 -0
  31. package/dist/components/InputAndCheck/index.js +0 -0
  32. package/dist/components/InputAndCheck/types.d.ts +35 -0
  33. package/dist/components/InputAndCheck/types.js +1 -0
  34. package/dist/components/MagnifyingIcon/MagnifyingIcon.d.ts +4 -0
  35. package/dist/components/MagnifyingIcon/MagnifyingIcon.js +60 -0
  36. package/dist/components/MagnifyingIcon/MagnifyingIcon.stories.d.ts +9 -0
  37. package/dist/components/MagnifyingIcon/MagnifyingIcon.stories.js +72 -0
  38. package/dist/components/MagnifyingIcon/MagnifyingIcon.test.d.ts +1 -0
  39. package/dist/components/MagnifyingIcon/MagnifyingIcon.test.js +101 -0
  40. package/dist/components/MagnifyingIcon/index.d.ts +2 -0
  41. package/dist/components/MagnifyingIcon/index.js +2 -0
  42. package/dist/components/MagnifyingIcon/types.d.ts +20 -0
  43. package/dist/components/MagnifyingIcon/types.js +2 -0
  44. package/dist/components/MultiSelect/MultiSelect.d.ts +4 -0
  45. package/dist/components/MultiSelect/MultiSelect.js +30 -0
  46. package/dist/components/MultiSelect/MultiSelect.stories.d.ts +10 -0
  47. package/dist/components/MultiSelect/MultiSelect.stories.js +162 -0
  48. package/dist/components/MultiSelect/MultiSelect.test.d.ts +1 -0
  49. package/dist/components/MultiSelect/MultiSelect.test.js +107 -0
  50. package/dist/components/MultiSelect/MultiSelect.types.d.ts +28 -0
  51. package/dist/components/MultiSelect/MultiSelect.types.js +1 -0
  52. package/dist/components/Page/ViewPageTemplate.stories.js +2 -3
  53. package/dist/components/PrimaryTableHeader/PrimaryTableHeader.d.ts +3 -0
  54. package/dist/components/PrimaryTableHeader/PrimaryTableHeader.js +72 -0
  55. package/dist/components/PrimaryTableHeader/PrimaryTableHeader.stories.d.ts +4 -0
  56. package/dist/components/PrimaryTableHeader/PrimaryTableHeader.stories.js +99 -0
  57. package/dist/components/PrimaryTableHeader/PrimaryTableHeader.test.d.ts +1 -0
  58. package/dist/components/PrimaryTableHeader/PrimaryTableHeader.test.js +124 -0
  59. package/dist/components/PrimaryTableHeader/index.d.ts +0 -0
  60. package/dist/components/PrimaryTableHeader/index.js +0 -0
  61. package/dist/components/PrimaryTableHeader/types.d.ts +35 -0
  62. package/dist/components/PrimaryTableHeader/types.js +2 -0
  63. package/dist/components/SearchInput/SearchInput.d.ts +1 -2
  64. package/dist/components/SearchInput/SearchInput.js +61 -11
  65. package/dist/components/SearchInput/SearchInput.stories.d.ts +2 -4
  66. package/dist/components/SearchInput/SearchInput.stories.js +80 -93
  67. package/dist/components/SearchInput/SearchInput.types.d.ts +37 -24
  68. package/dist/components/SearchInput/SearchNumberInput.d.ts +31 -0
  69. package/dist/components/SearchInput/SearchNumberInput.js +60 -0
  70. package/dist/components/SearchInput/SearchTextInput.d.ts +24 -0
  71. package/dist/components/SearchInput/SearchTextInput.js +65 -0
  72. package/dist/components/SortArrowIcon/SortArrowIcon.d.ts +4 -0
  73. package/dist/components/SortArrowIcon/SortArrowIcon.js +12 -0
  74. package/dist/components/SortArrowIcon/SortArrowIcon.stories.d.ts +17 -0
  75. package/dist/components/SortArrowIcon/SortArrowIcon.stories.js +77 -0
  76. package/dist/components/SortArrowIcon/SortArrowIcon.test.d.ts +1 -0
  77. package/dist/components/SortArrowIcon/SortArrowIcon.test.js +44 -0
  78. package/dist/components/SortArrowIcon/index.d.ts +2 -0
  79. package/dist/components/SortArrowIcon/index.js +2 -0
  80. package/dist/components/SortArrowIcon/types.d.ts +15 -0
  81. package/dist/components/SortArrowIcon/types.js +1 -0
  82. package/dist/components/SortArrows/SortArrows.d.ts +3 -0
  83. package/dist/components/SortArrows/SortArrows.js +33 -0
  84. package/dist/components/SortArrows/SortArrows.stories.d.ts +7 -0
  85. package/dist/components/SortArrows/SortArrows.stories.js +41 -0
  86. package/dist/components/SortArrows/SortArrows.test.d.ts +1 -0
  87. package/dist/components/SortArrows/SortArrows.test.js +150 -0
  88. package/dist/components/SortArrows/index.d.ts +2 -0
  89. package/dist/components/SortArrows/index.js +2 -0
  90. package/dist/components/SortArrows/types.d.ts +21 -0
  91. package/dist/components/SortArrows/types.js +1 -0
  92. package/dist/components/SortArrows/useSortArrowsViewModel.d.ts +30 -0
  93. package/dist/components/SortArrows/useSortArrowsViewModel.js +114 -0
  94. package/dist/components/SortArrows/useSortArrowsViewModel.test.d.ts +1 -0
  95. package/dist/components/SortArrows/useSortArrowsViewModel.test.js +100 -0
  96. package/dist/components/TableCell/TableCell.d.ts +3 -0
  97. package/dist/components/TableCell/TableCell.js +13 -0
  98. package/dist/components/TableCell/TableCell.stories.d.ts +16 -0
  99. package/dist/components/TableCell/TableCell.stories.js +99 -0
  100. package/dist/components/TableCell/TableCell.test.d.ts +1 -0
  101. package/dist/components/TableCell/TableCell.test.js +84 -0
  102. package/dist/components/TableCell/index.d.ts +2 -0
  103. package/dist/components/TableCell/index.js +2 -0
  104. package/dist/components/TableCell/types.d.ts +12 -0
  105. package/dist/components/TableCell/types.js +1 -0
  106. package/dist/components/TableHeaderContent/TableHeaderContent.d.ts +3 -0
  107. package/dist/components/TableHeaderContent/TableHeaderContent.js +5 -0
  108. package/dist/components/TableHeaderContent/TableHeaderContent.stories.d.ts +6 -0
  109. package/dist/components/TableHeaderContent/TableHeaderContent.stories.js +62 -0
  110. package/dist/components/TableHeaderContent/TableHeaderContent.test.d.ts +1 -0
  111. package/dist/components/TableHeaderContent/TableHeaderContent.test.js +41 -0
  112. package/dist/components/TableHeaderContent/index.d.ts +0 -0
  113. package/dist/components/TableHeaderContent/index.js +0 -0
  114. package/dist/components/TableHeaderContent/types.d.ts +5 -0
  115. package/dist/components/TableHeaderContent/types.js +1 -0
  116. package/dist/components/TableHeaderInput/TableHeaderInput.d.ts +3 -0
  117. package/dist/components/TableHeaderInput/TableHeaderInput.js +80 -0
  118. package/dist/components/TableHeaderInput/TableHeaderInput.stories.d.ts +10 -0
  119. package/dist/components/TableHeaderInput/TableHeaderInput.stories.js +82 -0
  120. package/dist/components/TableHeaderInput/TableHeaderInput.test.d.ts +1 -0
  121. package/dist/components/TableHeaderInput/TableHeaderInput.test.js +84 -0
  122. package/dist/components/TableHeaderInput/index.d.ts +1 -0
  123. package/dist/components/TableHeaderInput/index.js +1 -0
  124. package/dist/components/TableHeaderInput/types.d.ts +30 -0
  125. package/dist/components/TableHeaderInput/types.js +1 -0
  126. package/dist/components/TableRow/TableRow.d.ts +15 -0
  127. package/dist/components/TableRow/TableRow.js +21 -0
  128. package/dist/components/TableRow/TableRow.stories.d.ts +9 -0
  129. package/dist/components/TableRow/TableRow.stories.js +195 -0
  130. package/dist/components/TableRow/TableRow.test.d.ts +1 -0
  131. package/dist/components/TableRow/TableRow.test.js +44 -0
  132. package/dist/components/TableRow/index.d.ts +2 -0
  133. package/dist/components/TableRow/index.js +2 -0
  134. package/dist/components/TableRow/types.d.ts +11 -0
  135. package/dist/components/TableRow/types.js +1 -0
  136. package/dist/components/ToggleButton/ToggleButton.d.ts +4 -0
  137. package/dist/components/ToggleButton/ToggleButton.js +41 -0
  138. package/dist/components/ToggleButton/ToggleButton.stories.d.ts +11 -0
  139. package/dist/components/ToggleButton/ToggleButton.stories.js +111 -0
  140. package/dist/components/ToggleButton/ToggleButton.test.d.ts +1 -0
  141. package/dist/components/ToggleButton/ToggleButton.test.js +106 -0
  142. package/dist/components/ToggleButton/ToggleButton.types.d.ts +22 -0
  143. package/dist/components/ToggleButton/ToggleButton.types.js +1 -0
  144. package/package.json +11 -4
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // src/components/MagnifyingIcon.tsx
3
+ import { forwardRef, useEffect, useState } from "react";
4
+ import { getFontAwesomeIcon } from "../../utils/getFontAwesomeIcon";
5
+ const MagnifyingIcon = forwardRef(({ columnIndex, editingHeader, setEditingHeader, setHeaderValue, column, setSearchColumn, setActiveColumnFromSearch, setActiveColumn, iconColor = "text-blue-500", iconActiveColor = "text-green-500", iconActiveBackgroundColor = "bg-red-500", isActive, additionalIconClasses, setSearchText, searchText, isSearchOpen, }, ref) => {
6
+ const [showActiveStyles, setShowActiveStyles] = useState(false);
7
+ useEffect(() => {
8
+ if (isActive !== undefined) {
9
+ setShowActiveStyles(isActive);
10
+ }
11
+ else {
12
+ const checkSearchCriteria = () => {
13
+ const searchCriteriaRaw = localStorage.getItem("searchCriteria");
14
+ let searchCriteria = [];
15
+ try {
16
+ if (searchCriteriaRaw) {
17
+ searchCriteria = JSON.parse(searchCriteriaRaw);
18
+ }
19
+ }
20
+ catch (e) {
21
+ console.error("Failed to parse searchCriteria from localStorage:", e);
22
+ searchCriteria = [];
23
+ }
24
+ const hasSearchValue = searchCriteria.some((item) => String(item.searchColumn.id) ===
25
+ String(column.id) &&
26
+ item.submittedSearchText &&
27
+ item.submittedSearchText.trim() !== "");
28
+ setShowActiveStyles(hasSearchValue || editingHeader === columnIndex);
29
+ };
30
+ checkSearchCriteria();
31
+ const handleStorageEvent = () => {
32
+ checkSearchCriteria();
33
+ };
34
+ window.addEventListener("localStorageUpdate", handleStorageEvent);
35
+ return () => {
36
+ window.removeEventListener("localStorageUpdate", handleStorageEvent);
37
+ };
38
+ }
39
+ }, [isActive, editingHeader, column.id, columnIndex]);
40
+ useEffect(() => {
41
+ if (isSearchOpen) {
42
+ console.log("Search is open with text:", searchText);
43
+ }
44
+ }, [isSearchOpen, searchText]);
45
+ const handleClick = (event) => {
46
+ event.stopPropagation();
47
+ if (!isSearchOpen) {
48
+ setSearchText?.("");
49
+ }
50
+ setEditingHeader(columnIndex);
51
+ setHeaderValue(column.Header);
52
+ setSearchColumn(column);
53
+ setActiveColumnFromSearch(column.id);
54
+ setActiveColumn(columnIndex);
55
+ };
56
+ return (_jsx("div", { ref: ref, "data-testid": "magnifying-icon-test-id", className: `flex items-center cursor-pointer size-[18px] rounded relative ml-1 ${iconColor} ${showActiveStyles
57
+ ? `${iconActiveBackgroundColor} hover:bg-blue-500 still-active`
58
+ : "hover:bg-white"}`, onClick: handleClick, children: _jsx("div", { className: `text-xs w-full ${additionalIconClasses} ${showActiveStyles ? "text-white" : iconActiveColor}`, children: getFontAwesomeIcon("magnifyingGlass", "solid") }) }));
59
+ });
60
+ export default MagnifyingIcon;
@@ -0,0 +1,9 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import MagnifyingIcon from "./MagnifyingIcon";
3
+ declare const meta: Meta<typeof MagnifyingIcon>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof MagnifyingIcon>;
6
+ export declare const Default: Story;
7
+ export declare const WithActiveClass: Story;
8
+ export declare const WithClickAlert: Story;
9
+ export declare const WithMockLocalStorage: Story;
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import MagnifyingIcon from "./MagnifyingIcon";
3
+ const meta = {
4
+ title: "Components/MagnifyingIcon",
5
+ component: MagnifyingIcon,
6
+ tags: ["autodocs"],
7
+ argTypes: {
8
+ columnIndex: { control: "number" },
9
+ editingHeader: { control: "number" },
10
+ isActive: { control: "boolean" },
11
+ additionalIconClasses: { control: "text" },
12
+ },
13
+ // Add a decorator to wrap all stories in a div
14
+ decorators: [
15
+ (Story) => (_jsx("div", { style: {
16
+ padding: "20px",
17
+ borderRadius: "8px",
18
+ width: "fit-content",
19
+ }, children: _jsx(Story, {}) })),
20
+ ],
21
+ };
22
+ export default meta;
23
+ // Default story (without active class)
24
+ export const Default = {
25
+ args: {
26
+ columnIndex: 0,
27
+ editingHeader: null,
28
+ setEditingHeader: () => { },
29
+ setHeaderValue: () => { },
30
+ additionalIconClasses: "p-4",
31
+ column: {
32
+ id: "exampleColumn",
33
+ Header: "Example Column",
34
+ },
35
+ setSearchColumn: () => { },
36
+ setActiveColumnFromSearch: () => { },
37
+ setActiveColumn: () => { },
38
+ isActive: false,
39
+ },
40
+ };
41
+ // Story with active class
42
+ export const WithActiveClass = {
43
+ args: {
44
+ ...Default.args,
45
+ isActive: true,
46
+ },
47
+ };
48
+ export const WithClickAlert = {
49
+ args: {
50
+ ...Default.args,
51
+ setEditingHeader: (columnIndex) => {
52
+ alert(`the column index and data have been updated! Column Index =` +
53
+ columnIndex);
54
+ },
55
+ },
56
+ };
57
+ export const WithMockLocalStorage = {
58
+ args: {
59
+ ...Default.args,
60
+ },
61
+ decorators: [
62
+ (Story) => {
63
+ localStorage.setItem("searchCriteria", JSON.stringify([
64
+ {
65
+ searchColumn: { id: "exampleColumn" },
66
+ submittedSearchText: "test",
67
+ },
68
+ ]));
69
+ return _jsx(Story, {});
70
+ },
71
+ ],
72
+ };
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect, vi, beforeEach } from "vitest";
3
+ import { render, fireEvent, screen, waitFor } from "@testing-library/react";
4
+ import MagnifyingIcon from "./MagnifyingIcon";
5
+ describe("MagnifyingIcon", () => {
6
+ const mockSetEditingHeader = vi.fn();
7
+ const mockSetHeaderValue = vi.fn();
8
+ const mockSetSearchColumn = vi.fn();
9
+ const mockSetActiveColumnFromSearch = vi.fn();
10
+ const mockSetActiveColumn = vi.fn();
11
+ const defaultProps = {
12
+ columnIndex: 2,
13
+ editingHeader: null,
14
+ setEditingHeader: mockSetEditingHeader,
15
+ setHeaderValue: mockSetHeaderValue,
16
+ column: {
17
+ id: "exampleColumn",
18
+ Header: "Example Column",
19
+ },
20
+ setSearchColumn: mockSetSearchColumn,
21
+ setActiveColumnFromSearch: mockSetActiveColumnFromSearch,
22
+ setActiveColumn: mockSetActiveColumn,
23
+ };
24
+ beforeEach(() => {
25
+ vi.clearAllMocks();
26
+ localStorage.clear();
27
+ });
28
+ it("renders without crashing", () => {
29
+ render(_jsx(MagnifyingIcon, { ...defaultProps }));
30
+ const iconDiv = screen.getByTestId("magnifying-icon-test-id");
31
+ expect(iconDiv).toBeInTheDocument();
32
+ });
33
+ it("calls the appropriate props on click", () => {
34
+ render(_jsx(MagnifyingIcon, { ...defaultProps }));
35
+ const iconDiv = screen.getByTestId("magnifying-icon-test-id");
36
+ fireEvent.click(iconDiv);
37
+ expect(mockSetEditingHeader).toHaveBeenCalledWith(2);
38
+ expect(mockSetHeaderValue).toHaveBeenCalledWith("Example Column");
39
+ expect(mockSetSearchColumn).toHaveBeenCalledWith(defaultProps.column);
40
+ expect(mockSetActiveColumnFromSearch).toHaveBeenCalledWith("exampleColumn");
41
+ expect(mockSetActiveColumn).toHaveBeenCalledWith(2);
42
+ });
43
+ it("applies active styles if localStorage has matching search criteria", () => {
44
+ const fakeCriteria = [
45
+ {
46
+ searchColumn: { id: "exampleColumn" },
47
+ submittedSearchText: "Hello World",
48
+ },
49
+ ];
50
+ localStorage.setItem("searchCriteria", JSON.stringify(fakeCriteria));
51
+ render(_jsx(MagnifyingIcon, { ...defaultProps }));
52
+ const iconDiv = screen.getByTestId("magnifying-icon-test-id");
53
+ // Now "active" means we expect "bg-red-500" & "still-active"
54
+ expect(iconDiv.className).toContain("bg-red-500");
55
+ expect(iconDiv.className).toContain("still-active");
56
+ // Optionally also check the hover state:
57
+ expect(iconDiv.className).toContain("hover:bg-blue-500");
58
+ });
59
+ it("doesn't apply active styles if localStorage is empty", () => {
60
+ render(_jsx(MagnifyingIcon, { ...defaultProps }));
61
+ const iconDiv = screen.getByTestId("magnifying-icon-test-id");
62
+ // Not active => no "bg-red-500"
63
+ expect(iconDiv.className).not.toContain("bg-red-500");
64
+ // By default, we see "hover:bg-white"
65
+ expect(iconDiv.className).toContain("hover:bg-white");
66
+ });
67
+ it("applies active styles if the column is being edited (editingHeader === columnIndex)", () => {
68
+ render(_jsx(MagnifyingIcon, { ...defaultProps, editingHeader: 2 }));
69
+ const iconDiv = screen.getByTestId("magnifying-icon-test-id");
70
+ // Being edited => active => "bg-red-500"
71
+ expect(iconDiv.className).toContain("bg-red-500");
72
+ expect(iconDiv.className).toContain("still-active");
73
+ expect(iconDiv.className).toContain("hover:bg-blue-500");
74
+ });
75
+ it("handles invalid JSON in localStorage gracefully", () => {
76
+ localStorage.setItem("searchCriteria", "{ invalid json }");
77
+ const spy = vi.spyOn(console, "error").mockImplementation(() => { });
78
+ render(_jsx(MagnifyingIcon, { ...defaultProps }));
79
+ // We expect the parse error to have been caught:
80
+ expect(spy).toHaveBeenCalled();
81
+ spy.mockRestore();
82
+ });
83
+ it("updates active state when receiving a 'localStorageUpdate' event", async () => {
84
+ render(_jsx(MagnifyingIcon, { ...defaultProps }));
85
+ const iconDiv = screen.getByTestId("magnifying-icon-test-id");
86
+ expect(iconDiv.className).not.toContain("bg-red-500");
87
+ // Set localStorage and dispatch the event
88
+ localStorage.setItem("searchCriteria", JSON.stringify([
89
+ {
90
+ searchColumn: { id: "exampleColumn" },
91
+ submittedSearchText: "some text",
92
+ },
93
+ ]));
94
+ window.dispatchEvent(new Event("localStorageUpdate"));
95
+ // now wait for the re-render
96
+ await waitFor(() => {
97
+ expect(iconDiv.className).toContain("bg-red-500");
98
+ expect(iconDiv.className).toContain("still-active");
99
+ });
100
+ });
101
+ });
@@ -0,0 +1,2 @@
1
+ export { default } from "./MagnifyingIcon";
2
+ export * from "./types";
@@ -0,0 +1,2 @@
1
+ export { default } from "./MagnifyingIcon";
2
+ export * from "./types";
@@ -0,0 +1,20 @@
1
+ import { Column } from "react-table";
2
+ export interface MagnifyingIconProps<T extends object> {
3
+ columnIndex: number;
4
+ editingHeader: number | null;
5
+ setEditingHeader: (index: number | null) => void;
6
+ setHeaderValue: (value: string) => void;
7
+ column: Column<T>;
8
+ setSearchColumn: (col: Column<T>) => void;
9
+ setActiveColumnFromSearch: (id: string) => void;
10
+ setActiveColumn: (index: number) => void;
11
+ iconColor?: string;
12
+ iconBackgroundColor?: string;
13
+ iconActiveColor?: string;
14
+ iconActiveBackgroundColor?: string;
15
+ isActive?: boolean;
16
+ additionalIconClasses?: string;
17
+ setSearchText?: (text: string) => void;
18
+ searchText?: string;
19
+ isSearchOpen?: boolean;
20
+ }
@@ -0,0 +1,2 @@
1
+ // src/components/MagnifyingIcon/types.ts
2
+ export {};
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import { MultiSelectInputProps } from "./MultiSelect.types";
3
+ declare const MultiSelectInput: React.FC<MultiSelectInputProps>;
4
+ export default MultiSelectInput;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { MultiSelect } from "react-multi-select-component";
3
+ const MultiSelectInput = ({ id, name, options = [], selectedValue = [], onChange, placeholder = "Select...", isSearchable = true, isOpen, hasSelectAll = false, disabled = false, isLoading = false, onMenuToggle, overrideStrings, className, width = "w-72", otherProps, }) => {
4
+ // Convert your OptionType to the shape expected by react-multi-select-component.
5
+ const multiSelectOptions = options.map((option) => ({
6
+ label: option.name,
7
+ value: option.value,
8
+ }));
9
+ // Convert the currently selected value(s) to the shape expected by the component.
10
+ const multiSelectValue = selectedValue.map((item) => ({
11
+ label: item.name,
12
+ value: item.value,
13
+ }));
14
+ return (_jsx("div", { className: `${width}`, children: _jsx(MultiSelect, { id: id, name: name, className: className, options: multiSelectOptions, value: multiSelectValue, onChange: (selectedOptions) => {
15
+ // Convert back to OptionType when passing to onChange
16
+ const mapped = selectedOptions.map((opt) => ({
17
+ name: opt.label,
18
+ value: opt.value,
19
+ }));
20
+ onChange(mapped);
21
+ },
22
+ // Toggle search based on isSearchable
23
+ disableSearch: !isSearchable,
24
+ // Additional props from react-multi-select-component
25
+ isOpen: isOpen, hasSelectAll: hasSelectAll, disabled: disabled, isLoading: isLoading, onMenuToggle: onMenuToggle, overrideStrings: {
26
+ selectSomeItems: placeholder,
27
+ ...overrideStrings,
28
+ }, labelledBy: "Select", ...otherProps }) }));
29
+ };
30
+ export default MultiSelectInput;
@@ -0,0 +1,10 @@
1
+ import { Meta } from "@storybook/react";
2
+ import MultiSelect from "./MultiSelect";
3
+ declare const _default: Meta<typeof MultiSelect>;
4
+ export default _default;
5
+ export declare const Default: any;
6
+ export declare const WithSelectAll: any;
7
+ export declare const OpenByDefault: any;
8
+ export declare const Disabled: any;
9
+ export declare const LoadingState: any;
10
+ export declare const CustomStrings: any;
@@ -0,0 +1,162 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import MultiSelect from "./MultiSelect";
4
+ export default {
5
+ title: "Components/MultiSelect",
6
+ component: MultiSelect,
7
+ tags: ["autodocs"],
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ argTypes: {
12
+ id: {
13
+ control: "text",
14
+ description: "HTML id attribute for the multi-select.",
15
+ },
16
+ name: {
17
+ control: "text",
18
+ description: "HTML name attribute for the multi-select.",
19
+ },
20
+ options: {
21
+ control: "object",
22
+ description: "Array of option objects to display.",
23
+ },
24
+ value: {
25
+ control: "object",
26
+ description: "Currently selected options (array of objects).",
27
+ },
28
+ onChange: {
29
+ action: "changed",
30
+ description: "Callback function triggered when selection changes.",
31
+ },
32
+ placeholder: {
33
+ control: "text",
34
+ description: "Placeholder text when no items are selected.",
35
+ },
36
+ isSearchable: {
37
+ control: "boolean",
38
+ description: "Whether the dropdown has search enabled.",
39
+ },
40
+ isOpen: {
41
+ control: "boolean",
42
+ description: "Control whether the menu is open by default.",
43
+ },
44
+ hasSelectAll: {
45
+ control: "boolean",
46
+ description: "Show/hide the 'Select All' option.",
47
+ },
48
+ disabled: {
49
+ control: "boolean",
50
+ description: "Disable all user interaction.",
51
+ },
52
+ isLoading: {
53
+ control: "boolean",
54
+ description: "Show loading spinner and disable interaction.",
55
+ },
56
+ onMenuToggle: {
57
+ action: "menuToggled",
58
+ description: "Called when menu is opened or closed.",
59
+ },
60
+ overrideStrings: {
61
+ control: "object",
62
+ description: "Override default text strings (e.g., select all, search placeholder).",
63
+ },
64
+ className: {
65
+ control: "text",
66
+ description: "Additional CSS classes for the container.",
67
+ },
68
+ },
69
+ };
70
+ const Template = (args) => {
71
+ // Local state to control selected options in Storybook
72
+ const [selected, setSelected] = useState(args?.selectedValue || []);
73
+ return (_jsx(MultiSelect, { ...args, selectedValue: selected, onChange: (newValues) => {
74
+ setSelected(newValues);
75
+ args.onChange?.(newValues);
76
+ } }));
77
+ };
78
+ export const Default = Template.bind({});
79
+ Default.args = {
80
+ id: "default-multiselect",
81
+ name: "defaultMultiSelect",
82
+ options: [
83
+ { name: "Option 1", value: "option1" },
84
+ { name: "Option 2", value: "option2" },
85
+ { name: "Option 3", value: "option3" },
86
+ ],
87
+ selectedValue: [],
88
+ placeholder: "Select options...",
89
+ isSearchable: true,
90
+ hasSelectAll: false,
91
+ disabled: false,
92
+ isLoading: false,
93
+ };
94
+ export const WithSelectAll = Template.bind({});
95
+ WithSelectAll.args = {
96
+ id: "selectall-multiselect",
97
+ name: "selectAllMultiSelect",
98
+ options: [
99
+ { name: "Apple", value: "apple" },
100
+ { name: "Orange", value: "orange" },
101
+ { name: "Banana", value: "banana" },
102
+ { name: "Grape", value: "grape" },
103
+ ],
104
+ selectedValue: [],
105
+ placeholder: "Select fruits...",
106
+ hasSelectAll: true,
107
+ isSearchable: true,
108
+ };
109
+ export const OpenByDefault = Template.bind({});
110
+ OpenByDefault.args = {
111
+ id: "open-multiselect",
112
+ name: "openByDefaultMultiSelect",
113
+ options: [
114
+ { name: "React", value: "react" },
115
+ { name: "Vue", value: "vue" },
116
+ { name: "Angular", value: "angular" },
117
+ ],
118
+ selectedValue: [],
119
+ placeholder: "Pick your JS framework...",
120
+ isOpen: true,
121
+ hasSelectAll: false,
122
+ };
123
+ export const Disabled = Template.bind({});
124
+ Disabled.args = {
125
+ id: "disabled-multiselect",
126
+ name: "disabledMultiSelect",
127
+ options: [
128
+ { name: "Red", value: "red" },
129
+ { name: "Green", value: "green" },
130
+ { name: "Blue", value: "blue" },
131
+ ],
132
+ selectedValue: [],
133
+ placeholder: "Disabled...",
134
+ disabled: true,
135
+ };
136
+ export const LoadingState = Template.bind({});
137
+ LoadingState.args = {
138
+ id: "loading-multiselect",
139
+ name: "loadingMultiSelect",
140
+ options: [
141
+ { name: "Option A", value: "a" },
142
+ { name: "Option B", value: "b" },
143
+ { name: "Option C", value: "c" },
144
+ ],
145
+ selectedValue: [],
146
+ placeholder: "Loading...",
147
+ isLoading: true,
148
+ };
149
+ export const CustomStrings = Template.bind({});
150
+ CustomStrings.args = {
151
+ options: [
152
+ { name: "Item One", value: "1" },
153
+ { name: "Item Two", value: "2" },
154
+ { name: "Item Three", value: "3" },
155
+ ],
156
+ selectedValue: [],
157
+ overrideStrings: {
158
+ selectSomeItems: "Choose Some Items...",
159
+ selectAll: "Select Everything",
160
+ allItemsAreSelected: "All items selected!",
161
+ },
162
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,107 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { render, screen, fireEvent } from "@testing-library/react";
4
+ import { describe, test, expect, beforeEach, vi } from "vitest";
5
+ import MultiSelect from "./MultiSelect";
6
+ // Mock props
7
+ const mockOnChange = vi.fn();
8
+ const options = [
9
+ { name: "Option 1", value: "option1" },
10
+ { name: "Option 2", value: "option2" },
11
+ { name: "Option 3", value: "option3" },
12
+ ];
13
+ describe("MultiSelect Component", () => {
14
+ beforeEach(() => {
15
+ mockOnChange.mockClear();
16
+ });
17
+ test("renders the multi-select with the placeholder if no value is selected", () => {
18
+ render(_jsx(MultiSelect, { options: options, onChange: mockOnChange, selectedValue: [] }));
19
+ // The default placeholder is "Select..."
20
+ expect(screen.getByText("Select...")).toBeInTheDocument();
21
+ });
22
+ test("opens the dropdown menu when the placeholder is clicked", () => {
23
+ render(_jsx(MultiSelect, { options: options, onChange: mockOnChange, selectedValue: [] }));
24
+ // Click on the placeholder text
25
+ fireEvent.click(screen.getByText("Select..."));
26
+ // All options should now appear in the document
27
+ expect(screen.getByText("Option 1")).toBeInTheDocument();
28
+ expect(screen.getByText("Option 2")).toBeInTheDocument();
29
+ expect(screen.getByText("Option 3")).toBeInTheDocument();
30
+ });
31
+ test("selects an option and calls onChange with the new selection", () => {
32
+ render(_jsx(MultiSelect, { options: options, onChange: mockOnChange, selectedValue: [] }));
33
+ // Open the menu
34
+ fireEvent.click(screen.getByText("Select..."));
35
+ // Click on "Option 2"
36
+ fireEvent.click(screen.getByText("Option 2"));
37
+ // onChange should be called with the array of selected options
38
+ // For a single click, we expect [{ name: "Option 2", value: "option2" }]
39
+ expect(mockOnChange).toHaveBeenCalledWith([
40
+ { name: "Option 2", value: "option2" },
41
+ ]);
42
+ });
43
+ test("allows multiple selections and calls onChange with all selected items", () => {
44
+ const mockOnChange = vi.fn();
45
+ // A wrapper that holds the selectedValue in local state
46
+ function Wrapper() {
47
+ const [selectedValue, setSelectedValue] = React.useState([]);
48
+ return (_jsx(MultiSelect, { options: [
49
+ { name: "Option 1", value: "option1" },
50
+ { name: "Option 2", value: "option2" },
51
+ { name: "Option 3", value: "option3" },
52
+ ], selectedValue: selectedValue, onChange: (updated) => {
53
+ setSelectedValue(updated);
54
+ mockOnChange(updated);
55
+ } }));
56
+ }
57
+ render(_jsx(Wrapper, {}));
58
+ // 1) Open the dropdown
59
+ fireEvent.click(screen.getByText("Select..."));
60
+ // 2) Click the first item
61
+ fireEvent.click(screen.getByText("Option 1"));
62
+ // After first selection
63
+ expect(mockOnChange).toHaveBeenLastCalledWith([
64
+ { name: "Option 1", value: "option1" },
65
+ ]);
66
+ // 4) Click the third item
67
+ fireEvent.click(screen.getByText("Option 3"));
68
+ // Now it should have both Option 1 and Option 3
69
+ expect(mockOnChange).toHaveBeenLastCalledWith([
70
+ { name: "Option 1", value: "option1" },
71
+ { name: "Option 3", value: "option3" },
72
+ ]);
73
+ });
74
+ test("disables the component when disabled prop is true", () => {
75
+ render(_jsx(MultiSelect, { options: options, onChange: mockOnChange, selectedValue: [], disabled: true }));
76
+ // Attempt to click on the placeholder
77
+ fireEvent.click(screen.getByText("Select..."));
78
+ // Because it's disabled, the menu should not open
79
+ // So "Option 1" (etc.) should NOT be in the document
80
+ expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
81
+ });
82
+ test("displays a loading spinner when isLoading is true", () => {
83
+ const { container } = render(_jsx(MultiSelect, { options: [
84
+ { name: "Option 1", value: "option1" },
85
+ { name: "Option 2", value: "option2" },
86
+ ], selectedValue: [], onChange: vi.fn(), isLoading: true }));
87
+ // Look for the <svg> with className="spinner"
88
+ const spinner = container.querySelector(".spinner");
89
+ expect(spinner).not.toBeNull();
90
+ });
91
+ test("shows 'Select All' option if hasSelectAll is true", () => {
92
+ render(_jsx(MultiSelect, { options: options, onChange: mockOnChange, selectedValue: [], hasSelectAll: true }));
93
+ // Open the menu
94
+ fireEvent.click(screen.getByText("Select..."));
95
+ // "Select All" should appear
96
+ expect(screen.getByText("Select All")).toBeInTheDocument();
97
+ });
98
+ test("renders correctly with default props (options and selectedValue omitted)", () => {
99
+ // We do NOT provide options or selectedValue, so the defaults (`[]`) are used
100
+ const { container } = render(_jsx(MultiSelect, { onChange: vi.fn() }));
101
+ // Assertions to confirm the component renders without crashing
102
+ // and displays the default placeholder "Select..."
103
+ expect(screen.getByText("Select...")).toBeInTheDocument();
104
+ // Because options is [], there should be no option
105
+ expect(container.querySelector(".dropdown-content")).toBeNull();
106
+ });
107
+ });
@@ -0,0 +1,28 @@
1
+ export interface OptionType {
2
+ name: string;
3
+ value: string;
4
+ [key: string]: any;
5
+ }
6
+ export interface MultiSelectInputProps {
7
+ id?: string;
8
+ name?: string;
9
+ options?: OptionType[];
10
+ selectedValue?: OptionType[];
11
+ onChange: (selected: OptionType[]) => void;
12
+ placeholder?: string;
13
+ isSearchable?: boolean;
14
+ isOpen?: boolean;
15
+ hasSelectAll?: boolean;
16
+ disabled?: boolean;
17
+ isLoading?: boolean;
18
+ onMenuToggle?: (expanded: boolean) => void;
19
+ overrideStrings?: Partial<{
20
+ selectSomeItems: string;
21
+ allItemsAreSelected: string;
22
+ selectAll: string;
23
+ search: string;
24
+ }>;
25
+ className?: string;
26
+ width?: string;
27
+ otherProps?: any;
28
+ }
@@ -0,0 +1 @@
1
+ export {};