@marimo-team/islands 0.23.10-dev9 → 0.23.11-dev67

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 (188) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CyV83R2m.js → ConnectedDataExplorerComponent-Z3RP7Vmm.js} +31 -31
  2. package/dist/{ErrorBoundary-rULOrC_p.js → ErrorBoundary-DpbaKVv7.js} +1 -1
  3. package/dist/{any-language-editor-CiES2a2h.js → any-language-editor-CAgFD4Kd.js} +20 -20
  4. package/dist/assets/__vite-browser-external-eshhtsgZ.js +1 -0
  5. package/dist/assets/worker-CC0Oul9k.js +73 -0
  6. package/dist/{chat-ui-K84W5ecY.js → chat-ui-C4-GMW9y.js} +77 -77
  7. package/dist/{check-DTbrK0zt.js → check-BCaJeT-J.js} +1 -1
  8. package/dist/{chunk-5FQGJX7Z-BNjes6Yx.js → chunk-5FQGJX7Z-BbqSm5gU.js} +1227 -1222
  9. package/dist/{code-block-37QAKDTI-m92Yc8pv.js → code-block-37QAKDTI-O6IH6wAi.js} +1 -1
  10. package/dist/{code-visibility-v6ucEVUg.js → code-visibility-UH2371Sv.js} +1943 -1223
  11. package/dist/{copy-5jQ_kGE1.js → copy-UqRYxiOg.js} +1 -1
  12. package/dist/dist-7QfXoMdB.js +5 -0
  13. package/dist/{dist-DgnE8F-r.js → dist-A2846XWO.js} +1 -1
  14. package/dist/dist-BEXXyZig.js +5 -0
  15. package/dist/{dist-B3pZ0Ab6.js → dist-BR_gyG9L.js} +3 -3
  16. package/dist/{dist-CcXxepx6.js → dist-BSAt6RhH.js} +27 -27
  17. package/dist/{dist-Bde4a2kU.js → dist-BY018Paw.js} +8 -8
  18. package/dist/dist-BYj57OV4.js +5 -0
  19. package/dist/{dist-CUCNs1ja.js → dist-BaoDKvdy.js} +2 -2
  20. package/dist/{dist-Cy1WxgBD.js → dist-Bf7SHuNp.js} +5 -5
  21. package/dist/{dist-Bz_sYWbr.js → dist-Bk75fBZA.js} +2 -2
  22. package/dist/dist-BlSvQzNr.js +5 -0
  23. package/dist/{dist-C5VC_yzu.js → dist-BzEzfugY.js} +1 -1
  24. package/dist/dist-CCBlxAgS.js +8 -0
  25. package/dist/dist-CIDTVIUf.js +5 -0
  26. package/dist/{dist-CLUtPrdy.js → dist-CIYBwstr.js} +1 -1
  27. package/dist/{dist-BotSqB48.js → dist-C_Y3oV3C.js} +12 -12
  28. package/dist/{dist-BTfv03uy.js → dist-CcWX6tmx.js} +2 -2
  29. package/dist/{dist-BhM8gdSO.js → dist-CoXAujgg.js} +4 -4
  30. package/dist/{dist-4j4c7bjm.js → dist-CpxNdDkw.js} +3 -3
  31. package/dist/dist-CqQyhAM8.js +8 -0
  32. package/dist/dist-CwRu2Xzh.js +5 -0
  33. package/dist/{dist-BcuoonNH.js → dist-CxJDU6Bh.js} +9 -9
  34. package/dist/{dist-DxvORzUR.js → dist-D-W5ny5a.js} +8 -8
  35. package/dist/dist-D8CDTVgf.js +6 -0
  36. package/dist/dist-D8DNB0nO.js +8 -0
  37. package/dist/dist-DL6N_q-A.js +5 -0
  38. package/dist/{dist-BbbIBDiQ.js → dist-DMjWuVs8.js} +1 -1
  39. package/dist/dist-DOFbNV_b.js +8 -0
  40. package/dist/dist-DPrYzMY0.js +6 -0
  41. package/dist/{dist-h2c8sZvT.js → dist-DZORgqKY.js} +1 -1
  42. package/dist/{dist-B3P2fFpz.js → dist-DZo4nSS0.js} +14 -14
  43. package/dist/{dist-D4CewLk6.js → dist-Dax--nl9.js} +1 -1
  44. package/dist/{dist-DRfcqpxJ.js → dist-DgGbNavJ.js} +2 -2
  45. package/dist/{dist-C1BYNeCR.js → dist-Dk6PV_d3.js} +10 -10
  46. package/dist/{dist-fQ0ViXGs.js → dist-Dv_Y15yk.js} +107 -107
  47. package/dist/{dist-Bfwsv11D.js → dist-DyyjKEYf.js} +2 -2
  48. package/dist/{dist-p2qyWijU.js → dist-GZXUmt0b.js} +2 -2
  49. package/dist/{dist-CLJWPTX2.js → dist-LTU8Hdvn.js} +3 -3
  50. package/dist/{dist-DqAWR3CS.js → dist-M9Vag9Y0.js} +20 -20
  51. package/dist/{dist-DNdhYsgW.js → dist-U4F-tbMs.js} +79 -62
  52. package/dist/{dist-RqXTaiir.js → dist-abid3KgM.js} +11 -11
  53. package/dist/dist-cdmMjgsn.js +5 -0
  54. package/dist/dist-hT4QzYX-.js +1247 -0
  55. package/dist/{dist-luvabDEB.js → dist-t9Kf7xqC.js} +2 -2
  56. package/dist/{error-banner-5bz0L9hS.js → error-banner-CJXYJ6Sb.js} +11 -6
  57. package/dist/esm-BaH2eg5-.js +1171 -0
  58. package/dist/{esm-Duie8iU-.js → esm-ga2Bf3O2.js} +43 -43
  59. package/dist/{extends-CkydH1Q5.js → extends-D_hDsj6R.js} +4 -4
  60. package/dist/files/wasm-intro.py +11 -8
  61. package/dist/{formats-DHxc-FdY.js → formats-C4wO47tk.js} +1 -1
  62. package/dist/{glide-data-editor-CgIxTzAP.js → glide-data-editor-BPkCPs7L.js} +255 -255
  63. package/dist/{html-to-image-B93KtAVY.js → html-to-image-BwfcJJxA.js} +2412 -2282
  64. package/dist/{input-_2sjvfne.js → input-OdWHkobi.js} +263 -245
  65. package/dist/{label-LWtdw5i8.js → label-CC4ytI1X.js} +1 -1
  66. package/dist/main.js +7630 -7558
  67. package/dist/{mermaid-4DMBBIKO-DIdL224_.js → mermaid-4DMBBIKO-Eg3D4FVz.js} +1 -1
  68. package/dist/{mermaid-lXOw5Py9.js → mermaid-BotvIKg9.js} +4 -4
  69. package/dist/{process-output-BOXi9fnS.js → process-output-DsloEDna.js} +3 -3
  70. package/dist/{reveal-component-gRcjQbNL.js → reveal-component-BNU1V1x5.js} +698 -620
  71. package/dist/{spec-B96zNUEA.js → spec-X7FwLJni.js} +4 -4
  72. package/dist/{strings-Bu3vlb6W.js → strings-J57tzLr3.js} +47 -46
  73. package/dist/style.css +1 -1
  74. package/dist/{toDate-x-WRDCH7.js → toDate-d8RCRrRd.js} +2 -2
  75. package/dist/{tooltip-C5FYOpQc.js → tooltip-DpcyNkQ2.js} +2 -2
  76. package/dist/{types-CVvp1fKr.js → types-ChtMFmZ2.js} +1 -1
  77. package/dist/{useAsyncData-iRgKDT5s.js → useAsyncData-PonK__yh.js} +1 -1
  78. package/dist/{useDateFormatter-BA4FCquG.js → useDateFormatter-QB-3MpYr.js} +2 -2
  79. package/dist/{useDeepCompareMemoize-CkQ57VS2.js → useDeepCompareMemoize-D3NGWke6.js} +1 -1
  80. package/dist/{useLifecycle-BBO9PIph.js → useLifecycle-00mO3OSS.js} +2 -2
  81. package/dist/{useTheme-DHIrRQOe.js → useTheme-DEhDzATN.js} +1 -1
  82. package/dist/{vega-component-Dq-SH463.js → vega-component-DGPUhbDs.js} +8 -8
  83. package/dist/{zod-CoBiJ5v4.js → zod-aLSua2NL.js} +24 -23
  84. package/package.json +4 -3
  85. package/src/__tests__/setup.ts +16 -0
  86. package/src/components/chat/chat-display.tsx +2 -2
  87. package/src/components/data-table/TableBottomBar.tsx +1 -15
  88. package/src/components/data-table/TableTopBar.tsx +8 -13
  89. package/src/components/data-table/__tests__/TableBottomBar.test.tsx +6 -12
  90. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +227 -0
  91. package/src/components/data-table/column-visibility-dropdown.tsx +204 -0
  92. package/src/components/data-table/data-table.tsx +1 -1
  93. package/src/components/data-table/filter-by-values-picker.tsx +39 -17
  94. package/src/components/data-table/filter-pills.tsx +1 -1
  95. package/src/components/datasources/__tests__/filter-empty.test.ts +183 -0
  96. package/src/components/datasources/datasources.tsx +92 -3
  97. package/src/components/editor/cell/code/cell-editor.tsx +70 -32
  98. package/src/components/editor/cell/code/code-placeholder.css +18 -0
  99. package/src/components/editor/cell/code/code-placeholder.tsx +47 -0
  100. package/src/components/editor/cell/code/language-toggle.tsx +7 -1
  101. package/src/components/editor/chrome/wrapper/app-chrome.tsx +97 -52
  102. package/src/components/editor/chrome/wrapper/lazy-panels.ts +91 -0
  103. package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -0
  104. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +9 -2
  105. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +62 -27
  106. package/src/components/editor/connections/storage/add-storage-form.tsx +5 -1
  107. package/src/components/editor/connections/storage/as-code.ts +20 -6
  108. package/src/components/editor/documentation.css +35 -0
  109. package/src/components/editor/file-tree/file-explorer.tsx +8 -18
  110. package/src/components/editor/file-tree/tree-actions.tsx +46 -1
  111. package/src/components/editor/notebook-cell.tsx +3 -3
  112. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +20 -0
  113. package/src/components/editor/renderers/slides-layout/types.ts +1 -0
  114. package/src/components/markdown/__tests__/markdown-renderer.test.tsx +43 -0
  115. package/src/components/markdown/markdown-renderer.tsx +24 -1
  116. package/src/components/slides/__tests__/minimap-actions.test.tsx +166 -0
  117. package/src/components/slides/__tests__/reveal-component.test.ts +425 -0
  118. package/src/components/slides/minimap.tsx +127 -10
  119. package/src/components/slides/reveal-component.tsx +287 -61
  120. package/src/components/slides/slide-cell-view.tsx +26 -2
  121. package/src/components/slides/slide-form.tsx +26 -4
  122. package/src/components/storage/__tests__/storage-inspector.test.ts +180 -0
  123. package/src/components/storage/storage-inspector.tsx +535 -81
  124. package/src/components/ui/combobox.tsx +51 -32
  125. package/src/components/ui/number-field.tsx +5 -1
  126. package/src/components/ui/reorderable-list.tsx +13 -0
  127. package/src/components/ui/select-core/__tests__/use-select-list.test.ts +294 -0
  128. package/src/components/ui/select-core/__tests__/utils.test.ts +222 -0
  129. package/src/components/ui/select-core/index.ts +16 -0
  130. package/src/components/ui/select-core/option-row.tsx +33 -0
  131. package/src/components/ui/select-core/render-slot.ts +20 -0
  132. package/src/components/ui/select-core/select-list.tsx +248 -0
  133. package/src/components/ui/select-core/types.ts +44 -0
  134. package/src/components/ui/select-core/use-select-list.ts +347 -0
  135. package/src/components/ui/select-core/utils.ts +121 -0
  136. package/src/components/ui/toast.tsx +1 -0
  137. package/src/core/cells/__tests__/document-changes.test.ts +2 -2
  138. package/src/core/cells/__tests__/focus.test.ts +45 -2
  139. package/src/core/cells/focus.ts +1 -1
  140. package/src/core/cells/scrollCellIntoView.ts +4 -0
  141. package/src/core/cells/utils.ts +1 -1
  142. package/src/core/codemirror/__tests__/editor-mount-scheduler.test.ts +143 -0
  143. package/src/core/codemirror/copilot/getCodes.ts +3 -1
  144. package/src/core/codemirror/editor-mount-scheduler.ts +101 -0
  145. package/src/core/codemirror/language/languages/python.ts +2 -0
  146. package/src/core/codemirror/language/languages/sql/utils.ts +3 -1
  147. package/src/core/codemirror/lsp/__tests__/markdown-renderer.test.ts +41 -0
  148. package/src/core/codemirror/lsp/markdown-renderer.ts +59 -0
  149. package/src/core/datasets/data-source-connections.ts +2 -0
  150. package/src/core/errors/errors.ts +1 -1
  151. package/src/core/storage/__tests__/state.test.ts +49 -0
  152. package/src/core/storage/__tests__/useStorageEntries.test.tsx +199 -3
  153. package/src/core/storage/state.ts +124 -13
  154. package/src/core/storage/types.ts +6 -0
  155. package/src/core/wasm/worker/bootstrap.ts +12 -4
  156. package/src/plugins/core/__test__/registerReactComponent.test.ts +96 -0
  157. package/src/plugins/core/registerReactComponent.tsx +42 -10
  158. package/src/plugins/impl/MultiselectPlugin.tsx +19 -142
  159. package/src/plugins/impl/SearchableSelect.tsx +16 -97
  160. package/src/plugins/impl/SliderPlugin.tsx +132 -18
  161. package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
  162. package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
  163. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +278 -8
  164. package/src/utils/__tests__/numbers.test.ts +20 -0
  165. package/src/utils/errors.ts +9 -0
  166. package/src/utils/lazy.ts +6 -1
  167. package/src/utils/numbers.ts +27 -0
  168. package/src/utils/schedule-task.ts +71 -0
  169. package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +0 -1
  170. package/dist/assets/worker-ip3AI_sN.js +0 -73
  171. package/dist/dist-0Fif7jnk.js +0 -5
  172. package/dist/dist-B5h_9sHB.js +0 -6
  173. package/dist/dist-B9M6R5ye.js +0 -5
  174. package/dist/dist-BCt3tnck.js +0 -8
  175. package/dist/dist-BUIJwMwn.js +0 -8
  176. package/dist/dist-BpquMd3k.js +0 -5
  177. package/dist/dist-BzJsqYfz.js +0 -5
  178. package/dist/dist-CA5ELXAf.js +0 -6
  179. package/dist/dist-CLBRs6Uv.js +0 -5
  180. package/dist/dist-CStVCMbq.js +0 -5
  181. package/dist/dist-CZRIEY3Y.js +0 -8
  182. package/dist/dist-CuUHbFD0.js +0 -5
  183. package/dist/dist-DV7Iabxb.js +0 -8
  184. package/dist/dist-DhHh0jLg.js +0 -1247
  185. package/dist/dist-DuEeHMvL.js +0 -5
  186. package/dist/esm-CCuYCd3R.js +0 -1171
  187. package/src/plugins/impl/multiselectFilterFn.tsx +0 -22
  188. /package/src/components/{data-table → ui}/value-chips.tsx +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useControllableState } from "@radix-ui/react-use-controllable-state";
