@marimo-team/islands 0.23.10-dev9 → 0.23.11-dev1
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.
- package/dist/{ConnectedDataExplorerComponent-CyV83R2m.js → ConnectedDataExplorerComponent-Z3RP7Vmm.js} +31 -31
- package/dist/{ErrorBoundary-rULOrC_p.js → ErrorBoundary-DpbaKVv7.js} +1 -1
- package/dist/{any-language-editor-CiES2a2h.js → any-language-editor-CAgFD4Kd.js} +20 -20
- package/dist/assets/__vite-browser-external-eshhtsgZ.js +1 -0
- package/dist/assets/worker-CC0Oul9k.js +73 -0
- package/dist/{chat-ui-K84W5ecY.js → chat-ui-C4-GMW9y.js} +77 -77
- package/dist/{check-DTbrK0zt.js → check-BCaJeT-J.js} +1 -1
- package/dist/{chunk-5FQGJX7Z-BNjes6Yx.js → chunk-5FQGJX7Z-BbqSm5gU.js} +1227 -1222
- package/dist/{code-block-37QAKDTI-m92Yc8pv.js → code-block-37QAKDTI-O6IH6wAi.js} +1 -1
- package/dist/{code-visibility-v6ucEVUg.js → code-visibility-BDDI_K4x.js} +1943 -1223
- package/dist/{copy-5jQ_kGE1.js → copy-UqRYxiOg.js} +1 -1
- package/dist/dist-7QfXoMdB.js +5 -0
- package/dist/{dist-DgnE8F-r.js → dist-A2846XWO.js} +1 -1
- package/dist/dist-BEXXyZig.js +5 -0
- package/dist/{dist-B3pZ0Ab6.js → dist-BR_gyG9L.js} +3 -3
- package/dist/{dist-CcXxepx6.js → dist-BSAt6RhH.js} +27 -27
- package/dist/{dist-Bde4a2kU.js → dist-BY018Paw.js} +8 -8
- package/dist/dist-BYj57OV4.js +5 -0
- package/dist/{dist-CUCNs1ja.js → dist-BaoDKvdy.js} +2 -2
- package/dist/{dist-Cy1WxgBD.js → dist-Bf7SHuNp.js} +5 -5
- package/dist/{dist-Bz_sYWbr.js → dist-Bk75fBZA.js} +2 -2
- package/dist/dist-BlSvQzNr.js +5 -0
- package/dist/{dist-C5VC_yzu.js → dist-BzEzfugY.js} +1 -1
- package/dist/dist-CCBlxAgS.js +8 -0
- package/dist/dist-CIDTVIUf.js +5 -0
- package/dist/{dist-CLUtPrdy.js → dist-CIYBwstr.js} +1 -1
- package/dist/{dist-BotSqB48.js → dist-C_Y3oV3C.js} +12 -12
- package/dist/{dist-BTfv03uy.js → dist-CcWX6tmx.js} +2 -2
- package/dist/{dist-BhM8gdSO.js → dist-CoXAujgg.js} +4 -4
- package/dist/{dist-4j4c7bjm.js → dist-CpxNdDkw.js} +3 -3
- package/dist/dist-CqQyhAM8.js +8 -0
- package/dist/dist-CwRu2Xzh.js +5 -0
- package/dist/{dist-BcuoonNH.js → dist-CxJDU6Bh.js} +9 -9
- package/dist/{dist-DxvORzUR.js → dist-D-W5ny5a.js} +8 -8
- package/dist/dist-D8CDTVgf.js +6 -0
- package/dist/dist-D8DNB0nO.js +8 -0
- package/dist/dist-DL6N_q-A.js +5 -0
- package/dist/{dist-BbbIBDiQ.js → dist-DMjWuVs8.js} +1 -1
- package/dist/dist-DOFbNV_b.js +8 -0
- package/dist/dist-DPrYzMY0.js +6 -0
- package/dist/{dist-h2c8sZvT.js → dist-DZORgqKY.js} +1 -1
- package/dist/{dist-B3P2fFpz.js → dist-DZo4nSS0.js} +14 -14
- package/dist/{dist-D4CewLk6.js → dist-Dax--nl9.js} +1 -1
- package/dist/{dist-DRfcqpxJ.js → dist-DgGbNavJ.js} +2 -2
- package/dist/{dist-C1BYNeCR.js → dist-Dk6PV_d3.js} +10 -10
- package/dist/{dist-fQ0ViXGs.js → dist-Dv_Y15yk.js} +107 -107
- package/dist/{dist-Bfwsv11D.js → dist-DyyjKEYf.js} +2 -2
- package/dist/{dist-p2qyWijU.js → dist-GZXUmt0b.js} +2 -2
- package/dist/{dist-CLJWPTX2.js → dist-LTU8Hdvn.js} +3 -3
- package/dist/{dist-DqAWR3CS.js → dist-M9Vag9Y0.js} +20 -20
- package/dist/{dist-DNdhYsgW.js → dist-U4F-tbMs.js} +79 -62
- package/dist/{dist-RqXTaiir.js → dist-abid3KgM.js} +11 -11
- package/dist/dist-cdmMjgsn.js +5 -0
- package/dist/dist-hT4QzYX-.js +1247 -0
- package/dist/{dist-luvabDEB.js → dist-t9Kf7xqC.js} +2 -2
- package/dist/{error-banner-5bz0L9hS.js → error-banner-CJXYJ6Sb.js} +11 -6
- package/dist/esm-BaH2eg5-.js +1171 -0
- package/dist/{esm-Duie8iU-.js → esm-ga2Bf3O2.js} +43 -43
- package/dist/{extends-CkydH1Q5.js → extends-D_hDsj6R.js} +4 -4
- package/dist/files/wasm-intro.py +11 -8
- package/dist/{formats-DHxc-FdY.js → formats-C4wO47tk.js} +1 -1
- package/dist/{glide-data-editor-CgIxTzAP.js → glide-data-editor-BPkCPs7L.js} +255 -255
- package/dist/{html-to-image-B93KtAVY.js → html-to-image-BwfcJJxA.js} +2412 -2282
- package/dist/{input-_2sjvfne.js → input-OdWHkobi.js} +263 -245
- package/dist/{label-LWtdw5i8.js → label-CC4ytI1X.js} +1 -1
- package/dist/main.js +7630 -7558
- package/dist/{mermaid-4DMBBIKO-DIdL224_.js → mermaid-4DMBBIKO-Eg3D4FVz.js} +1 -1
- package/dist/{mermaid-lXOw5Py9.js → mermaid-BotvIKg9.js} +4 -4
- package/dist/{process-output-BOXi9fnS.js → process-output-DsloEDna.js} +3 -3
- package/dist/{reveal-component-gRcjQbNL.js → reveal-component-BOEk3vwx.js} +698 -620
- package/dist/{spec-B96zNUEA.js → spec-X7FwLJni.js} +4 -4
- package/dist/{strings-Bu3vlb6W.js → strings-J57tzLr3.js} +47 -46
- package/dist/style.css +1 -1
- package/dist/{toDate-x-WRDCH7.js → toDate-d8RCRrRd.js} +2 -2
- package/dist/{tooltip-C5FYOpQc.js → tooltip-DpcyNkQ2.js} +2 -2
- package/dist/{types-CVvp1fKr.js → types-ChtMFmZ2.js} +1 -1
- package/dist/{useAsyncData-iRgKDT5s.js → useAsyncData-PonK__yh.js} +1 -1
- package/dist/{useDateFormatter-BA4FCquG.js → useDateFormatter-QB-3MpYr.js} +2 -2
- package/dist/{useDeepCompareMemoize-CkQ57VS2.js → useDeepCompareMemoize-D3NGWke6.js} +1 -1
- package/dist/{useLifecycle-BBO9PIph.js → useLifecycle-00mO3OSS.js} +2 -2
- package/dist/{useTheme-DHIrRQOe.js → useTheme-DEhDzATN.js} +1 -1
- package/dist/{vega-component-Dq-SH463.js → vega-component-DGPUhbDs.js} +8 -8
- package/dist/{zod-CoBiJ5v4.js → zod-aLSua2NL.js} +24 -23
- package/package.json +4 -3
- package/src/__tests__/setup.ts +16 -0
- package/src/components/chat/chat-display.tsx +2 -2
- package/src/components/data-table/TableBottomBar.tsx +1 -15
- package/src/components/data-table/TableTopBar.tsx +8 -13
- package/src/components/data-table/__tests__/TableBottomBar.test.tsx +6 -12
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +227 -0
- package/src/components/data-table/column-visibility-dropdown.tsx +204 -0
- package/src/components/data-table/data-table.tsx +1 -1
- package/src/components/data-table/filter-by-values-picker.tsx +39 -17
- package/src/components/data-table/filter-pills.tsx +1 -1
- package/src/components/datasources/__tests__/filter-empty.test.ts +183 -0
- package/src/components/datasources/datasources.tsx +92 -3
- package/src/components/editor/ai/ai-completion-editor.tsx +6 -5
- package/src/components/editor/cell/code/cell-editor.tsx +70 -32
- package/src/components/editor/cell/code/code-placeholder.css +18 -0
- package/src/components/editor/cell/code/code-placeholder.tsx +47 -0
- package/src/components/editor/cell/code/language-toggle.tsx +7 -1
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +97 -52
- package/src/components/editor/chrome/wrapper/lazy-panels.ts +91 -0
- package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -0
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +9 -2
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +62 -27
- package/src/components/editor/connections/storage/add-storage-form.tsx +5 -1
- package/src/components/editor/connections/storage/as-code.ts +20 -6
- package/src/components/editor/documentation.css +35 -0
- package/src/components/editor/file-tree/file-explorer.tsx +8 -18
- package/src/components/editor/file-tree/tree-actions.tsx +46 -1
- package/src/components/editor/notebook-cell.tsx +3 -3
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +20 -0
- package/src/components/editor/renderers/slides-layout/types.ts +1 -0
- package/src/components/markdown/__tests__/markdown-renderer.test.tsx +43 -0
- package/src/components/markdown/markdown-renderer.tsx +24 -1
- package/src/components/slides/__tests__/minimap-actions.test.tsx +166 -0
- package/src/components/slides/__tests__/reveal-component.test.ts +425 -0
- package/src/components/slides/minimap.tsx +127 -10
- package/src/components/slides/reveal-component.tsx +287 -61
- package/src/components/slides/slide-cell-view.tsx +26 -2
- package/src/components/slides/slide-form.tsx +26 -4
- package/src/components/storage/__tests__/storage-inspector.test.ts +180 -0
- package/src/components/storage/storage-inspector.tsx +535 -81
- package/src/components/ui/combobox.tsx +51 -32
- package/src/components/ui/number-field.tsx +5 -1
- package/src/components/ui/reorderable-list.tsx +13 -0
- package/src/components/ui/select-core/__tests__/use-select-list.test.ts +294 -0
- package/src/components/ui/select-core/__tests__/utils.test.ts +222 -0
- package/src/components/ui/select-core/index.ts +16 -0
- package/src/components/ui/select-core/option-row.tsx +33 -0
- package/src/components/ui/select-core/render-slot.ts +20 -0
- package/src/components/ui/select-core/select-list.tsx +248 -0
- package/src/components/ui/select-core/types.ts +44 -0
- package/src/components/ui/select-core/use-select-list.ts +347 -0
- package/src/components/ui/select-core/utils.ts +121 -0
- package/src/components/ui/toast.tsx +1 -0
- package/src/core/ai/__tests__/strip-wrapping-backticks.test.ts +133 -0
- package/src/core/ai/stream-completion-text.ts +48 -0
- package/src/core/ai/strip-wrapping-backticks.ts +88 -0
- package/src/core/cells/__tests__/document-changes.test.ts +2 -2
- package/src/core/cells/__tests__/focus.test.ts +45 -2
- package/src/core/cells/focus.ts +1 -1
- package/src/core/cells/scrollCellIntoView.ts +4 -0
- package/src/core/cells/utils.ts +1 -1
- package/src/core/codemirror/__tests__/editor-mount-scheduler.test.ts +143 -0
- package/src/core/codemirror/ai/request.ts +2 -14
- package/src/core/codemirror/copilot/getCodes.ts +3 -1
- package/src/core/codemirror/editor-mount-scheduler.ts +101 -0
- package/src/core/codemirror/language/languages/python.ts +2 -0
- package/src/core/codemirror/language/languages/sql/utils.ts +3 -1
- package/src/core/codemirror/lsp/__tests__/markdown-renderer.test.ts +41 -0
- package/src/core/codemirror/lsp/markdown-renderer.ts +59 -0
- package/src/core/datasets/data-source-connections.ts +2 -0
- package/src/core/errors/errors.ts +1 -1
- package/src/core/storage/__tests__/state.test.ts +49 -0
- package/src/core/storage/__tests__/useStorageEntries.test.tsx +199 -3
- package/src/core/storage/state.ts +124 -13
- package/src/core/storage/types.ts +6 -0
- package/src/core/wasm/worker/bootstrap.ts +12 -4
- package/src/plugins/core/__test__/registerReactComponent.test.ts +96 -0
- package/src/plugins/core/registerReactComponent.tsx +42 -10
- package/src/plugins/impl/MultiselectPlugin.tsx +19 -142
- package/src/plugins/impl/SearchableSelect.tsx +16 -97
- package/src/plugins/impl/SliderPlugin.tsx +132 -18
- package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
- package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
- package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +278 -8
- package/src/utils/__tests__/numbers.test.ts +20 -0
- package/src/utils/errors.ts +9 -0
- package/src/utils/lazy.ts +6 -1
- package/src/utils/numbers.ts +27 -0
- package/src/utils/schedule-task.ts +71 -0
- package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +0 -1
- package/dist/assets/worker-ip3AI_sN.js +0 -73
- package/dist/dist-0Fif7jnk.js +0 -5
- package/dist/dist-B5h_9sHB.js +0 -6
- package/dist/dist-B9M6R5ye.js +0 -5
- package/dist/dist-BCt3tnck.js +0 -8
- package/dist/dist-BUIJwMwn.js +0 -8
- package/dist/dist-BpquMd3k.js +0 -5
- package/dist/dist-BzJsqYfz.js +0 -5
- package/dist/dist-CA5ELXAf.js +0 -6
- package/dist/dist-CLBRs6Uv.js +0 -5
- package/dist/dist-CStVCMbq.js +0 -5
- package/dist/dist-CZRIEY3Y.js +0 -8
- package/dist/dist-CuUHbFD0.js +0 -5
- package/dist/dist-DV7Iabxb.js +0 -8
- package/dist/dist-DhHh0jLg.js +0 -1247
- package/dist/dist-DuEeHMvL.js +0 -5
- package/dist/esm-CCuYCd3R.js +0 -1171
- package/src/plugins/impl/multiselectFilterFn.tsx +0 -22
- /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 = (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 = (
|
|
123
|
-
|
|
128
|
+
const handleSelect = useCallback(
|
|
129
|
+
(selectedValue: unknown) => {
|
|
130
|
+
let newValue: TValue | TValue[] | null = selectedValue as TValue;
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 = [
|
|
141
|
+
newValue = [newValue];
|
|
132
142
|
}
|
|
133
|
-
} else {
|
|
134
|
-
newValue =
|
|
143
|
+
} else if (value === selectedValue) {
|
|
144
|
+
newValue = null;
|
|
135
145
|
}
|
|
136
|
-
} else if (value === selectedValue) {
|
|
137
|
-
newValue = null;
|
|
138
|
-
}
|
|
139
146
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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">
|
|
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={
|
|
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
|
-
{
|
|
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
|
-
(
|
|
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
|
+
});
|