@marimo-team/islands 0.23.10-dev26 → 0.23.10-dev28
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-Dy2BhYTf.js} +1334 -1044
- package/dist/main.js +7143 -7119
- package/dist/{reveal-component-xsFYQVKT.js → reveal-component-B5af53Y1.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
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import { type JSX, useId, useMemo
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { type JSX, useId, useMemo } from "react";
|
|
3
|
+
import type { Option } from "@/components/ui/select-core";
|
|
4
|
+
import { SelectList } from "@/components/ui/select-core";
|
|
5
5
|
import { cn } from "../../utils/cn";
|
|
6
6
|
import { Labeled } from "./common/labeled";
|
|
7
|
-
import { multiselectFilterFn } from "./multiselectFilterFn";
|
|
8
7
|
|
|
9
8
|
interface SearchableSelectProps {
|
|
10
9
|
options: string[];
|
|
@@ -16,8 +15,6 @@ interface SearchableSelectProps {
|
|
|
16
15
|
disabled: boolean;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
const NONE_KEY = "__none__";
|
|
20
|
-
|
|
21
18
|
export const SearchableSelect = (props: SearchableSelectProps): JSX.Element => {
|
|
22
19
|
const {
|
|
23
20
|
options,
|
|
@@ -29,104 +26,26 @@ export const SearchableSelect = (props: SearchableSelectProps): JSX.Element => {
|
|
|
29
26
|
disabled,
|
|
30
27
|
} = props;
|
|
31
28
|
const id = useId();
|
|
32
|
-
const [searchQuery, setSearchQuery] = useState<string>("");
|
|
33
|
-
|
|
34
|
-
const filteredOptions = useMemo(() => {
|
|
35
|
-
if (!searchQuery) {
|
|
36
|
-
return options;
|
|
37
|
-
}
|
|
38
|
-
return options.filter(
|
|
39
|
-
(option) => multiselectFilterFn(option, searchQuery) === 1,
|
|
40
|
-
);
|
|
41
|
-
}, [options, searchQuery]);
|
|
42
|
-
|
|
43
|
-
const handleValueChange = (newValue: string | null) => {
|
|
44
|
-
if (newValue == null) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (newValue === NONE_KEY) {
|
|
49
|
-
setValue(null);
|
|
50
|
-
} else {
|
|
51
|
-
setValue(newValue);
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const renderList = () => {
|
|
56
|
-
const extraOptions = allowSelectNone ? (
|
|
57
|
-
<ComboboxItem key={NONE_KEY} value={NONE_KEY}>
|
|
58
|
-
--
|
|
59
|
-
</ComboboxItem>
|
|
60
|
-
) : null;
|
|
61
|
-
|
|
62
|
-
if (filteredOptions.length > 200) {
|
|
63
|
-
return (
|
|
64
|
-
<Virtuoso
|
|
65
|
-
style={{ height: "200px" }}
|
|
66
|
-
totalCount={filteredOptions.length}
|
|
67
|
-
overscan={50}
|
|
68
|
-
itemContent={(i: number) => {
|
|
69
|
-
const option = filteredOptions[i];
|
|
70
29
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
if (i === 0) {
|
|
78
|
-
return (
|
|
79
|
-
<>
|
|
80
|
-
{extraOptions}
|
|
81
|
-
{comboboxItem}
|
|
82
|
-
</>
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return comboboxItem;
|
|
87
|
-
}}
|
|
88
|
-
/>
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const list = filteredOptions.map((option) => (
|
|
93
|
-
<ComboboxItem key={option} value={option}>
|
|
94
|
-
{option}
|
|
95
|
-
</ComboboxItem>
|
|
96
|
-
));
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<>
|
|
100
|
-
{extraOptions}
|
|
101
|
-
{list}
|
|
102
|
-
</>
|
|
103
|
-
);
|
|
104
|
-
};
|
|
30
|
+
const items = useMemo<Array<Option<string>>>(
|
|
31
|
+
() => options.map((option) => ({ value: option, label: option })),
|
|
32
|
+
[options],
|
|
33
|
+
);
|
|
105
34
|
|
|
106
35
|
return (
|
|
107
36
|
<Labeled label={label} id={id} fullWidth={fullWidth}>
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return option;
|
|
114
|
-
}}
|
|
115
|
-
placeholder="Select..."
|
|
37
|
+
<SelectList<string>
|
|
38
|
+
id={id}
|
|
39
|
+
options={items}
|
|
40
|
+
value={value}
|
|
41
|
+
onChange={(next) => setValue((next as string | null) ?? null)}
|
|
116
42
|
multiple={false}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
})}
|
|
120
|
-
value={value ?? NONE_KEY}
|
|
121
|
-
onValueChange={handleValueChange}
|
|
122
|
-
shouldFilter={false}
|
|
123
|
-
search={searchQuery}
|
|
124
|
-
onSearchChange={setSearchQuery}
|
|
43
|
+
allowSelectNone={allowSelectNone}
|
|
44
|
+
fullWidth={fullWidth}
|
|
125
45
|
disabled={disabled}
|
|
46
|
+
className={cn({ "w-full": fullWidth })}
|
|
126
47
|
data-testid="marimo-plugin-searchable-dropdown"
|
|
127
|
-
|
|
128
|
-
{renderList()}
|
|
129
|
-
</Combobox>
|
|
48
|
+
/>
|
|
130
49
|
</Labeled>
|
|
131
50
|
);
|
|
132
51
|
};
|
|
@@ -225,8 +225,11 @@ describe("DropdownPlugin", () => {
|
|
|
225
225
|
screen.getByTestId("marimo-plugin-searchable-dropdown").firstChild!,
|
|
226
226
|
);
|
|
227
227
|
|
|
228
|
-
//
|
|
229
|
-
|
|
228
|
+
// Re-picking the current value clears it when allowSelectNone
|
|
229
|
+
const bananaOption = screen
|
|
230
|
+
.getAllByRole("option")
|
|
231
|
+
.find((el) => el.textContent === "Banana");
|
|
232
|
+
fireEvent.click(bananaOption!);
|
|
230
233
|
expect(setValue).toHaveBeenCalledWith([]);
|
|
231
234
|
});
|
|
232
235
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { beforeEach, expect, it, vi } from "vitest";
|
|
3
3
|
import { initialModeAtom } from "@/core/mode";
|
|
4
4
|
import { store } from "@/core/state/jotai";
|
|
5
|
-
import { multiselectFilterFn } from "
|
|
5
|
+
import { multiselectFilterFn } from "@/components/ui/select-core";
|
|
6
6
|
|
|
7
7
|
function filterOptions(filter: string, items: string[]) {
|
|
8
8
|
return items.filter((option) => multiselectFilterFn(option, filter));
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* We override the default filter function which focuses on sorting by relevance with a fuzzy-match,
|
|
5
|
-
* instead of filtering out.
|
|
6
|
-
* The default filter function is `command-score`.
|
|
7
|
-
*
|
|
8
|
-
* Our filter function only matches if all words in the value are present in the option.
|
|
9
|
-
* This is more strict than the default, but more lenient than an exact match.
|
|
10
|
-
*
|
|
11
|
-
* Examples:
|
|
12
|
-
* - "foo bar" matches "foo bar"
|
|
13
|
-
* - "bar foo" matches "foo bar"
|
|
14
|
-
* - "foob" does not matches "foo bar"
|
|
15
|
-
*/
|
|
16
|
-
export function multiselectFilterFn(option: string, value: string): number {
|
|
17
|
-
const words = value.split(/\s+/);
|
|
18
|
-
const match = words.every((word) =>
|
|
19
|
-
option.toLowerCase().includes(word.toLowerCase()),
|
|
20
|
-
);
|
|
21
|
-
return match ? 1 : 0;
|
|
22
|
-
}
|
|
File without changes
|