@noya-app/noya-designsystem 0.1.45 → 0.1.46
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +9 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +1144 -1070
- package/dist/index.d.ts +1144 -1070
- package/dist/index.js +10127 -9653
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10407 -9937
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/combobox.test.ts +137 -89
- package/src/components/Breadcrumbs.tsx +29 -0
- package/src/components/Collection.tsx +74 -0
- package/src/components/Combobox.tsx +69 -52
- package/src/components/ComboboxMenu.tsx +37 -19
- package/src/components/DropdownMenu.tsx +0 -1
- package/src/components/EditableText.tsx +203 -0
- package/src/components/Grid.tsx +243 -0
- package/src/components/GridView.tsx +86 -96
- package/src/components/List.tsx +268 -0
- package/src/components/ListView.tsx +10 -9
- package/src/components/SearchCompletionMenu.tsx +13 -9
- package/src/components/SegmentedControl.tsx +7 -4
- package/src/components/SelectMenu.tsx +9 -5
- package/src/components/Toolbar.tsx +5 -4
- package/src/components/TreeView.tsx +1 -0
- package/src/components/WorkspaceLayout.tsx +28 -4
- package/src/components/internal/Menu.tsx +55 -14
- package/src/components/internal/MenuViewport.tsx +78 -77
- package/src/index.tsx +6 -4
- package/src/utils/combobox.ts +115 -103
- package/src/utils/createSectionedMenu.ts +3 -9
- package/src/utils/fuzzyScorer.ts +11 -8
- package/src/utils/selection.ts +16 -4
- package/src/components/SidebarList.tsx +0 -252
|
@@ -65,86 +65,87 @@ export const MenuViewport = <T extends string>({
|
|
|
65
65
|
return (
|
|
66
66
|
<>
|
|
67
67
|
{items.map((item, index) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (item.type === "sectionHeader") {
|
|
78
|
-
return (
|
|
79
|
-
<SectionHeader
|
|
80
|
-
isFirst={index === 0}
|
|
81
|
-
key={item.id}
|
|
82
|
-
{...item}
|
|
83
|
-
indented={hasCheckedItem}
|
|
84
|
-
/>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
68
|
+
switch (item.type) {
|
|
69
|
+
case "separator":
|
|
70
|
+
return (
|
|
71
|
+
<Components.Separator
|
|
72
|
+
key={index}
|
|
73
|
+
className={styles.separatorStyle}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
87
76
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
77
|
+
case "sectionHeader":
|
|
78
|
+
return (
|
|
79
|
+
<SectionHeader
|
|
80
|
+
isFirst={index === 0}
|
|
81
|
+
key={item.id}
|
|
82
|
+
{...item}
|
|
83
|
+
indented={hasCheckedItem}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
case "submenu":
|
|
87
|
+
if (
|
|
88
|
+
item.items &&
|
|
89
|
+
Components.Sub &&
|
|
90
|
+
Components.SubTrigger &&
|
|
91
|
+
Components.SubContent &&
|
|
92
|
+
Components.Portal
|
|
93
|
+
) {
|
|
94
|
+
return (
|
|
95
|
+
<Components.Sub key={item.id}>
|
|
96
|
+
<Components.SubTrigger
|
|
97
|
+
className={styles.itemStyle({ disabled: item.disabled })}
|
|
98
|
+
asChild
|
|
99
|
+
onClick={(e) => {
|
|
100
|
+
e.stopPropagation();
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<SelectItem
|
|
105
|
+
value={item.id}
|
|
106
|
+
icon={item.icon}
|
|
107
|
+
checked={item.checked}
|
|
108
|
+
onSelect={onSelect}
|
|
109
|
+
Components={Components}
|
|
110
|
+
indented={hasCheckedItem}
|
|
111
|
+
>
|
|
112
|
+
<div className="flex items-center flex-1">
|
|
113
|
+
{item.title}
|
|
114
|
+
</div>
|
|
115
|
+
<ChevronRightIcon className="-mr-1" />
|
|
116
|
+
</SelectItem>
|
|
117
|
+
</Components.SubTrigger>
|
|
118
|
+
<Components.Portal>
|
|
119
|
+
<Components.SubContent
|
|
120
|
+
alignOffset={-5}
|
|
121
|
+
className={styles.contentStyle}
|
|
122
|
+
>
|
|
123
|
+
<MenuViewport
|
|
124
|
+
items={item.items}
|
|
125
|
+
Components={Components}
|
|
126
|
+
onSelect={onSelect}
|
|
127
|
+
/>
|
|
128
|
+
</Components.SubContent>
|
|
129
|
+
</Components.Portal>
|
|
130
|
+
</Components.Sub>
|
|
131
|
+
);
|
|
132
|
+
} else return null;
|
|
133
|
+
default:
|
|
134
|
+
return (
|
|
135
|
+
<SelectItem
|
|
136
|
+
key={item.value}
|
|
137
|
+
value={item.value}
|
|
138
|
+
icon={item.icon}
|
|
139
|
+
checked={item.checked}
|
|
140
|
+
shortcut={item.shortcut}
|
|
141
|
+
onSelect={onSelect}
|
|
142
|
+
Components={Components}
|
|
143
|
+
indented={hasCheckedItem}
|
|
102
144
|
>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
checked={item.checked}
|
|
107
|
-
shortcut={item.shortcut}
|
|
108
|
-
onSelect={onSelect}
|
|
109
|
-
Components={Components}
|
|
110
|
-
indented={hasCheckedItem}
|
|
111
|
-
>
|
|
112
|
-
<div className="flex items-center flex-1">
|
|
113
|
-
{item.title ?? value}
|
|
114
|
-
</div>
|
|
115
|
-
<ChevronRightIcon className="-mr-1" />
|
|
116
|
-
</SelectItem>
|
|
117
|
-
</Components.SubTrigger>
|
|
118
|
-
<Components.Portal>
|
|
119
|
-
<Components.SubContent
|
|
120
|
-
alignOffset={-5}
|
|
121
|
-
className={styles.contentStyle}
|
|
122
|
-
>
|
|
123
|
-
<MenuViewport
|
|
124
|
-
items={item.items}
|
|
125
|
-
Components={Components}
|
|
126
|
-
onSelect={onSelect}
|
|
127
|
-
/>
|
|
128
|
-
</Components.SubContent>
|
|
129
|
-
</Components.Portal>
|
|
130
|
-
</Components.Sub>
|
|
131
|
-
);
|
|
145
|
+
{item.title ?? item.value}
|
|
146
|
+
</SelectItem>
|
|
147
|
+
);
|
|
132
148
|
}
|
|
133
|
-
|
|
134
|
-
return (
|
|
135
|
-
<SelectItem
|
|
136
|
-
key={value}
|
|
137
|
-
value={value}
|
|
138
|
-
icon={item.icon}
|
|
139
|
-
checked={item.checked}
|
|
140
|
-
shortcut={item.shortcut}
|
|
141
|
-
onSelect={onSelect}
|
|
142
|
-
Components={Components}
|
|
143
|
-
indented={hasCheckedItem}
|
|
144
|
-
>
|
|
145
|
-
{item.title ?? value}
|
|
146
|
-
</SelectItem>
|
|
147
|
-
);
|
|
148
149
|
})}
|
|
149
150
|
</>
|
|
150
151
|
);
|
package/src/index.tsx
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
export * from "./components/ActivityIndicator";
|
|
3
3
|
export * from "./components/AnimatePresence";
|
|
4
4
|
export * from "./components/Avatar";
|
|
5
|
+
export * from "./components/Breadcrumbs";
|
|
5
6
|
export * from "./components/Button";
|
|
6
7
|
export * from "./components/Checkbox";
|
|
7
8
|
export * from "./components/Chip";
|
|
9
|
+
export * from "./components/Collection";
|
|
8
10
|
export * from "./components/Combobox";
|
|
9
11
|
export * from "./components/ComboboxMenu";
|
|
10
12
|
export * from "./components/CommandPalette";
|
|
@@ -13,11 +15,13 @@ export * from "./components/Dialog";
|
|
|
13
15
|
export * from "./components/Divider";
|
|
14
16
|
export * from "./components/DraggableMenuButton";
|
|
15
17
|
export * from "./components/DropdownMenu";
|
|
18
|
+
export * from "./components/EditableText";
|
|
16
19
|
export * from "./components/Fade";
|
|
17
20
|
export * from "./components/FillInputField";
|
|
18
21
|
export * from "./components/FillPreviewBackground";
|
|
19
22
|
export * from "./components/FloatingWindow";
|
|
20
23
|
export * from "./components/GradientPicker";
|
|
24
|
+
export * from "./components/Grid";
|
|
21
25
|
export * from "./components/GridView";
|
|
22
26
|
export * from "./components/IconButton";
|
|
23
27
|
export * from "./components/Icons";
|
|
@@ -27,14 +31,13 @@ export * from "./components/internal/Menu";
|
|
|
27
31
|
export type {
|
|
28
32
|
ExtractMenuItemType,
|
|
29
33
|
MenuItem,
|
|
30
|
-
|
|
31
|
-
SectionHeaderMenuItem,
|
|
32
|
-
SeparatorItem,
|
|
34
|
+
SelectableMenuItem,
|
|
33
35
|
} from "./components/internal/Menu";
|
|
34
36
|
export * from "./components/internal/MenuViewport";
|
|
35
37
|
export * from "./components/Label";
|
|
36
38
|
export * from "./components/LabeledElementView";
|
|
37
39
|
export * from "./components/LabeledField";
|
|
40
|
+
export * from "./components/List";
|
|
38
41
|
export * from "./components/ListView";
|
|
39
42
|
export * from "./components/Message";
|
|
40
43
|
export * from "./components/Popover";
|
|
@@ -43,7 +46,6 @@ export * from "./components/ScrollArea";
|
|
|
43
46
|
export * from "./components/SearchCompletionMenu";
|
|
44
47
|
export * from "./components/SegmentedControl";
|
|
45
48
|
export * from "./components/SelectMenu";
|
|
46
|
-
export * from "./components/SidebarList";
|
|
47
49
|
export * from "./components/Slider";
|
|
48
50
|
export * from "./components/Sortable";
|
|
49
51
|
export type { RelativeDropPosition } from "./components/Sortable";
|
package/src/utils/combobox.ts
CHANGED
|
@@ -1,83 +1,76 @@
|
|
|
1
1
|
import { chunkBy, partition } from "@noya-app/noya-utils";
|
|
2
|
-
import {
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
import {
|
|
4
|
+
isMenuItemSectionHeader,
|
|
5
|
+
isNonSelectableMenuItem,
|
|
6
|
+
isSelectableMenuItem,
|
|
7
|
+
MenuItem,
|
|
8
|
+
ScoredMenuItem,
|
|
9
|
+
SectionHeaderMenuItem,
|
|
10
|
+
SelectableMenuItem,
|
|
11
|
+
} from "../components/internal/Menu";
|
|
3
12
|
import { fuzzyFilter, IScoredItem } from "./fuzzyScorer";
|
|
4
13
|
|
|
5
|
-
export type
|
|
6
|
-
type?: undefined;
|
|
7
|
-
id: string;
|
|
8
|
-
name: string;
|
|
9
|
-
icon?: ReactNode;
|
|
10
|
-
shortcut?: string;
|
|
11
|
-
disabled?: boolean;
|
|
12
|
-
alwaysInclude?: boolean;
|
|
13
|
-
checked?: boolean;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export type ComboboxSectionHeader = {
|
|
17
|
-
type: "sectionHeader";
|
|
18
|
-
id: string;
|
|
19
|
-
name: string;
|
|
20
|
-
maxVisibleItems?: number;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type ComboboxItem = ComboboxOption | ComboboxSectionHeader;
|
|
24
|
-
|
|
25
|
-
export type InternalComboboxItem =
|
|
26
|
-
| (ComboboxOption & IScoredItem)
|
|
27
|
-
| ComboboxSectionHeader;
|
|
28
|
-
|
|
29
|
-
export type ComboboxStateSnapshot = {
|
|
14
|
+
export type ComboboxStateSnapshot<T extends string> = {
|
|
30
15
|
filter: string;
|
|
31
16
|
selectedIndex: number;
|
|
32
|
-
filteredItems:
|
|
17
|
+
filteredItems: ScoredMenuItem<T>[];
|
|
33
18
|
lastSubmittedValue: { filter: string; itemName: string };
|
|
34
19
|
};
|
|
35
20
|
|
|
36
|
-
export class ComboboxState {
|
|
21
|
+
export class ComboboxState<T extends string> {
|
|
37
22
|
private filter: string;
|
|
38
23
|
private selectedIndex: number;
|
|
39
|
-
private items:
|
|
24
|
+
private items: MenuItem<T>[];
|
|
40
25
|
private lastSubmittedValue: { filter: string; itemName: string };
|
|
41
26
|
private subscribers: (() => void)[] = [];
|
|
42
27
|
private version = 0;
|
|
43
|
-
private _cachedSnapshot: [number, ComboboxStateSnapshot] | undefined;
|
|
28
|
+
private _cachedSnapshot: [number, ComboboxStateSnapshot<T>] | undefined;
|
|
44
29
|
|
|
45
30
|
constructor(
|
|
46
|
-
items:
|
|
47
|
-
|
|
31
|
+
items: MenuItem<T>[],
|
|
32
|
+
initialTitle: string,
|
|
48
33
|
allowArbitraryValues: boolean
|
|
49
34
|
) {
|
|
50
35
|
this.items = items;
|
|
51
36
|
this.lastSubmittedValue = {
|
|
52
|
-
filter:
|
|
53
|
-
itemName:
|
|
37
|
+
filter: initialTitle,
|
|
38
|
+
itemName: initialTitle,
|
|
54
39
|
};
|
|
55
40
|
|
|
56
41
|
// Initialize with the initial value
|
|
57
|
-
this.filter =
|
|
42
|
+
this.filter = initialTitle;
|
|
58
43
|
this.selectedIndex = 0;
|
|
59
44
|
|
|
60
45
|
// Create initial snapshot
|
|
61
46
|
this._cachedSnapshot = [
|
|
62
47
|
0,
|
|
63
48
|
{
|
|
64
|
-
filter:
|
|
49
|
+
filter: initialTitle,
|
|
65
50
|
selectedIndex: 0,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
51
|
+
|
|
52
|
+
filteredItems: this.items.flatMap((item, index) =>
|
|
53
|
+
isMenuItemSectionHeader(item)
|
|
54
|
+
? item
|
|
55
|
+
: isSelectableMenuItem(item)
|
|
56
|
+
? {
|
|
57
|
+
...item,
|
|
58
|
+
index,
|
|
59
|
+
score: 0,
|
|
60
|
+
}
|
|
61
|
+
: []
|
|
62
|
+
),
|
|
70
63
|
lastSubmittedValue: this.lastSubmittedValue,
|
|
71
64
|
},
|
|
72
65
|
];
|
|
73
66
|
|
|
74
67
|
// If there's an initial value, validate it
|
|
75
|
-
if (
|
|
68
|
+
if (initialTitle) {
|
|
76
69
|
// Check if the initial value exists in items
|
|
77
|
-
const hasMatch = items.some(
|
|
70
|
+
const hasMatch = this.items.some(
|
|
78
71
|
(item) =>
|
|
79
|
-
item.type
|
|
80
|
-
item.
|
|
72
|
+
item.type === undefined &&
|
|
73
|
+
item.title?.toString().toLowerCase() === initialTitle.toLowerCase()
|
|
81
74
|
);
|
|
82
75
|
|
|
83
76
|
// Get filtered items to check for fuzzy matches
|
|
@@ -92,10 +85,20 @@ export class ComboboxState {
|
|
|
92
85
|
{
|
|
93
86
|
filter: "",
|
|
94
87
|
selectedIndex: 0,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
88
|
+
|
|
89
|
+
filteredItems: this.items.flatMap((item): ScoredMenuItem<T>[] =>
|
|
90
|
+
isSelectableMenuItem(item)
|
|
91
|
+
? [
|
|
92
|
+
{
|
|
93
|
+
...item,
|
|
94
|
+
index: 0,
|
|
95
|
+
score: 0,
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
: item.type === "sectionHeader"
|
|
99
|
+
? [item]
|
|
100
|
+
: []
|
|
101
|
+
),
|
|
99
102
|
lastSubmittedValue: this.lastSubmittedValue,
|
|
100
103
|
},
|
|
101
104
|
];
|
|
@@ -116,11 +119,11 @@ export class ComboboxState {
|
|
|
116
119
|
this.subscribers.forEach((listener) => listener());
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
private getFilteredItems():
|
|
122
|
+
private getFilteredItems(): ScoredMenuItem<T>[] {
|
|
120
123
|
return filterWithGroupedSections(this.items, this.filter);
|
|
121
124
|
}
|
|
122
125
|
|
|
123
|
-
getSnapshot = (): ComboboxStateSnapshot => {
|
|
126
|
+
getSnapshot = (): ComboboxStateSnapshot<T> => {
|
|
124
127
|
if (this._cachedSnapshot && this._cachedSnapshot[0] === this.version) {
|
|
125
128
|
return this._cachedSnapshot[1];
|
|
126
129
|
}
|
|
@@ -136,7 +139,7 @@ export class ComboboxState {
|
|
|
136
139
|
return snapshot;
|
|
137
140
|
};
|
|
138
141
|
|
|
139
|
-
setItems(items:
|
|
142
|
+
setItems(items: MenuItem<T>[]) {
|
|
140
143
|
this.items = items;
|
|
141
144
|
this.notifyChange();
|
|
142
145
|
}
|
|
@@ -148,13 +151,13 @@ export class ComboboxState {
|
|
|
148
151
|
// Find exact match if it exists (case insensitive)
|
|
149
152
|
const exactMatchIndex = filteredItems.findIndex(
|
|
150
153
|
(item) =>
|
|
151
|
-
item
|
|
152
|
-
item.
|
|
154
|
+
isSelectableMenuItem(item) &&
|
|
155
|
+
item.title?.toString().toLowerCase().startsWith(filter.toLowerCase())
|
|
153
156
|
);
|
|
154
157
|
|
|
155
158
|
// Find the first non-header item
|
|
156
|
-
const firstSelectableIndex = filteredItems.findIndex(
|
|
157
|
-
(item)
|
|
159
|
+
const firstSelectableIndex = filteredItems.findIndex((item): boolean =>
|
|
160
|
+
isSelectableMenuItem(item)
|
|
158
161
|
);
|
|
159
162
|
|
|
160
163
|
this.selectedIndex =
|
|
@@ -169,29 +172,28 @@ export class ComboboxState {
|
|
|
169
172
|
filteredItems,
|
|
170
173
|
this.selectedIndex,
|
|
171
174
|
direction === "down" ? "next" : "previous",
|
|
172
|
-
(item) => item
|
|
175
|
+
(item) => isNonSelectableMenuItem(item)
|
|
173
176
|
);
|
|
174
177
|
this.notifyChange();
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
selectCurrentItem(
|
|
178
|
-
item?:
|
|
179
|
-
):
|
|
181
|
+
item?: ScoredMenuItem<T>
|
|
182
|
+
): SelectableMenuItem<T> | undefined {
|
|
180
183
|
const itemToSelect = item || this.getFilteredItems()[this.selectedIndex];
|
|
181
|
-
if (
|
|
184
|
+
if (itemToSelect && isSelectableMenuItem(itemToSelect)) {
|
|
185
|
+
this.lastSubmittedValue = {
|
|
186
|
+
filter: itemToSelect.title!.toString(),
|
|
187
|
+
itemName: itemToSelect.title!.toString(),
|
|
188
|
+
};
|
|
189
|
+
this.filter = itemToSelect.title!.toString();
|
|
190
|
+
this.notifyChange();
|
|
191
|
+
return itemToSelect;
|
|
192
|
+
} else {
|
|
182
193
|
// If no valid item to select and arbitrary values aren't allowed,
|
|
183
194
|
// restore the last valid value
|
|
184
195
|
this.restoreLastSubmittedValue();
|
|
185
|
-
return;
|
|
186
196
|
}
|
|
187
|
-
|
|
188
|
-
this.lastSubmittedValue = {
|
|
189
|
-
filter: itemToSelect.name,
|
|
190
|
-
itemName: itemToSelect.name,
|
|
191
|
-
};
|
|
192
|
-
this.filter = itemToSelect.name;
|
|
193
|
-
this.notifyChange();
|
|
194
|
-
return itemToSelect;
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
restoreLastSubmittedValue() {
|
|
@@ -205,25 +207,25 @@ export class ComboboxState {
|
|
|
205
207
|
this.notifyChange();
|
|
206
208
|
}
|
|
207
209
|
|
|
208
|
-
getSelectedItem():
|
|
210
|
+
getSelectedItem(): ScoredMenuItem<T> | undefined {
|
|
209
211
|
const filteredItems = this.getFilteredItems();
|
|
210
212
|
return filteredItems[this.selectedIndex];
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
getTypeaheadValue(): string | undefined {
|
|
214
216
|
const selectedItem = this.getSelectedItem();
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
+
if (selectedItem && isSelectableMenuItem(selectedItem))
|
|
218
|
+
return selectedItem.title!.toString();
|
|
217
219
|
}
|
|
218
220
|
|
|
219
221
|
setSelectedIndex(index: number) {
|
|
220
222
|
const filteredItems = this.getFilteredItems();
|
|
221
223
|
|
|
222
|
-
// If trying to select a header, find the next selectable item
|
|
223
|
-
if (filteredItems[index]
|
|
224
|
-
// Look for the next
|
|
224
|
+
// If trying to select a header or separator, find the next selectable item
|
|
225
|
+
if (isNonSelectableMenuItem(filteredItems[index])) {
|
|
226
|
+
// Look for the next selectable item
|
|
225
227
|
for (let i = index + 1; i < filteredItems.length; i++) {
|
|
226
|
-
if (filteredItems[i]
|
|
228
|
+
if (isSelectableMenuItem(filteredItems[i])) {
|
|
227
229
|
this.selectedIndex = i;
|
|
228
230
|
this.notifyChange();
|
|
229
231
|
return;
|
|
@@ -231,7 +233,7 @@ export class ComboboxState {
|
|
|
231
233
|
}
|
|
232
234
|
// If no next item, look backwards
|
|
233
235
|
for (let i = index - 1; i >= 0; i--) {
|
|
234
|
-
if (filteredItems[i]
|
|
236
|
+
if (isSelectableMenuItem(filteredItems[i])) {
|
|
235
237
|
this.selectedIndex = i;
|
|
236
238
|
this.notifyChange();
|
|
237
239
|
return;
|
|
@@ -255,18 +257,18 @@ export class ComboboxState {
|
|
|
255
257
|
}
|
|
256
258
|
}
|
|
257
259
|
|
|
258
|
-
function filterWithGroupedSections(
|
|
259
|
-
items:
|
|
260
|
+
function filterWithGroupedSections<T extends string>(
|
|
261
|
+
items: MenuItem<T>[],
|
|
260
262
|
query: string
|
|
261
|
-
):
|
|
262
|
-
const sections = chunkBy(items, (_a, b) => b
|
|
263
|
-
let result:
|
|
263
|
+
): ScoredMenuItem<T>[] {
|
|
264
|
+
const sections = chunkBy(items, (_a, b) => isSelectableMenuItem(b));
|
|
265
|
+
let result: ScoredMenuItem<T>[] = [];
|
|
264
266
|
let currentIndex = 0;
|
|
265
267
|
|
|
266
268
|
const createExtraItem = (
|
|
267
|
-
item:
|
|
269
|
+
item: SelectableMenuItem<T>,
|
|
268
270
|
index: number
|
|
269
|
-
):
|
|
271
|
+
): ScoredMenuItem<T> => ({
|
|
270
272
|
...item,
|
|
271
273
|
index: currentIndex + index,
|
|
272
274
|
score: 0,
|
|
@@ -274,56 +276,66 @@ function filterWithGroupedSections(
|
|
|
274
276
|
|
|
275
277
|
const createScoredItem = (
|
|
276
278
|
item: IScoredItem,
|
|
277
|
-
|
|
278
|
-
):
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
279
|
+
selectableItems: Array<SelectableMenuItem<T>>
|
|
280
|
+
): ScoredMenuItem<T> => {
|
|
281
|
+
const menuItem = selectableItems[item.index];
|
|
282
|
+
return {
|
|
283
|
+
...item,
|
|
284
|
+
...menuItem,
|
|
285
|
+
index: currentIndex + item.index,
|
|
286
|
+
};
|
|
287
|
+
};
|
|
283
288
|
|
|
284
289
|
for (const section of sections) {
|
|
285
|
-
const [
|
|
286
|
-
section
|
|
287
|
-
|
|
290
|
+
const [sectionHeaders, selectableItems] = partition(
|
|
291
|
+
section.filter(
|
|
292
|
+
(item): item is SelectableMenuItem<T> | SectionHeaderMenuItem =>
|
|
293
|
+
item.type !== "submenu" && item.type !== "separator"
|
|
294
|
+
),
|
|
295
|
+
(item): item is SectionHeaderMenuItem => isMenuItemSectionHeader(item)
|
|
288
296
|
);
|
|
289
297
|
|
|
290
|
-
let newItems:
|
|
298
|
+
let newItems: ScoredMenuItem<T>[];
|
|
291
299
|
|
|
292
300
|
if (!query.trim()) {
|
|
293
301
|
// For empty query, include all items with score 0
|
|
294
|
-
newItems =
|
|
302
|
+
newItems = selectableItems.map((item, index) =>
|
|
303
|
+
createExtraItem(item, index)
|
|
304
|
+
);
|
|
295
305
|
} else {
|
|
296
306
|
const scoredItems = fuzzyFilter({
|
|
297
|
-
items:
|
|
307
|
+
items: selectableItems.map((item) => item.title.toString()),
|
|
298
308
|
query,
|
|
299
309
|
});
|
|
300
310
|
|
|
301
311
|
const usedIndexes = new Set(scoredItems.map((item) => item.index));
|
|
302
|
-
const extraItems =
|
|
303
|
-
(item, index):
|
|
304
|
-
item
|
|
312
|
+
const extraItems = selectableItems.flatMap(
|
|
313
|
+
(item, index): ScoredMenuItem<T>[] =>
|
|
314
|
+
item?.alwaysInclude && !usedIndexes.has(index)
|
|
305
315
|
? [createExtraItem(item, index)]
|
|
306
316
|
: []
|
|
307
317
|
);
|
|
308
318
|
|
|
309
319
|
newItems = scoredItems
|
|
310
|
-
.map((item) => createScoredItem(item,
|
|
320
|
+
.map((item) => createScoredItem(item, selectableItems))
|
|
311
321
|
.concat(extraItems);
|
|
312
322
|
}
|
|
313
323
|
|
|
314
324
|
// If we have items or this is an empty query
|
|
315
325
|
if (newItems.length > 0 || !query.trim()) {
|
|
316
326
|
const maxVisibleItems =
|
|
317
|
-
|
|
327
|
+
sectionHeaders.length > 0 && sectionHeaders[0]?.maxVisibleItems
|
|
328
|
+
? sectionHeaders[0]?.maxVisibleItems
|
|
329
|
+
: undefined;
|
|
318
330
|
|
|
319
331
|
if (maxVisibleItems !== undefined) {
|
|
320
332
|
newItems = newItems.slice(0, maxVisibleItems);
|
|
321
333
|
}
|
|
322
334
|
|
|
323
|
-
result.push(...
|
|
335
|
+
result.push(...sectionHeaders, ...newItems);
|
|
324
336
|
}
|
|
325
337
|
|
|
326
|
-
currentIndex +=
|
|
338
|
+
currentIndex += selectableItems.length;
|
|
327
339
|
}
|
|
328
340
|
|
|
329
341
|
return result;
|
|
@@ -363,7 +375,7 @@ export function getNextIndex<T>(
|
|
|
363
375
|
return nextIndex;
|
|
364
376
|
}
|
|
365
377
|
|
|
366
|
-
export function useComboboxState(state: ComboboxState) {
|
|
378
|
+
export function useComboboxState<T extends string>(state: ComboboxState<T>) {
|
|
367
379
|
return useSyncExternalStore(
|
|
368
380
|
state.subscribe,
|
|
369
381
|
state.getSnapshot,
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MenuItem,
|
|
3
|
-
RegularMenuItem,
|
|
4
|
-
SeparatorItem,
|
|
5
|
-
} from "../components/internal/Menu";
|
|
1
|
+
import { MenuItem, SeparatorItem } from "../components/internal/Menu";
|
|
6
2
|
|
|
7
3
|
export type Optional<T> = T | false | null | undefined;
|
|
8
4
|
|
|
9
5
|
function withSeparators<T extends string>(
|
|
10
|
-
elements:
|
|
6
|
+
elements: MenuItem<T>[][],
|
|
11
7
|
separator: SeparatorItem
|
|
12
8
|
): MenuItem<T>[] {
|
|
13
9
|
const result: MenuItem<T>[] = [];
|
|
@@ -23,9 +19,7 @@ function withSeparators<T extends string>(
|
|
|
23
19
|
return result;
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
export type MenuConfig<T extends string> = Optional<
|
|
27
|
-
Optional<RegularMenuItem<T>>[]
|
|
28
|
-
>[];
|
|
22
|
+
export type MenuConfig<T extends string> = Optional<Optional<MenuItem<T>>[]>[];
|
|
29
23
|
|
|
30
24
|
export function createSectionedMenu<T extends string>(
|
|
31
25
|
...sections: MenuConfig<T>
|