@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.
@@ -1,10 +1,9 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { type JSX, useId, useMemo, useState } from "react";
3
- import { Virtuoso } from "react-virtuoso";
4
- import { Combobox, ComboboxItem } from "../../components/ui/combobox";
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
- const comboboxItem = (
72
- <ComboboxItem key={option} value={option}>
73
- {option}
74
- </ComboboxItem>
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
- <Combobox<string>
109
- displayValue={(option) => {
110
- if (option === NONE_KEY) {
111
- return "--";
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
- className={cn({
118
- "w-full": fullWidth,
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
- // Select none should clear value
229
- fireEvent.click(screen.getByText("--"));
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 "../multiselectFilterFn";
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
- }