4
4
  import { Check, ChevronDownIcon, XCircle } from "lucide-react";
5
- import React, { createContext } from "react";
5
+ import React, { createContext, useCallback, useMemo } from "react";
6
6
  import { cn } from "../../utils/cn";
7
7
  import { Functions } from "../../utils/functions";
8
8
  import { Badge } from "./badge";
@@ -40,6 +40,8 @@ interface ComboboxCommonProps<TValue> {
40
40
  id?: string;
41
41
  keepPopoverOpenOnSelect?: boolean;
42
42
  disabled?: boolean;
43
+ /** Override the trigger contents with a node (e.g. a compact chip summary). */
44
+ renderValue?: (value: TValue[] | TValue | null) => React.ReactNode;
43
45
  }
44
46
 
45
47
  type ComboboxFilterProps =
@@ -97,6 +99,7 @@ export const Combobox = <TValue,>({
97
99
  keepPopoverOpenOnSelect,
98
100
  id,
99
101
  disabled = false,
102
+ renderValue,
100
103
  ...rest
101
104
  }: ComboboxProps<TValue>) => {
102
105
  const [open = false, setOpen] = useControllableState({
@@ -112,39 +115,45 @@ export const Combobox = <TValue,>({
112
115
  },
113
116
  });
114
117
 
115
- const isSelected = (selectedValue: unknown) => {
116
- if (Array.isArray(value)) {
117
- return value.includes(selectedValue as TValue);
118
- }
119
- return value === selectedValue;
120
- };
118
+ const isSelected = useCallback(
119
+ (selectedValue: unknown) => {
120
+ if (Array.isArray(value)) {
121
+ return value.includes(selectedValue as TValue);
122
+ }
123
+ return value === selectedValue;
124
+ },
125
+ [value],
126
+ );
121
127
 
122
- const handleSelect = (selectedValue: unknown) => {
123
- let newValue: TValue | TValue[] | null = selectedValue as TValue;
128
+ const handleSelect = useCallback(
129
+ (selectedValue: unknown) => {
130
+ let newValue: TValue | TValue[] | null = selectedValue as TValue;
124
131
 
125
- if (multiple) {
126
- if (Array.isArray(value)) {
127
- if (value.includes(newValue)) {
128
- const newArr = value.filter((val) => val !== selectedValue);
129
- newValue = newArr.length > 0 ? newArr : [];
132
+ if (multiple) {
133
+ if (Array.isArray(value)) {
134
+ if (value.includes(newValue)) {
135
+ const newArr = value.filter((val) => val !== selectedValue);
136
+ newValue = newArr.length > 0 ? newArr : [];
137
+ } else {
138
+ newValue = [...value, newValue];
139
+ }
130
140
  } else {
131
- newValue = [...value, newValue];
141
+ newValue = [newValue];
132
142
  }
133
- } else {
134
- newValue = [newValue];
143
+ } else if (value === selectedValue) {
144
+ newValue = null;
135
145
  }
136
- } else if (value === selectedValue) {
137
- newValue = null;
138
- }
139
146
 
140
- setValue(newValue);
141
- const keepOpen = keepPopoverOpenOnSelect ?? multiple;
142
- if (!keepOpen) {
143
- setOpen(false);
144
- }
145
- };
147
+ setValue(newValue);
148
+ const keepOpen = keepPopoverOpenOnSelect ?? multiple;
149
+ if (!keepOpen) {
150
+ setOpen(false);
151
+ }
152
+ },
153
+ [keepPopoverOpenOnSelect, multiple, value, setValue, setOpen],
154
+ );
146
155
 
147
- const renderValue = (): string => {
156
+ const renderValueLabel = (): string => {
148
157
  // If we show chips, we don't want to change the placeholder
149
158
  if (multiple && chips && placeholder) {
150
159
  return placeholder;
@@ -168,6 +177,14 @@ export const Combobox = <TValue,>({
168
177
  return placeholder ?? "--";
169
178
  };
170
179
 
180
+ const comboboxContextValue: ComboboxContextValue = useMemo(
181
+ () => ({
182
+ isSelected,
183
+ onSelect: handleSelect,
184
+ }),
185
+ [isSelected, handleSelect],
186
+ );
187
+
171
188
  return (
172
189
  <div className={cn("relative")} {...rest}>
173
190
  <Popover
@@ -191,7 +208,9 @@ export const Combobox = <TValue,>({
191
208
  aria-expanded={open}
192
209
  aria-disabled={disabled}
193
210
  >
194
- <span className="truncate flex-1 min-w-0">{renderValue()}</span>
211
+ <span className="truncate flex-1 min-w-0">
212
+ {renderValue ? renderValue(value ?? null) : renderValueLabel()}
213
+ </span>
195
214
  <ChevronDownIcon className="ml-3 w-4 h-4 opacity-50 shrink-0" />
196
215
  </button>
197
216
  </PopoverTrigger>
@@ -209,7 +228,7 @@ export const Combobox = <TValue,>({
209
228
  />
210
229
  <CommandList className="max-h-60 py-.5">
211
230
  <CommandEmpty>{emptyState}</CommandEmpty>
212
- <ComboboxContext value={{ isSelected, onSelect: handleSelect }}>
231
+ <ComboboxContext value={comboboxContextValue}>
213
232
  {children}
214
233
  </ComboboxContext>
215
234
  </CommandList>
@@ -277,12 +296,14 @@ export const ComboboxItem = React.forwardRef(
277
296
  ? value.value
278
297
  : String(value);
279
298
  const context = React.use(ComboboxContext);
299
+ const isOptionSelected = context.isSelected(value);
280
300
 
281
301
  return (
282
302
  <CommandItem
283
303
  ref={ref}
284
304
  className={cn("pl-6 m-1 py-1", className)}
285
305
  role="option"
306
+ aria-selected={isOptionSelected}
286
307
  value={valueAsString}
287
308
  disabled={disabled}
288
309
  onSelect={() => {
@@ -290,9 +311,7 @@ export const ComboboxItem = React.forwardRef(
290
311
  onSelect?.(value);
291
312
  }}
292
313
  >
293
- {context.isSelected(value) && (
294
- <Check className="absolute left-1 h-4 w-4" />
295
- )}
314
+ {isOptionSelected && <Check className="absolute left-1 h-4 w-4" />}
296
315
  {children}
297
316
  </CommandItem>
298
317
  );
@@ -25,7 +25,10 @@ export interface NumberFieldProps extends AriaNumberFieldProps {
25
25
  }
26
26
 
27
27
  export const NumberField = React.forwardRef<HTMLInputElement, NumberFieldProps>(
28
- ({ placeholder, variant = "default", onInputText, ...props }, ref) => {
28
+ (
29
+ { placeholder, variant = "default", onInputText, formatOptions, ...props },
30
+ ref,
31
+ ) => {
29
32
  const { locale } = useLocale();
30
33
  return (
31
34
  <AriaNumberField
@@ -33,6 +36,7 @@ export const NumberField = React.forwardRef<HTMLInputElement, NumberFieldProps>(
33
36
  formatOptions={{
34
37
  minimumFractionDigits: 0,
35
38
  maximumFractionDigits: maxFractionalDigits(locale),
39
+ ...formatOptions,
36
40
  }}
37
41
  >
38
42
  <div
@@ -57,6 +57,12 @@ export interface ReorderableListProps<T> {
57
57
  * Callback when an item is clicked
58
58
  */
59
59
  onAction?: (item: T) => void;
60
+ /**
61
+ * Fired when an item is hovered or focused. Useful for preloading the
62
+ * resource the item points to before it is activated. Attached to the
63
+ * focusable list item so it works for both pointer and keyboard users.
64
+ */
65
+ onItemPreloadHint?: (item: T) => void;
60
66
  /**
61
67
  * All available items that can be added to the list
62
68
  */
@@ -142,6 +148,7 @@ export const ReorderableList = <T extends object>({
142
148
  ariaLabel = "Reorderable list",
143
149
  className,
144
150
  crossListDrag,
151
+ onItemPreloadHint,
145
152
  }: ReorderableListProps<T>) => {
146
153
  const mimeType = crossListDrag
147
154
  ? getDragMimeType(crossListDrag.dragType)
@@ -294,6 +301,12 @@ export const ReorderableList = <T extends object>({
294
301
  key={getKey(item)}
295
302
  id={getKey(item)}
296
303
  className="active:cursor-grabbing data-[dragging]:opacity-60 outline-none"
304
+ onHoverStart={
305
+ onItemPreloadHint ? () => onItemPreloadHint(item) : undefined
306
+ }
307
+ onFocus={
308
+ onItemPreloadHint ? () => onItemPreloadHint(item) : undefined
309
+ }
297
310
  >
298
311
  {renderItem(item)}
299
312
  </ListBoxItem>
@@ -0,0 +1,294 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { act, renderHook } from "@testing-library/react";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import type { BulkAction, Option } from "../types";
5
+ import { useSelectList } from "../use-select-list";
6
+
7
+ const opts: Array<Option<string>> = [
8
+ { value: "a", label: "apple" },
9
+ { value: "b", label: "banana" },
10
+ { value: "c", label: "cherry" },
11
+ ];
12
+
13
+ describe("useSelectList - search", () => {
14
+ it("filters visibleOptions by label using the strict word match", () => {
15
+ const { result } = renderHook(() =>
16
+ useSelectList({
17
+ options: opts,
18
+ value: [],
19
+ onChange: vi.fn(),
20
+ multiple: true,
21
+ }),
22
+ );
23
+ act(() => result.current.setSearchQuery("ban"));
24
+ expect(result.current.visibleOptions.map((o) => o.value)).toEqual(["b"]);
25
+ });
26
+
27
+ it("returns all options when the query is empty", () => {
28
+ const { result } = renderHook(() =>
29
+ useSelectList({
30
+ options: opts,
31
+ value: [],
32
+ onChange: vi.fn(),
33
+ multiple: true,
34
+ }),
35
+ );
36
+ expect(result.current.visibleOptions).toHaveLength(3);
37
+ });
38
+
39
+ it("keeps options for any positive filter score, not just 1", () => {
40
+ const { result } = renderHook(() =>
41
+ useSelectList({
42
+ options: opts,
43
+ value: [],
44
+ onChange: vi.fn(),
45
+ multiple: true,
46
+ filterFn: (label) => (label === "apple" ? 0.5 : 0),
47
+ }),
48
+ );
49
+ act(() => result.current.setSearchQuery("anything"));
50
+ expect(result.current.visibleOptions.map((o) => o.value)).toEqual(["a"]);
51
+ });
52
+ });
53
+
54
+ describe("useSelectList - multi toggle", () => {
55
+ it("adds an unselected value", () => {
56
+ const onChange = vi.fn();
57
+ const { result } = renderHook(() =>
58
+ useSelectList({ options: opts, value: ["a"], onChange, multiple: true }),
59
+ );
60
+ act(() => result.current.toggle("b"));
61
+ expect(onChange).toHaveBeenCalledWith(["a", "b"]);
62
+ });
63
+
64
+ it("removes a selected value", () => {
65
+ const onChange = vi.fn();
66
+ const { result } = renderHook(() =>
67
+ useSelectList({
68
+ options: opts,
69
+ value: ["a", "b"],
70
+ onChange,
71
+ multiple: true,
72
+ }),
73
+ );
74
+ act(() => result.current.toggle("a"));
75
+ expect(onChange).toHaveBeenCalledWith(["b"]);
76
+ });
77
+
78
+ it("drops the oldest selection when maxSelections is exceeded", () => {
79
+ const onChange = vi.fn();
80
+ const { result } = renderHook(() =>
81
+ useSelectList({
82
+ options: opts,
83
+ value: ["a", "b"],
84
+ onChange,
85
+ multiple: true,
86
+ maxSelections: 2,
87
+ }),
88
+ );
89
+ act(() => result.current.toggle("c"));
90
+ expect(onChange).toHaveBeenCalledWith(["b", "c"]);
91
+ });
92
+
93
+ it("isChecked reflects membership", () => {
94
+ const { result } = renderHook(() =>
95
+ useSelectList({
96
+ options: opts,
97
+ value: ["a"],
98
+ onChange: vi.fn(),
99
+ multiple: true,
100
+ }),
101
+ );
102
+ expect(result.current.isChecked("a")).toBe(true);
103
+ expect(result.current.isChecked("b")).toBe(false);
104
+ });
105
+ });
106
+
107
+ describe("useSelectList - single toggle", () => {
108
+ it("replaces the value", () => {
109
+ const onChange = vi.fn();
110
+ const { result } = renderHook(() =>
111
+ useSelectList({ options: opts, value: "a", onChange, multiple: false }),
112
+ );
113
+ act(() => result.current.toggle("b"));
114
+ expect(onChange).toHaveBeenCalledWith("b");
115
+ });
116
+
117
+ it("clears to null when allowSelectNone and the value is re-toggled", () => {
118
+ const onChange = vi.fn();
119
+ const { result } = renderHook(() =>
120
+ useSelectList({
121
+ options: opts,
122
+ value: "a",
123
+ onChange,
124
+ multiple: false,
125
+ allowSelectNone: true,
126
+ }),
127
+ );
128
+ act(() => result.current.toggle("a"));
129
+ expect(onChange).toHaveBeenCalledWith(null);
130
+ });
131
+ });
132
+
133
+ describe("useSelectList - pinning + freeze", () => {
134
+ it("pins selected to the top of visibleOptions when open and idle", () => {
135
+ const { result } = renderHook(() =>
136
+ useSelectList({
137
+ options: opts,
138
+ value: ["c"],
139
+ onChange: vi.fn(),
140
+ multiple: true,
141
+ pinSelected: true,
142
+ }),
143
+ );
144
+ act(() => result.current.setOpen(true));
145
+ expect(result.current.visibleOptions.map((o) => o.value)).toEqual([
146
+ "c",
147
+ "a",
148
+ "b",
149
+ ]);
150
+ expect(result.current.pinnedCount).toBe(1);
151
+ });
152
+
153
+ it("freezes pinned order while open: a newly toggled item does not re-pin", () => {
154
+ const onChange = vi.fn();
155
+ const { result, rerender } = renderHook(
156
+ ({ value }) =>
157
+ useSelectList({
158
+ options: opts,
159
+ value,
160
+ onChange,
161
+ multiple: true,
162
+ pinSelected: true,
163
+ }),
164
+ { initialProps: { value: ["c"] as string[] } },
165
+ );
166
+ act(() => result.current.setOpen(true));
167
+ rerender({ value: ["c", "a"] });
168
+ expect(result.current.visibleOptions.map((o) => o.value)).toEqual([
169
+ "c",
170
+ "a",
171
+ "b",
172
+ ]);
173
+ expect(result.current.isChecked("a")).toBe(true);
174
+ });
175
+
176
+ it("re-pins when the search clears", () => {
177
+ const { result, rerender } = renderHook(
178
+ ({ value }) =>
179
+ useSelectList({
180
+ options: opts,
181
+ value,
182
+ onChange: vi.fn(),
183
+ multiple: true,
184
+ pinSelected: true,
185
+ }),
186
+ { initialProps: { value: ["b"] as string[] } },
187
+ );
188
+ act(() => result.current.setOpen(true));
189
+ act(() => result.current.setSearchQuery("a"));
190
+ rerender({ value: ["b", "a"] });
191
+ act(() => result.current.setSearchQuery(""));
192
+ expect(result.current.visibleOptions.map((o) => o.value)).toEqual([
193
+ "b",
194
+ "a",
195
+ "c",
196
+ ]);
197
+ });
198
+
199
+ it("does not pin when pinSelected is false", () => {
200
+ const { result } = renderHook(() =>
201
+ useSelectList({
202
+ options: opts,
203
+ value: ["c"],
204
+ onChange: vi.fn(),
205
+ multiple: true,
206
+ }),
207
+ );
208
+ act(() => result.current.setOpen(true));
209
+ expect(result.current.visibleOptions.map((o) => o.value)).toEqual([
210
+ "a",
211
+ "b",
212
+ "c",
213
+ ]);
214
+ expect(result.current.pinnedCount).toBe(0);
215
+ });
216
+ });
217
+
218
+ describe("useSelectList - bulk", () => {
219
+ const findAction = <K extends BulkAction<string>["kind"]>(
220
+ actions: ReadonlyArray<BulkAction<string>>,
221
+ kind: K,
222
+ ): Extract<BulkAction<string>, { kind: K }> | undefined =>
223
+ actions.find(
224
+ (a): a is Extract<BulkAction<string>, { kind: K }> => a.kind === kind,
225
+ );
226
+
227
+ it("idle: select-all picks every option; deselect-all clears", () => {
228
+ const onChange = vi.fn();
229
+ const { result } = renderHook(() =>
230
+ useSelectList({ options: opts, value: ["a"], onChange, multiple: true }),
231
+ );
232
+ act(() => findAction(result.current.bulkActions, "select-all")?.run());
233
+ expect(onChange).toHaveBeenCalledWith(["a", "b", "c"]);
234
+ onChange.mockClear();
235
+ act(() => findAction(result.current.bulkActions, "deselect-all")?.run());
236
+ expect(onChange).toHaveBeenCalledWith([]);
237
+ });
238
+
239
+ it("idle: select-all skips disabled options and keeps the existing selection", () => {
240
+ const onChange = vi.fn();
241
+ const withDisabled: Array<Option<string>> = [
242
+ { value: "a", label: "apple" },
243
+ { value: "b", label: "banana", disabled: true },
244
+ { value: "c", label: "cherry" },
245
+ ];
246
+ const { result } = renderHook(() =>
247
+ useSelectList({
248
+ options: withDisabled,
249
+ value: ["c"],
250
+ onChange,
251
+ multiple: true,
252
+ }),
253
+ );
254
+ act(() => findAction(result.current.bulkActions, "select-all")?.run());
255
+ expect(onChange).toHaveBeenCalledWith(["c", "a"]);
256
+ });
257
+
258
+ it("searching: select-matching acts only on the matches (additive)", () => {
259
+ const onChange = vi.fn();
260
+ const { result } = renderHook(() =>
261
+ useSelectList({ options: opts, value: ["a"], onChange, multiple: true }),
262
+ );
263
+ act(() => result.current.setSearchQuery("b"));
264
+ act(() => findAction(result.current.bulkActions, "select-matching")?.run());
265
+ expect(onChange).toHaveBeenCalledWith(["a", "b"]);
266
+ });
267
+
268
+ it("exposes idle bulkActions in select-then-deselect order", () => {
269
+ const { result } = renderHook(() =>
270
+ useSelectList({
271
+ options: opts,
272
+ value: ["a"],
273
+ onChange: vi.fn(),
274
+ multiple: true,
275
+ }),
276
+ );
277
+ const kinds = result.current.bulkActions.map((a) => a.kind);
278
+ expect(kinds).toEqual(["select-all", "deselect-all"]);
279
+ const selectAll = findAction(result.current.bulkActions, "select-all");
280
+ expect(selectAll && "enabled" in selectAll && selectAll.enabled).toBe(true);
281
+ });
282
+
283
+ it("bulkActions is empty for single-select", () => {
284
+ const { result } = renderHook(() =>
285
+ useSelectList({
286
+ options: opts,
287
+ value: "a",
288
+ onChange: vi.fn(),
289
+ multiple: false,
290
+ }),
291
+ );
292
+ expect(result.current.bulkActions).toEqual([]);
293
+ });
294
+ });