@marimo-team/islands 0.23.10-dev26 → 0.23.10-dev27
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/{code-visibility-Rcdlclvw.js → code-visibility-DI2QSiFC.js} +1334 -1044
- package/dist/main.js +7143 -7119
- package/dist/{reveal-component-xsFYQVKT.js → reveal-component-BA7HaWOX.js} +16 -16
- package/package.json +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/ui/combobox.tsx +51 -32
- 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/plugins/impl/MultiselectPlugin.tsx +19 -142
- package/src/plugins/impl/SearchableSelect.tsx +16 -97
- package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
- package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
- package/src/plugins/impl/multiselectFilterFn.tsx +0 -22
- /package/src/components/{data-table → ui}/value-chips.tsx +0 -0
|
@@ -9,7 +9,7 @@ import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
|
9
9
|
import { lt as kioskModeAtom } from "./html-to-image-UEH5lFDZ.js";
|
|
10
10
|
import "./chunk-5FQGJX7Z-BNjes6Yx.js";
|
|
11
11
|
import { u as createLucideIcon } from "./dist-Dk6PV_d3.js";
|
|
12
|
-
import { $ as
|
|
12
|
+
import { $t as Expand, Qt as EyeOff, a as DEFAULT_SLIDE_TYPE, c as Slide, i as DEFAULT_DECK_TRANSITION, nn as Code, nt as PanelGroup, rt as PanelResizeHandle, s as SlideSidebar, t as useNotebookCodeAvailable, tt as Panel } from "./code-visibility-DI2QSiFC.js";
|
|
13
13
|
import { J as useDebouncedCallback } from "./input-CMYy4hzj.js";
|
|
14
14
|
import "./toDate-d8RCRrRd.js";
|
|
15
15
|
import "./react-dom-BTJzcVJ9.js";
|
|
@@ -7249,8 +7249,8 @@ var SubslideView = (e5) => {
|
|
|
7249
7249
|
if (t[0] !== i || t[1] !== r || t[2] !== a || t[3] !== n) {
|
|
7250
7250
|
let { slideLevel: e6, cumulativeByBlock: ro2 } = buildSubslideNotes(n, a);
|
|
7251
7251
|
s = e6;
|
|
7252
|
-
let
|
|
7253
|
-
o = w, f = "h-full w-full overflow-auto flex", c =
|
|
7252
|
+
let p2 = n.blocks.some((e7) => e7.cells.some((e8) => r(e8.id)));
|
|
7253
|
+
o = w, f = "h-full w-full overflow-auto flex", c = p2 ? "mo-slide-content flex flex-col gap-3" : "mo-slide-content", t[10] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (l = { margin: "auto 20px" }, t[10] = l) : l = t[10], u = n.blocks.map((e7, t2) => {
|
|
7254
7254
|
let n2 = e7.cells.map((e8) => r(e8.id) ? i ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlideCellView, { cell: e8 }, e8.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlideCellReadOnlyView, { cell: e8 }, e8.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slide, {
|
|
7255
7255
|
cellId: e8.id,
|
|
7256
7256
|
status: e8.status,
|
|
@@ -7272,15 +7272,15 @@ var SubslideView = (e5) => {
|
|
|
7272
7272
|
style: l,
|
|
7273
7273
|
children: u
|
|
7274
7274
|
}), t[11] = c, t[12] = l, t[13] = u, t[14] = ro) : ro = t[14];
|
|
7275
|
-
let
|
|
7276
|
-
t[15] !== f || t[16] !== ro ? (
|
|
7275
|
+
let p;
|
|
7276
|
+
t[15] !== f || t[16] !== ro ? (p = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
7277
7277
|
className: f,
|
|
7278
7278
|
children: ro
|
|
7279
|
-
}), t[15] = f, t[16] = ro, t[17] =
|
|
7279
|
+
}), t[15] = f, t[16] = ro, t[17] = p) : p = t[17];
|
|
7280
7280
|
let ao;
|
|
7281
7281
|
t[18] === s ? ao = t[19] : (ao = s && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NotesAside, { text: s }), t[18] = s, t[19] = ao);
|
|
7282
7282
|
let oo;
|
|
7283
|
-
return t[20] !== o || t[21] !==
|
|
7283
|
+
return t[20] !== o || t[21] !== p || t[22] !== ao ? (oo = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(o, { children: [p, ao] }), t[20] = o, t[21] = p, t[22] = ao, t[23] = oo) : oo = t[23], oo;
|
|
7284
7284
|
};
|
|
7285
7285
|
function parkedRendersSource(e5) {
|
|
7286
7286
|
let { isNoOutputPreview: t, isEditable: n, showCode: r } = e5;
|
|
@@ -7302,9 +7302,9 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7302
7302
|
status: n.status,
|
|
7303
7303
|
output: n.output
|
|
7304
7304
|
}), t[3] = n.id, t[4] = n.output, t[5] = n.status, t[6] = o) : o = t[6], o;
|
|
7305
|
-
}, reveal_component_default = ({ slideCells: e5, layout: o, setLayout: s, noOutputIds: l, activeIndex:
|
|
7305
|
+
}, reveal_component_default = ({ slideCells: e5, layout: o, setLayout: s, noOutputIds: l, activeIndex: ro, onSlideChange: io, mode: p, configWidth: fo, isEditable: po = false }) => {
|
|
7306
7306
|
var _a2, _b2;
|
|
7307
|
-
let yo = (0, import_react.useRef)(null), bo = (0, import_react.useRef)(null), { width: So, height: Co } = useSlideDimensions(yo), wo = useFullScreenElement() != null, To = useAtomValue(kioskModeAtom), Eo = (0, import_react.useMemo)(() => To ? [] : [ce], [To]), [Do, Oo] = (0, import_react.useState)(() => /* @__PURE__ */ new Set()), ko = useNotebookCodeAvailable(e5), Ao = !isIslands() && ko, jo =
|
|
7307
|
+
let yo = (0, import_react.useRef)(null), bo = (0, import_react.useRef)(null), { width: So, height: Co } = useSlideDimensions(yo), wo = useFullScreenElement() != null, To = useAtomValue(kioskModeAtom), Eo = (0, import_react.useMemo)(() => To ? [] : [ce], [To]), [Do, Oo] = (0, import_react.useState)(() => /* @__PURE__ */ new Set()), ko = useNotebookCodeAvailable(e5), Ao = !isIslands() && ko, jo = ro == null ? void 0 : e5[ro], Mo = jo ?? e5.at(0), { parkedPreviewCell: No, isHeldEdit: Po, isNoOutputPreview: Fo, heldEditCellId: Io, heldShowsCode: Lo, toggleHeldShowsCode: Ro } = useParkedPreview({
|
|
7308
7308
|
activeCell: jo,
|
|
7309
7309
|
slideConfigs: o.cells,
|
|
7310
7310
|
noOutputIds: l
|
|
@@ -7350,7 +7350,7 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7350
7350
|
qo
|
|
7351
7351
|
]), Yo = useEvent_default((t) => {
|
|
7352
7352
|
let n = resolveDeckNavigationTarget({
|
|
7353
|
-
activeIndex:
|
|
7353
|
+
activeIndex: ro,
|
|
7354
7354
|
cells: e5,
|
|
7355
7355
|
cellToTarget: Wo,
|
|
7356
7356
|
getId: (e6) => e6.id
|
|
@@ -7361,7 +7361,7 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7361
7361
|
let e6 = bo.current;
|
|
7362
7362
|
e6 != null && Yo(e6);
|
|
7363
7363
|
}, [
|
|
7364
|
-
|
|
7364
|
+
ro,
|
|
7365
7365
|
Wo,
|
|
7366
7366
|
e5,
|
|
7367
7367
|
Yo
|
|
@@ -7391,14 +7391,14 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7391
7391
|
let e6 = bo.current;
|
|
7392
7392
|
if (!e6) return;
|
|
7393
7393
|
let t = resolveActiveCellIndex(Go, e6.getIndices());
|
|
7394
|
-
t != null && (
|
|
7394
|
+
t != null && (io == null ? void 0 : io(t));
|
|
7395
7395
|
}), $o = useEvent_default((t) => {
|
|
7396
|
-
if (!No ||
|
|
7396
|
+
if (!No || ro == null || Events.fromInput(t)) return;
|
|
7397
7397
|
let n = classifyNavKey(t);
|
|
7398
7398
|
if (n === 0) return;
|
|
7399
7399
|
t.preventDefault(), t.stopPropagation();
|
|
7400
|
-
let i =
|
|
7401
|
-
i < 0 || i >= e5.length || (
|
|
7400
|
+
let i = ro + n;
|
|
7401
|
+
i < 0 || i >= e5.length || (io == null ? void 0 : io(i));
|
|
7402
7402
|
}), ns = useEvent_default(() => {
|
|
7403
7403
|
Qo(), triggerResize(bo.current);
|
|
7404
7404
|
});
|
|
@@ -7494,7 +7494,7 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7494
7494
|
]
|
|
7495
7495
|
})
|
|
7496
7496
|
});
|
|
7497
|
-
return
|
|
7497
|
+
return p === "read" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
7498
7498
|
className: "flex-1 min-w-0 flex flex-row gap-3",
|
|
7499
7499
|
children: os
|
|
7500
7500
|
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
package/package.json
CHANGED
|
@@ -7,6 +7,8 @@ import { useMemo, useState } from "react";
|
|
|
7
7
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
8
8
|
import { ErrorBanner } from "@/plugins/impl/common/error-banner";
|
|
9
9
|
import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin";
|
|
10
|
+
import type { Option } from "@/components/ui/select-core";
|
|
11
|
+
import { useSelectList } from "@/components/ui/select-core";
|
|
10
12
|
import { Logger } from "@/utils/Logger";
|
|
11
13
|
import { Sets } from "@/utils/sets";
|
|
12
14
|
import { smartMatch } from "@/utils/smartMatch";
|
|
@@ -23,7 +25,7 @@ import {
|
|
|
23
25
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
|
24
26
|
import { SentinelCell } from "./sentinel-cell";
|
|
25
27
|
import { detectSentinel, stringifyUnknownValue } from "./utils";
|
|
26
|
-
import { CompactChipRow } from "
|
|
28
|
+
import { CompactChipRow } from "@/components/ui/value-chips";
|
|
27
29
|
|
|
28
30
|
const TOP_K_ROWS = 30;
|
|
29
31
|
|
|
@@ -101,8 +103,6 @@ export const FilterByValuesList = <TData, TValue>({
|
|
|
101
103
|
onChange,
|
|
102
104
|
creatable = false,
|
|
103
105
|
}: FilterByValuesListProps<TData, TValue>) => {
|
|
104
|
-
const [query, setQuery] = useState<string>("");
|
|
105
|
-
|
|
106
106
|
const { data, isPending, error } = useAsyncData(async () => {
|
|
107
107
|
if (!calculateTopKRows) {
|
|
108
108
|
return null;
|
|
@@ -111,27 +111,49 @@ export const FilterByValuesList = <TData, TValue>({
|
|
|
111
111
|
return res.data;
|
|
112
112
|
}, [calculateTopKRows, column.id]);
|
|
113
113
|
|
|
114
|
-
const
|
|
114
|
+
const options = useMemo<Array<Option<unknown>>>(() => {
|
|
115
115
|
if (!data) {
|
|
116
116
|
return [];
|
|
117
117
|
}
|
|
118
118
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
smartMatch(query, str) ||
|
|
127
|
-
str.toLowerCase().includes(query.toLowerCase())
|
|
128
|
-
);
|
|
129
|
-
});
|
|
119
|
+
return data
|
|
120
|
+
.filter(([value]) => value !== undefined)
|
|
121
|
+
.map(([value, count]) => ({
|
|
122
|
+
value,
|
|
123
|
+
label: String(value),
|
|
124
|
+
data: { count },
|
|
125
|
+
}));
|
|
130
126
|
} catch (error_) {
|
|
131
|
-
Logger.error("Error
|
|
127
|
+
Logger.error("Error building filter options", error_);
|
|
132
128
|
return [];
|
|
133
129
|
}
|
|
134
|
-
}, [data
|
|
130
|
+
}, [data]);
|
|
131
|
+
|
|
132
|
+
const list = useSelectList<unknown>({
|
|
133
|
+
options,
|
|
134
|
+
value: [...chosenValues],
|
|
135
|
+
onChange: (next) => onChange(next as unknown[]),
|
|
136
|
+
multiple: true,
|
|
137
|
+
filterFn: (label, q) =>
|
|
138
|
+
smartMatch(q, label) || label.toLowerCase().includes(q.toLowerCase())
|
|
139
|
+
? 1
|
|
140
|
+
: 0,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const query = list.searchQuery;
|
|
144
|
+
const setQuery = list.setSearchQuery;
|
|
145
|
+
|
|
146
|
+
const filteredData = useMemo<Array<[unknown, number | undefined]>>(
|
|
147
|
+
() =>
|
|
148
|
+
list.visibleOptions.map(
|
|
149
|
+
(o) =>
|
|
150
|
+
[o.value, (o.data as { count: number }).count] as [
|
|
151
|
+
unknown,
|
|
152
|
+
number | undefined,
|
|
153
|
+
],
|
|
154
|
+
),
|
|
155
|
+
[list.visibleOptions],
|
|
156
|
+
);
|
|
135
157
|
|
|
136
158
|
// Surface chosen values that aren't in the top-K so they stay visible/uncheckable.
|
|
137
159
|
// Count is undefined for these rows; the cell renders an em-dash.
|
|
@@ -21,7 +21,7 @@ import { AddFilterButton } from "./add-filter-button";
|
|
|
21
21
|
import { FilterPillEditor } from "./filter-pill-editor";
|
|
22
22
|
import { type ColumnFilterValue, formatValue, type Snapshot } from "./filters";
|
|
23
23
|
import { extractTimezone } from "./types";
|
|
24
|
-
import { ChipWithComma, CompactChipRow } from "
|
|
24
|
+
import { ChipWithComma, CompactChipRow } from "@/components/ui/value-chips";
|
|
25
25
|
|
|
26
26
|
interface Props<TData> {
|
|
27
27
|
filters: ColumnFiltersState | undefined;
|
|
@@ -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
|
);
|
|
@@ -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
|
+
});
|