@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noya-app/noya-designsystem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"module": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"@dnd-kit/core": "3.1.1",
|
|
24
24
|
"@dnd-kit/modifiers": "3.0.0",
|
|
25
25
|
"@dnd-kit/sortable": "4.0.0",
|
|
26
|
-
"@noya-app/noya-colorpicker": "0.1.
|
|
26
|
+
"@noya-app/noya-colorpicker": "0.1.17",
|
|
27
27
|
"@noya-app/noya-utils": "0.1.3",
|
|
28
|
-
"@noya-app/noya-geometry": "0.1.
|
|
28
|
+
"@noya-app/noya-geometry": "0.1.7",
|
|
29
29
|
"@noya-app/noya-icons": "0.1.6",
|
|
30
30
|
"@noya-app/noya-keymap": "0.1.3",
|
|
31
31
|
"@radix-ui/primitive": "^1.0.0",
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import {
|
|
3
|
+
isSelectableMenuItem,
|
|
4
|
+
isSelectableMenuItemWithScore,
|
|
5
|
+
MenuItem,
|
|
6
|
+
} from "../components/internal/Menu";
|
|
7
|
+
import { ComboboxState } from "../utils/combobox";
|
|
8
|
+
|
|
9
|
+
const testItems: MenuItem<string>[] = [
|
|
10
|
+
{ value: "1", title: "font-bold" },
|
|
11
|
+
{ value: "2", title: "font-italic" },
|
|
12
|
+
{ value: "3", title: "font-regular" },
|
|
9
13
|
{
|
|
10
14
|
type: "sectionHeader" as const,
|
|
11
15
|
id: "header1",
|
|
12
|
-
|
|
16
|
+
title: "Border Styles",
|
|
13
17
|
maxVisibleItems: 2,
|
|
14
18
|
},
|
|
15
|
-
{
|
|
16
|
-
{
|
|
17
|
-
{
|
|
19
|
+
{ value: "4", title: "border-solid" },
|
|
20
|
+
{ value: "5", title: "border-dashed" },
|
|
21
|
+
{ value: "6", title: "border-dotted" },
|
|
18
22
|
];
|
|
19
23
|
|
|
20
24
|
test("initializes with default values", () => {
|
|
@@ -38,7 +42,8 @@ test("filters items based on query", () => {
|
|
|
38
42
|
expect(snapshot.filteredItems).toHaveLength(3);
|
|
39
43
|
expect(
|
|
40
44
|
snapshot.filteredItems.every(
|
|
41
|
-
(item) =>
|
|
45
|
+
(item) =>
|
|
46
|
+
isSelectableMenuItem(item) && item.title?.toString().includes("font")
|
|
42
47
|
)
|
|
43
48
|
).toBe(true);
|
|
44
49
|
});
|
|
@@ -53,11 +58,16 @@ test("respects section header maxVisibleItems", () => {
|
|
|
53
58
|
// 2. Only 2 items (maxVisibleItems) from that section
|
|
54
59
|
expect(snapshot.filteredItems).toHaveLength(3);
|
|
55
60
|
expect(snapshot.filteredItems[0].type).toBe("sectionHeader");
|
|
56
|
-
expect(snapshot.filteredItems[0].
|
|
61
|
+
expect(snapshot.filteredItems[0].title).toBe("Border Styles");
|
|
57
62
|
|
|
58
63
|
const items = snapshot.filteredItems.slice(1);
|
|
59
64
|
expect(items).toHaveLength(2); // maxVisibleItems is 2
|
|
60
|
-
expect(
|
|
65
|
+
expect(
|
|
66
|
+
items.every(
|
|
67
|
+
(item) =>
|
|
68
|
+
isSelectableMenuItem(item) && item.title?.toString().includes("border")
|
|
69
|
+
)
|
|
70
|
+
).toBe(true);
|
|
61
71
|
});
|
|
62
72
|
|
|
63
73
|
test("respects section header maxVisibleItems with empty query", () => {
|
|
@@ -67,7 +77,7 @@ test("respects section header maxVisibleItems with empty query", () => {
|
|
|
67
77
|
|
|
68
78
|
// Find the border section
|
|
69
79
|
const borderSectionIndex = snapshot.filteredItems.findIndex(
|
|
70
|
-
(item) => item.type === "sectionHeader" && item.
|
|
80
|
+
(item) => item.type === "sectionHeader" && item.title === "Border Styles"
|
|
71
81
|
);
|
|
72
82
|
|
|
73
83
|
// Get items in the border section
|
|
@@ -78,7 +88,11 @@ test("respects section header maxVisibleItems with empty query", () => {
|
|
|
78
88
|
// Should only show maxVisibleItems (2) items from the border section
|
|
79
89
|
expect(borderSectionItems).toHaveLength(2);
|
|
80
90
|
expect(
|
|
81
|
-
borderSectionItems.every(
|
|
91
|
+
borderSectionItems.every(
|
|
92
|
+
(item) =>
|
|
93
|
+
isSelectableMenuItem(item) &&
|
|
94
|
+
item.title?.toString().startsWith("border-")
|
|
95
|
+
)
|
|
82
96
|
).toBe(true);
|
|
83
97
|
});
|
|
84
98
|
|
|
@@ -110,7 +124,7 @@ test("selects current item", () => {
|
|
|
110
124
|
state.moveSelection("down"); // Select second item
|
|
111
125
|
const selectedItem = state.selectCurrentItem();
|
|
112
126
|
|
|
113
|
-
expect(selectedItem?.
|
|
127
|
+
expect(selectedItem?.title).toBe("font-italic");
|
|
114
128
|
const snapshot = state.getSnapshot();
|
|
115
129
|
expect(snapshot.filter).toBe("font-italic");
|
|
116
130
|
expect(snapshot.lastSubmittedValue).toEqual({
|
|
@@ -155,18 +169,27 @@ test("handles empty query", () => {
|
|
|
155
169
|
|
|
156
170
|
// Verify font items (first 3)
|
|
157
171
|
const fontItems = snapshot.filteredItems.slice(0, 3);
|
|
158
|
-
expect(
|
|
172
|
+
expect(
|
|
173
|
+
fontItems.every(
|
|
174
|
+
(item) =>
|
|
175
|
+
isSelectableMenuItem(item) && item.title?.toString().startsWith("font-")
|
|
176
|
+
)
|
|
177
|
+
).toBe(true);
|
|
159
178
|
|
|
160
179
|
// Verify border section
|
|
161
180
|
expect(snapshot.filteredItems[3].type).toBe("sectionHeader");
|
|
162
|
-
expect(snapshot.filteredItems[3].
|
|
181
|
+
expect(snapshot.filteredItems[3].title).toBe("Border Styles");
|
|
163
182
|
|
|
164
183
|
// Verify border items (last 2, limited by maxVisibleItems)
|
|
165
184
|
const borderItems = snapshot.filteredItems.slice(4);
|
|
166
185
|
expect(borderItems).toHaveLength(2);
|
|
167
|
-
expect(
|
|
168
|
-
|
|
169
|
-
|
|
186
|
+
expect(
|
|
187
|
+
borderItems.every(
|
|
188
|
+
(item) =>
|
|
189
|
+
isSelectableMenuItem(item) &&
|
|
190
|
+
item.title?.toString().startsWith("border-")
|
|
191
|
+
)
|
|
192
|
+
).toBe(true);
|
|
170
193
|
});
|
|
171
194
|
|
|
172
195
|
test("provides typeahead value", () => {
|
|
@@ -220,7 +243,10 @@ test("supports fuzzy matching", () => {
|
|
|
220
243
|
const snapshot = state.getSnapshot();
|
|
221
244
|
|
|
222
245
|
expect(snapshot.filteredItems).toHaveLength(1);
|
|
223
|
-
expect(
|
|
246
|
+
expect(
|
|
247
|
+
isSelectableMenuItemWithScore(snapshot.filteredItems[0]) &&
|
|
248
|
+
snapshot.filteredItems[0].title === "font-bold"
|
|
249
|
+
).toBe(true);
|
|
224
250
|
});
|
|
225
251
|
|
|
226
252
|
test("wraps selection at boundaries", () => {
|
|
@@ -237,10 +263,10 @@ test("wraps selection at boundaries", () => {
|
|
|
237
263
|
});
|
|
238
264
|
|
|
239
265
|
test("respects alwaysInclude flag", () => {
|
|
240
|
-
const itemsWithAlwaysInclude = [
|
|
241
|
-
{
|
|
242
|
-
{
|
|
243
|
-
{
|
|
266
|
+
const itemsWithAlwaysInclude: MenuItem<string>[] = [
|
|
267
|
+
{ value: "font-bold", title: "font-bold", alwaysInclude: true },
|
|
268
|
+
{ value: "font-italic", title: "font-italic" },
|
|
269
|
+
{ value: "font-regular", title: "font-regular" },
|
|
244
270
|
];
|
|
245
271
|
|
|
246
272
|
const state = new ComboboxState(itemsWithAlwaysInclude, "", true);
|
|
@@ -248,11 +274,15 @@ test("respects alwaysInclude flag", () => {
|
|
|
248
274
|
const snapshot = state.getSnapshot();
|
|
249
275
|
|
|
250
276
|
expect(snapshot.filteredItems).toHaveLength(2); // Should include "italic" match and "bold" due to alwaysInclude
|
|
251
|
-
expect(snapshot.filteredItems.some((item) => item.name === "font-bold")).toBe(
|
|
252
|
-
true
|
|
253
|
-
);
|
|
254
277
|
expect(
|
|
255
|
-
snapshot.filteredItems.some(
|
|
278
|
+
snapshot.filteredItems.some(
|
|
279
|
+
(item) => isSelectableMenuItem(item) && item.title === "font-bold"
|
|
280
|
+
)
|
|
281
|
+
).toBe(true);
|
|
282
|
+
expect(
|
|
283
|
+
snapshot.filteredItems.some(
|
|
284
|
+
(item) => isSelectableMenuItem(item) && item.title === "font-italic"
|
|
285
|
+
)
|
|
256
286
|
).toBe(true);
|
|
257
287
|
});
|
|
258
288
|
|
|
@@ -265,24 +295,24 @@ test("handles empty items array", () => {
|
|
|
265
295
|
});
|
|
266
296
|
|
|
267
297
|
test("handles multiple section headers", () => {
|
|
268
|
-
const itemsWithMultipleSections = [
|
|
298
|
+
const itemsWithMultipleSections: MenuItem<string>[] = [
|
|
269
299
|
{
|
|
270
|
-
type: "sectionHeader"
|
|
300
|
+
type: "sectionHeader",
|
|
271
301
|
id: "header1",
|
|
272
|
-
|
|
302
|
+
title: "Fonts",
|
|
273
303
|
maxVisibleItems: 2,
|
|
274
304
|
},
|
|
275
|
-
{
|
|
276
|
-
{
|
|
277
|
-
{
|
|
305
|
+
{ value: "font-bold", title: "font-bold" },
|
|
306
|
+
{ value: "font-italic", title: "font-italic" },
|
|
307
|
+
{ value: "font-regular", title: "font-regular" },
|
|
278
308
|
{
|
|
279
|
-
type: "sectionHeader"
|
|
309
|
+
type: "sectionHeader",
|
|
280
310
|
id: "header2",
|
|
281
|
-
|
|
311
|
+
title: "Borders",
|
|
282
312
|
maxVisibleItems: 1,
|
|
283
313
|
},
|
|
284
|
-
{
|
|
285
|
-
{
|
|
314
|
+
{ value: "border-solid", title: "border-solid" },
|
|
315
|
+
{ value: "border-dashed", title: "border-dashed" },
|
|
286
316
|
];
|
|
287
317
|
|
|
288
318
|
const state = new ComboboxState(itemsWithMultipleSections, "", true);
|
|
@@ -301,37 +331,38 @@ test("handles multiple section headers", () => {
|
|
|
301
331
|
state.setFilter("font");
|
|
302
332
|
snapshot = state.getSnapshot();
|
|
303
333
|
expect(snapshot.filteredItems).toHaveLength(3); // header + 2 items (maxVisibleItems: 2)
|
|
334
|
+
|
|
304
335
|
expect(snapshot.filteredItems[0].type).toBe("sectionHeader");
|
|
305
|
-
expect(snapshot.filteredItems[0].
|
|
336
|
+
expect(snapshot.filteredItems[0].title).toBe("Fonts");
|
|
306
337
|
expect(snapshot.filteredItems.slice(1)).toHaveLength(2);
|
|
307
338
|
|
|
308
339
|
state.setFilter("border");
|
|
309
340
|
snapshot = state.getSnapshot();
|
|
310
341
|
expect(snapshot.filteredItems).toHaveLength(2); // header + 1 item (maxVisibleItems: 1)
|
|
311
342
|
expect(snapshot.filteredItems[0].type).toBe("sectionHeader");
|
|
312
|
-
expect(snapshot.filteredItems[0].
|
|
343
|
+
expect(snapshot.filteredItems[0].title).toBe("Borders");
|
|
313
344
|
expect(snapshot.filteredItems.slice(1)).toHaveLength(1);
|
|
314
345
|
});
|
|
315
346
|
|
|
316
347
|
test("handles multiple section headers with empty query", () => {
|
|
317
|
-
const itemsWithMultipleSections = [
|
|
348
|
+
const itemsWithMultipleSections: MenuItem<string>[] = [
|
|
318
349
|
{
|
|
319
|
-
type: "sectionHeader"
|
|
350
|
+
type: "sectionHeader",
|
|
320
351
|
id: "header1",
|
|
321
|
-
|
|
352
|
+
title: "Fonts",
|
|
322
353
|
maxVisibleItems: 2,
|
|
323
354
|
},
|
|
324
|
-
{
|
|
325
|
-
{
|
|
326
|
-
{
|
|
355
|
+
{ value: "font-bold", title: "font-bold" },
|
|
356
|
+
{ value: "font-italic", title: "font-italic" },
|
|
357
|
+
{ value: "font-regular", title: "font-regular" },
|
|
327
358
|
{
|
|
328
|
-
type: "sectionHeader"
|
|
359
|
+
type: "sectionHeader",
|
|
329
360
|
id: "header2",
|
|
330
|
-
|
|
361
|
+
title: "Borders",
|
|
331
362
|
maxVisibleItems: 1,
|
|
332
363
|
},
|
|
333
|
-
{
|
|
334
|
-
{
|
|
364
|
+
{ value: "border-solid", title: "border-solid" },
|
|
365
|
+
{ value: "border-dashed", title: "border-dashed" },
|
|
335
366
|
];
|
|
336
367
|
|
|
337
368
|
const state = new ComboboxState(itemsWithMultipleSections, "", true);
|
|
@@ -347,22 +378,30 @@ test("handles multiple section headers with empty query", () => {
|
|
|
347
378
|
|
|
348
379
|
// Verify fonts section
|
|
349
380
|
expect(snapshot.filteredItems[0].type).toBe("sectionHeader");
|
|
350
|
-
expect(snapshot.filteredItems[0].
|
|
381
|
+
expect(snapshot.filteredItems[0].title).toBe("Fonts");
|
|
351
382
|
expect(snapshot.filteredItems.slice(1, 3)).toHaveLength(2);
|
|
352
383
|
expect(
|
|
353
384
|
snapshot.filteredItems
|
|
354
385
|
.slice(1, 3)
|
|
355
|
-
.every(
|
|
386
|
+
.every(
|
|
387
|
+
(item) =>
|
|
388
|
+
isSelectableMenuItemWithScore(item) &&
|
|
389
|
+
item.title?.toString().startsWith("font-")
|
|
390
|
+
)
|
|
356
391
|
).toBe(true);
|
|
357
392
|
|
|
358
393
|
// Verify borders section
|
|
359
394
|
expect(snapshot.filteredItems[3].type).toBe("sectionHeader");
|
|
360
|
-
expect(snapshot.filteredItems[3].
|
|
395
|
+
expect(snapshot.filteredItems[3].title).toBe("Borders");
|
|
361
396
|
expect(snapshot.filteredItems.slice(4)).toHaveLength(1);
|
|
362
397
|
expect(
|
|
363
398
|
snapshot.filteredItems
|
|
364
399
|
.slice(4)
|
|
365
|
-
.every(
|
|
400
|
+
.every(
|
|
401
|
+
(item) =>
|
|
402
|
+
isSelectableMenuItemWithScore(item) &&
|
|
403
|
+
item.title?.toString().startsWith("border-")
|
|
404
|
+
)
|
|
366
405
|
).toBe(true);
|
|
367
406
|
});
|
|
368
407
|
|
|
@@ -375,8 +414,8 @@ test("handles case insensitive matching", () => {
|
|
|
375
414
|
expect(
|
|
376
415
|
snapshot.filteredItems.every(
|
|
377
416
|
(item) =>
|
|
378
|
-
item
|
|
379
|
-
item.
|
|
417
|
+
isSelectableMenuItem(item) &&
|
|
418
|
+
item.title?.toString().toLowerCase().includes("font")
|
|
380
419
|
)
|
|
381
420
|
).toBe(true);
|
|
382
421
|
});
|
|
@@ -387,16 +426,16 @@ test("maintains correct indices after filtering", () => {
|
|
|
387
426
|
const snapshot = state.getSnapshot();
|
|
388
427
|
|
|
389
428
|
// Check that indices match original positions
|
|
390
|
-
const boldItem = snapshot.filteredItems
|
|
391
|
-
(
|
|
392
|
-
|
|
393
|
-
);
|
|
429
|
+
const boldItem = snapshot.filteredItems
|
|
430
|
+
.filter(isSelectableMenuItemWithScore)
|
|
431
|
+
.find((item) => item.title === "font-bold");
|
|
432
|
+
expect(boldItem?.title).toBe("font-bold");
|
|
394
433
|
expect(boldItem?.index).toBe(0);
|
|
395
434
|
|
|
396
|
-
const italicItem = snapshot.filteredItems
|
|
397
|
-
(
|
|
398
|
-
|
|
399
|
-
);
|
|
435
|
+
const italicItem = snapshot.filteredItems
|
|
436
|
+
.filter(isSelectableMenuItemWithScore)
|
|
437
|
+
.find((item) => item.title === "font-italic");
|
|
438
|
+
expect(italicItem?.title).toBe("font-italic");
|
|
400
439
|
expect(italicItem?.index).toBe(1);
|
|
401
440
|
});
|
|
402
441
|
|
|
@@ -408,18 +447,19 @@ test("handles section header selection", () => {
|
|
|
408
447
|
let snapshot = state.getSnapshot();
|
|
409
448
|
// Should select the next selectable item after the header
|
|
410
449
|
expect(snapshot.selectedIndex).toBe(4);
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
450
|
+
const selectedItem = snapshot.filteredItems[snapshot.selectedIndex];
|
|
451
|
+
expect(
|
|
452
|
+
isSelectableMenuItemWithScore(selectedItem) && selectedItem.title
|
|
453
|
+
).toBe("border-solid");
|
|
414
454
|
|
|
415
455
|
// Try to select a header when it's the last item
|
|
416
|
-
const itemsWithHeaderAtEnd = [
|
|
417
|
-
{
|
|
418
|
-
{
|
|
456
|
+
const itemsWithHeaderAtEnd: MenuItem<string>[] = [
|
|
457
|
+
{ value: "item1", title: "item1" },
|
|
458
|
+
{ value: "item2", title: "item2" },
|
|
419
459
|
{
|
|
420
460
|
type: "sectionHeader" as const,
|
|
421
461
|
id: "header",
|
|
422
|
-
|
|
462
|
+
title: "Header",
|
|
423
463
|
},
|
|
424
464
|
];
|
|
425
465
|
const stateWithHeaderAtEnd = new ComboboxState(
|
|
@@ -431,17 +471,21 @@ test("handles section header selection", () => {
|
|
|
431
471
|
snapshot = stateWithHeaderAtEnd.getSnapshot();
|
|
432
472
|
// Should select the previous selectable item before the header
|
|
433
473
|
expect(snapshot.selectedIndex).toBe(1);
|
|
434
|
-
|
|
474
|
+
const selectedItemEndHeader = snapshot.filteredItems[snapshot.selectedIndex];
|
|
475
|
+
expect(
|
|
476
|
+
isSelectableMenuItemWithScore(selectedItemEndHeader) &&
|
|
477
|
+
selectedItemEndHeader.value
|
|
478
|
+
).toBe("item2");
|
|
435
479
|
|
|
436
480
|
// Try to select a header when it's between two sections
|
|
437
|
-
const itemsWithHeaderBetween = [
|
|
438
|
-
{
|
|
481
|
+
const itemsWithHeaderBetween: MenuItem<string>[] = [
|
|
482
|
+
{ value: "item1", title: "item1" },
|
|
439
483
|
{
|
|
440
|
-
type: "sectionHeader"
|
|
484
|
+
type: "sectionHeader",
|
|
441
485
|
id: "header",
|
|
442
|
-
|
|
486
|
+
title: "Header",
|
|
443
487
|
},
|
|
444
|
-
{
|
|
488
|
+
{ value: "item2", title: "item2" },
|
|
445
489
|
];
|
|
446
490
|
const stateWithHeaderBetween = new ComboboxState(
|
|
447
491
|
itemsWithHeaderBetween,
|
|
@@ -452,21 +496,26 @@ test("handles section header selection", () => {
|
|
|
452
496
|
snapshot = stateWithHeaderBetween.getSnapshot();
|
|
453
497
|
// Should select the next selectable item after the header
|
|
454
498
|
expect(snapshot.selectedIndex).toBe(2);
|
|
455
|
-
|
|
499
|
+
const selectedItemBetweenHeader =
|
|
500
|
+
snapshot.filteredItems[snapshot.selectedIndex];
|
|
501
|
+
expect(
|
|
502
|
+
isSelectableMenuItemWithScore(selectedItemBetweenHeader) &&
|
|
503
|
+
selectedItemBetweenHeader.value
|
|
504
|
+
).toBe("item2");
|
|
456
505
|
});
|
|
457
506
|
|
|
458
507
|
test("handles section header selection with no selectable items", () => {
|
|
459
508
|
// Test with only headers
|
|
460
|
-
const onlyHeaders = [
|
|
509
|
+
const onlyHeaders: MenuItem<string>[] = [
|
|
461
510
|
{
|
|
462
|
-
type: "sectionHeader"
|
|
511
|
+
type: "sectionHeader",
|
|
463
512
|
id: "header1",
|
|
464
|
-
|
|
513
|
+
title: "Header 1",
|
|
465
514
|
},
|
|
466
515
|
{
|
|
467
|
-
type: "sectionHeader"
|
|
516
|
+
type: "sectionHeader",
|
|
468
517
|
id: "header2",
|
|
469
|
-
|
|
518
|
+
title: "Header 2",
|
|
470
519
|
},
|
|
471
520
|
];
|
|
472
521
|
const state = new ComboboxState(onlyHeaders, "", true);
|
|
@@ -485,13 +534,12 @@ test("selects specific item when provided", () => {
|
|
|
485
534
|
// Get the items we can select from
|
|
486
535
|
const snapshot = state.getSnapshot();
|
|
487
536
|
const italicItem = snapshot.filteredItems.find(
|
|
488
|
-
(item)
|
|
489
|
-
item.type !== "sectionHeader" && item.name === "font-italic"
|
|
537
|
+
(item) => isSelectableMenuItem(item) && item.title === "font-italic"
|
|
490
538
|
);
|
|
491
539
|
|
|
492
540
|
// Select a specific item, even though it's not the currently selected one
|
|
493
541
|
const selectedItem = state.selectCurrentItem(italicItem);
|
|
494
|
-
expect(selectedItem?.
|
|
542
|
+
expect(selectedItem?.title).toBe("font-italic");
|
|
495
543
|
|
|
496
544
|
// Verify the state was updated
|
|
497
545
|
const newSnapshot = state.getSnapshot();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { cx } from "../utils/classNames";
|
|
5
|
+
import { Text, TextProps } from "./Text";
|
|
6
|
+
|
|
7
|
+
type BreadcrumbTextProps = Omit<
|
|
8
|
+
TextProps,
|
|
9
|
+
"variant" | "whiteSpace" | "lineHeight" | "userSelect"
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
export const BreadcrumbText = forwardRef(function BreadcrumbText(
|
|
13
|
+
{ children, className, ...props }: BreadcrumbTextProps,
|
|
14
|
+
ref: React.ForwardedRef<HTMLSpanElement>
|
|
15
|
+
) {
|
|
16
|
+
return (
|
|
17
|
+
<Text
|
|
18
|
+
{...props}
|
|
19
|
+
variant="small"
|
|
20
|
+
className={cx(
|
|
21
|
+
"hover:text-breadcrumb-text-hover hover:cursor-pointer hover:whitespace-pre hover:select-none whitespace-pre leading-[15px] select-none",
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
ref={ref}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</Text>
|
|
28
|
+
);
|
|
29
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { forwardRefGeneric } from "../../../noya-react-utils/src/utils/reactGenerics";
|
|
3
|
+
import { Grid, GridViewSize } from "../components/Grid";
|
|
4
|
+
import { MenuItem } from "../components/internal/Menu";
|
|
5
|
+
import { ListView, ListViewRootProps } from "../components/ListView";
|
|
6
|
+
import { List } from "./List";
|
|
7
|
+
|
|
8
|
+
export type CollectionViewType = "grid" | "list";
|
|
9
|
+
|
|
10
|
+
export type CollectionRef = {
|
|
11
|
+
editName: (id: string) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface CollectionProps<T, M extends string = string> {
|
|
15
|
+
className?: string;
|
|
16
|
+
items: T[];
|
|
17
|
+
getId: (item: T) => string;
|
|
18
|
+
getName: (item: T) => string;
|
|
19
|
+
getExpanded?: (item: T) => boolean | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Whether directories should be expandable.
|
|
22
|
+
* @default true
|
|
23
|
+
*/
|
|
24
|
+
expandable?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Whether to allow renaming files
|
|
27
|
+
* @default true
|
|
28
|
+
*/
|
|
29
|
+
renamable?: boolean;
|
|
30
|
+
menuItems?: MenuItem<M>[];
|
|
31
|
+
onSelectMenuItem?: (action: M, selectedItems: T[]) => void;
|
|
32
|
+
onRename?: (item: T, newName: string) => void;
|
|
33
|
+
renderThumbnail?: (item: T, selected: boolean) => React.ReactNode;
|
|
34
|
+
renderAction?: (item: T, selected: boolean) => React.ReactNode;
|
|
35
|
+
renderDetail?: (item: T, selected: boolean) => React.ReactNode;
|
|
36
|
+
/** Callback when selection changes. If not provided, selection will be disabled. */
|
|
37
|
+
onSelectionChange?: (
|
|
38
|
+
selectedItemIds: string[],
|
|
39
|
+
event?: ListView.ClickInfo
|
|
40
|
+
) => void;
|
|
41
|
+
scrollable?: boolean;
|
|
42
|
+
itemRoleDescription?: string;
|
|
43
|
+
/** Position of the detail content. Defaults to 'end'. */
|
|
44
|
+
detailPosition?: "end" | "below";
|
|
45
|
+
/** Size of the list items. Defaults to 'medium'. */
|
|
46
|
+
size?: GridViewSize;
|
|
47
|
+
/** For testing: Override the hover state with a specific item ID */
|
|
48
|
+
testHoveredId?: string;
|
|
49
|
+
/** For testing: Override the renaming state with a specific item ID */
|
|
50
|
+
testRenamingId?: string;
|
|
51
|
+
setExpanded?: (item: T, expanded: boolean) => void;
|
|
52
|
+
acceptsDrop?: ListViewRootProps["acceptsDrop"];
|
|
53
|
+
onMoveItem?: ListViewRootProps["onMoveItem"];
|
|
54
|
+
getDepth?: (item: T) => number;
|
|
55
|
+
onFilesDrop?: (event: React.DragEvent<Element>) => void;
|
|
56
|
+
getRenamable?: (item: T) => boolean;
|
|
57
|
+
/** Currently selected file IDs */
|
|
58
|
+
selectedIds?: string[];
|
|
59
|
+
viewType?: CollectionViewType;
|
|
60
|
+
onDoubleClickItem?: (itemId: string) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const Collection = forwardRefGeneric(function Collection<
|
|
64
|
+
T,
|
|
65
|
+
M extends string = string,
|
|
66
|
+
>(props: CollectionProps<T, M>, ref: React.ForwardedRef<CollectionRef>) {
|
|
67
|
+
const { viewType, ...rest } = props;
|
|
68
|
+
|
|
69
|
+
return viewType === "grid" ? (
|
|
70
|
+
<Grid ref={ref} {...rest} />
|
|
71
|
+
) : (
|
|
72
|
+
<List ref={ref} {...rest} />
|
|
73
|
+
);
|
|
74
|
+
});
|