@noya-app/noya-designsystem 0.1.44 → 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 +13 -10
- package/CHANGELOG.md +18 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +1194 -878
- package/dist/index.d.ts +1194 -878
- package/dist/index.js +11432 -10219
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11556 -10360
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/combobox.test.ts +137 -89
- package/src/components/AnimatePresence.tsx +10 -1
- package/src/components/Avatar.tsx +2 -0
- package/src/components/Breadcrumbs.tsx +29 -0
- package/src/components/Checkbox.tsx +6 -1
- package/src/components/Collection.tsx +74 -0
- package/src/components/Combobox.tsx +75 -56
- package/src/components/ComboboxMenu.tsx +61 -28
- package/src/components/CommandPalette.tsx +69 -0
- package/src/components/ContextMenu.tsx +33 -134
- package/src/components/DropdownMenu.tsx +46 -155
- package/src/components/EditableText.tsx +203 -0
- package/src/components/Fade.tsx +62 -0
- package/src/components/Grid.tsx +243 -0
- package/src/components/GridView.tsx +86 -96
- package/src/components/InputField.tsx +109 -133
- package/src/components/Label.tsx +81 -7
- package/src/components/LabeledField.tsx +112 -0
- package/src/components/List.tsx +268 -0
- package/src/components/ListView.tsx +65 -61
- package/src/components/Popover.tsx +12 -1
- package/src/components/SearchCompletionMenu.tsx +210 -0
- package/src/components/SegmentedControl.tsx +12 -6
- package/src/components/SelectMenu.tsx +110 -126
- package/src/components/Slider.tsx +18 -26
- package/src/components/Text.tsx +1 -0
- package/src/components/TextArea.tsx +4 -1
- package/src/components/Toolbar.tsx +9 -4
- package/src/components/TreeView.tsx +4 -0
- package/src/components/WorkspaceLayout.tsx +64 -20
- package/src/components/internal/Menu.tsx +107 -18
- package/src/components/internal/MenuViewport.tsx +152 -0
- package/src/components/internal/SelectItem.tsx +111 -0
- package/src/hooks/useIndent.ts +12 -0
- package/src/hooks/useLabel.ts +51 -0
- package/src/hooks/usePreservePanelSize.tsx +50 -19
- package/src/index.tsx +15 -6
- package/src/utils/combobox.ts +116 -101
- package/src/utils/createSectionedMenu.ts +11 -14
- package/src/utils/fuzzyScorer.ts +11 -8
- package/src/utils/selection.ts +48 -0
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { DropdownChevronIcon } from "@noya-app/noya-icons";
|
|
2
|
+
import { forwardRefGeneric, memoGeneric } from "@noya-app/react-utils";
|
|
2
3
|
import React, {
|
|
3
4
|
FocusEventHandler,
|
|
4
5
|
ForwardedRef,
|
|
5
|
-
forwardRef,
|
|
6
|
-
memo,
|
|
7
6
|
useCallback,
|
|
8
7
|
useEffect,
|
|
9
8
|
useImperativeHandle,
|
|
@@ -11,25 +10,28 @@ import React, {
|
|
|
11
10
|
useRef,
|
|
12
11
|
useState,
|
|
13
12
|
} from "react";
|
|
13
|
+
import { useLabel } from "../hooks/useLabel";
|
|
14
14
|
import { cx } from "../utils/classNames";
|
|
15
|
-
import {
|
|
16
|
-
ComboboxItem,
|
|
17
|
-
ComboboxOption,
|
|
18
|
-
ComboboxState,
|
|
19
|
-
InternalComboboxItem,
|
|
20
|
-
useComboboxState,
|
|
21
|
-
} from "../utils/combobox";
|
|
15
|
+
import { ComboboxState, useComboboxState } from "../utils/combobox";
|
|
22
16
|
import { ActivityIndicator } from "./ActivityIndicator";
|
|
23
17
|
import { ComboboxMenu } from "./ComboboxMenu";
|
|
24
18
|
import { InputField, InputFieldSize } from "./InputField";
|
|
19
|
+
import {
|
|
20
|
+
MenuItem,
|
|
21
|
+
ScoredMenuItem,
|
|
22
|
+
SelectableMenuItem,
|
|
23
|
+
isMenuItemSectionHeader,
|
|
24
|
+
isNonSelectableMenuItem,
|
|
25
|
+
isSelectableMenuItem,
|
|
26
|
+
} from "./internal/Menu";
|
|
25
27
|
import { ListView } from "./ListView";
|
|
26
28
|
import { Small } from "./Text";
|
|
27
29
|
|
|
28
|
-
export type BaseComboboxProps = {
|
|
30
|
+
export type BaseComboboxProps<T extends string> = {
|
|
29
31
|
id?: string;
|
|
30
32
|
loading?: boolean;
|
|
31
33
|
placeholder?: string;
|
|
32
|
-
items:
|
|
34
|
+
items: MenuItem<T>[];
|
|
33
35
|
onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
|
|
34
36
|
onDeleteWhenEmpty?: () => void;
|
|
35
37
|
size?: InputFieldSize;
|
|
@@ -53,39 +55,42 @@ export type StringModeOnlyProps = {
|
|
|
53
55
|
allowArbitraryValues?: boolean;
|
|
54
56
|
};
|
|
55
57
|
|
|
56
|
-
export type StringModeProps = BaseComboboxProps &
|
|
58
|
+
export type StringModeProps<T extends string> = BaseComboboxProps<T> &
|
|
59
|
+
StringModeOnlyProps;
|
|
57
60
|
|
|
58
|
-
export type OptionModeOnlyProps = {
|
|
61
|
+
export type OptionModeOnlyProps<T extends string> = {
|
|
59
62
|
mode?: "option";
|
|
60
|
-
value?:
|
|
61
|
-
onChange?: (value:
|
|
62
|
-
onSelectItem?: (value:
|
|
63
|
-
onBlur?: (didSubmit: boolean, value:
|
|
64
|
-
onHoverItem?: (value:
|
|
63
|
+
value?: SelectableMenuItem<T>;
|
|
64
|
+
onChange?: (value: SelectableMenuItem<T>) => void;
|
|
65
|
+
onSelectItem?: (value: SelectableMenuItem<T>) => void;
|
|
66
|
+
onBlur?: (didSubmit: boolean, value: SelectableMenuItem<T>) => void;
|
|
67
|
+
onHoverItem?: (value: SelectableMenuItem<T> | undefined) => void;
|
|
65
68
|
allowArbitraryValues?: false;
|
|
66
69
|
};
|
|
67
70
|
|
|
68
|
-
export type OptionModeProps = BaseComboboxProps &
|
|
71
|
+
export type OptionModeProps<T extends string> = BaseComboboxProps<T> &
|
|
72
|
+
OptionModeOnlyProps<T>;
|
|
69
73
|
|
|
70
|
-
export type ComboboxProps
|
|
74
|
+
export type ComboboxProps<T extends string> =
|
|
75
|
+
| StringModeProps<T>
|
|
76
|
+
| OptionModeProps<T>;
|
|
71
77
|
|
|
72
|
-
export interface ComboboxRef {
|
|
78
|
+
export interface ComboboxRef<T extends string> {
|
|
73
79
|
focus(): void;
|
|
74
|
-
setValue(value: string |
|
|
80
|
+
setValue(value: string | SelectableMenuItem<T>): void;
|
|
75
81
|
selectAllInputText(): void;
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
export const Combobox =
|
|
79
|
-
|
|
84
|
+
export const Combobox = memoGeneric(
|
|
85
|
+
forwardRefGeneric(function Combobox<T extends string>(
|
|
80
86
|
{
|
|
81
|
-
id,
|
|
82
87
|
loading,
|
|
83
88
|
placeholder,
|
|
84
89
|
items,
|
|
85
90
|
size,
|
|
86
91
|
style,
|
|
87
92
|
className,
|
|
88
|
-
label,
|
|
93
|
+
label: labelProp,
|
|
89
94
|
children,
|
|
90
95
|
hideMenuWhenEmptyValue = false,
|
|
91
96
|
readOnly = false,
|
|
@@ -94,12 +99,12 @@ export const Combobox = memo(
|
|
|
94
99
|
onFocus,
|
|
95
100
|
onDeleteWhenEmpty,
|
|
96
101
|
...rest
|
|
97
|
-
}: ComboboxProps
|
|
98
|
-
forwardedRef: ForwardedRef<ComboboxRef
|
|
102
|
+
}: ComboboxProps<T>,
|
|
103
|
+
forwardedRef: ForwardedRef<ComboboxRef<T>>
|
|
99
104
|
) {
|
|
100
105
|
// We make a separate props object for the mode-specific props for memoizing
|
|
101
106
|
// since we put the props object into hook deps
|
|
102
|
-
const props = useMemo((): StringModeOnlyProps | OptionModeOnlyProps => {
|
|
107
|
+
const props = useMemo((): StringModeOnlyProps | OptionModeOnlyProps<T> => {
|
|
103
108
|
if (rest.mode === "string") {
|
|
104
109
|
return {
|
|
105
110
|
mode: rest.mode,
|
|
@@ -118,7 +123,7 @@ export const Combobox = memo(
|
|
|
118
123
|
onSelectItem: rest.onSelectItem,
|
|
119
124
|
onHoverItem: rest.onHoverItem,
|
|
120
125
|
onBlur: rest.onBlur,
|
|
121
|
-
} satisfies OptionModeOnlyProps
|
|
126
|
+
} satisfies OptionModeOnlyProps<T>;
|
|
122
127
|
}
|
|
123
128
|
}, [
|
|
124
129
|
rest.mode,
|
|
@@ -136,7 +141,7 @@ export const Combobox = memo(
|
|
|
136
141
|
if (props.mode === "string") {
|
|
137
142
|
return props.value ?? "";
|
|
138
143
|
} else {
|
|
139
|
-
return props.value?.
|
|
144
|
+
return props.value?.title?.toString() ?? "";
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
|
|
@@ -154,6 +159,9 @@ export const Combobox = memo(
|
|
|
154
159
|
const [isFocused, setIsFocused] = useState(false);
|
|
155
160
|
const listRef = useRef<ListView.VirtualizedList>(null);
|
|
156
161
|
const [shouldBlur, setShouldBlur] = useState(false);
|
|
162
|
+
const { fieldId: id } = useLabel({
|
|
163
|
+
fieldId: rest.id,
|
|
164
|
+
});
|
|
157
165
|
|
|
158
166
|
// Keep state in sync with items prop
|
|
159
167
|
useEffect(() => {
|
|
@@ -185,7 +193,7 @@ export const Combobox = memo(
|
|
|
185
193
|
);
|
|
186
194
|
} else {
|
|
187
195
|
const selectedItem = comboboxState.getSelectedItem();
|
|
188
|
-
if (selectedItem && selectedItem
|
|
196
|
+
if (selectedItem && isSelectableMenuItem(selectedItem)) {
|
|
189
197
|
props.onBlur?.(
|
|
190
198
|
state.filter === state.lastSubmittedValue.filter,
|
|
191
199
|
selectedItem
|
|
@@ -211,14 +219,14 @@ export const Combobox = memo(
|
|
|
211
219
|
);
|
|
212
220
|
|
|
213
221
|
const handleUpdateSelection = useCallback(
|
|
214
|
-
(item:
|
|
215
|
-
if (item.type
|
|
222
|
+
(item: ScoredMenuItem<T>) => {
|
|
223
|
+
if (item.type !== undefined) return;
|
|
216
224
|
|
|
217
225
|
const selectedItem = comboboxState.selectCurrentItem(item);
|
|
218
|
-
if (!selectedItem
|
|
226
|
+
if (!selectedItem) return;
|
|
219
227
|
|
|
220
228
|
if (props.mode === "string") {
|
|
221
|
-
props.onSelectItem?.(selectedItem.
|
|
229
|
+
props.onSelectItem?.(selectedItem.value);
|
|
222
230
|
props.onHoverItem?.(undefined);
|
|
223
231
|
} else {
|
|
224
232
|
props.onSelectItem?.(selectedItem);
|
|
@@ -233,9 +241,9 @@ export const Combobox = memo(
|
|
|
233
241
|
(index: number) => {
|
|
234
242
|
comboboxState.setSelectedIndex(index);
|
|
235
243
|
const item = comboboxState.getSelectedItem();
|
|
236
|
-
if (item && item
|
|
244
|
+
if (item && isSelectableMenuItem(item)) {
|
|
237
245
|
if (props.mode === "string") {
|
|
238
|
-
props.onHoverItem?.(item.
|
|
246
|
+
props.onHoverItem?.(item.value);
|
|
239
247
|
} else {
|
|
240
248
|
props.onHoverItem?.(item);
|
|
241
249
|
}
|
|
@@ -254,7 +262,7 @@ export const Combobox = memo(
|
|
|
254
262
|
props.onChange?.(value);
|
|
255
263
|
} else {
|
|
256
264
|
const selectedItem = comboboxState.getSelectedItem();
|
|
257
|
-
if (selectedItem && selectedItem
|
|
265
|
+
if (selectedItem && isSelectableMenuItem(selectedItem)) {
|
|
258
266
|
props.onChange?.(selectedItem);
|
|
259
267
|
}
|
|
260
268
|
}
|
|
@@ -308,14 +316,17 @@ export const Combobox = memo(
|
|
|
308
316
|
} else {
|
|
309
317
|
// In autocomplete mode:
|
|
310
318
|
// 1. Always set the filter to the selected item
|
|
311
|
-
// 2. If the filter matches the item
|
|
319
|
+
// 2. If the filter matches the item value, submit it
|
|
312
320
|
event.preventDefault();
|
|
313
321
|
event.stopPropagation();
|
|
314
|
-
if (item
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
322
|
+
if (isNonSelectableMenuItem(item)) return;
|
|
323
|
+
if (isSelectableMenuItem(item)) {
|
|
324
|
+
if (item.value === filter) {
|
|
325
|
+
handleUpdateSelection(item);
|
|
326
|
+
} else {
|
|
327
|
+
comboboxState.setFilter(item.value?.toString() ?? "");
|
|
328
|
+
ref.current?.focus();
|
|
329
|
+
}
|
|
319
330
|
}
|
|
320
331
|
handled = true;
|
|
321
332
|
}
|
|
@@ -374,11 +385,11 @@ export const Combobox = memo(
|
|
|
374
385
|
ref.current.setSelectionRange(length, length);
|
|
375
386
|
}
|
|
376
387
|
},
|
|
377
|
-
setValue: (value: string |
|
|
388
|
+
setValue: (value: string | SelectableMenuItem<T>) => {
|
|
378
389
|
if (typeof value === "string") {
|
|
379
390
|
comboboxState.setFilter(value);
|
|
380
391
|
} else {
|
|
381
|
-
comboboxState.setFilter(value.
|
|
392
|
+
comboboxState.setFilter(value.value);
|
|
382
393
|
}
|
|
383
394
|
},
|
|
384
395
|
selectAllInputText: () => {
|
|
@@ -418,6 +429,18 @@ export const Combobox = memo(
|
|
|
418
429
|
handleBlur();
|
|
419
430
|
}, [shouldBlur, handleBlur]);
|
|
420
431
|
|
|
432
|
+
const { sectionHeaderCount, menuItemsCount } = useMemo(
|
|
433
|
+
() => ({
|
|
434
|
+
sectionHeaderCount: filteredItems.filter((item) =>
|
|
435
|
+
isMenuItemSectionHeader(item)
|
|
436
|
+
).length,
|
|
437
|
+
menuItemsCount: filteredItems.filter((item) =>
|
|
438
|
+
isSelectableMenuItem(item)
|
|
439
|
+
).length,
|
|
440
|
+
}),
|
|
441
|
+
[filteredItems]
|
|
442
|
+
);
|
|
443
|
+
|
|
421
444
|
return (
|
|
422
445
|
<InputField.Root
|
|
423
446
|
id={id}
|
|
@@ -425,13 +448,10 @@ export const Combobox = memo(
|
|
|
425
448
|
sideOffset={6}
|
|
426
449
|
renderPopoverContent={({ width }) => {
|
|
427
450
|
const height = Math.min(
|
|
428
|
-
ListView.calculateHeight(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
.length,
|
|
433
|
-
"label"
|
|
434
|
-
),
|
|
451
|
+
ListView.calculateHeight({
|
|
452
|
+
menuItemsCount,
|
|
453
|
+
sectionHeaderCount,
|
|
454
|
+
}),
|
|
435
455
|
ListView.rowHeight * 10.5
|
|
436
456
|
);
|
|
437
457
|
|
|
@@ -500,11 +520,10 @@ export const Combobox = memo(
|
|
|
500
520
|
)}
|
|
501
521
|
{children}
|
|
502
522
|
{loading && (
|
|
503
|
-
<InputField.Label>
|
|
523
|
+
<InputField.Label htmlFor={id}>
|
|
504
524
|
<ActivityIndicator />
|
|
505
525
|
</InputField.Label>
|
|
506
526
|
)}
|
|
507
|
-
<InputField.Label>{label}</InputField.Label>
|
|
508
527
|
{!readOnly && (
|
|
509
528
|
<InputField.Button variant="floating" onClick={handleChevronClick}>
|
|
510
529
|
<DropdownChevronIcon
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
import { Size } from "@noya-app/noya-geometry";
|
|
2
|
-
import
|
|
2
|
+
import { CheckIcon } from "@noya-app/noya-icons";
|
|
3
|
+
import { forwardRefGeneric, memoGeneric } from "@noya-app/react-utils";
|
|
4
|
+
import React, { ForwardedRef } from "react";
|
|
3
5
|
import { cx } from "../utils/classNames";
|
|
4
|
-
|
|
6
|
+
|
|
5
7
|
import { fuzzyTokenize } from "../utils/fuzzyScorer";
|
|
8
|
+
import { renderIcon } from "./Icons";
|
|
6
9
|
import { IVirtualizedList, ListView } from "./ListView";
|
|
7
10
|
import { Spacer } from "./Spacer";
|
|
11
|
+
import {
|
|
12
|
+
CHECKBOX_RIGHT_INSET,
|
|
13
|
+
CHECKBOX_WIDTH,
|
|
14
|
+
KeyboardShortcut,
|
|
15
|
+
ScoredMenuItem,
|
|
16
|
+
styles,
|
|
17
|
+
} from "./internal/Menu";
|
|
8
18
|
|
|
9
|
-
interface ComboboxMenuProps {
|
|
10
|
-
items:
|
|
19
|
+
interface ComboboxMenuProps<T extends string> {
|
|
20
|
+
items: ScoredMenuItem<T>[];
|
|
11
21
|
selectedIndex: number;
|
|
12
|
-
onSelectItem: (item:
|
|
22
|
+
onSelectItem: (item: ScoredMenuItem<T>) => void;
|
|
13
23
|
onHoverIndex: (index: number) => void;
|
|
14
24
|
listSize: Size;
|
|
15
25
|
style?: React.CSSProperties;
|
|
16
26
|
}
|
|
17
27
|
|
|
18
|
-
export const ComboboxMenu =
|
|
19
|
-
|
|
28
|
+
export const ComboboxMenu = memoGeneric(
|
|
29
|
+
forwardRefGeneric(function ComboboxMenu<T extends string>(
|
|
20
30
|
{
|
|
21
31
|
items,
|
|
22
32
|
selectedIndex,
|
|
@@ -24,14 +34,22 @@ export const ComboboxMenu = memo(
|
|
|
24
34
|
onHoverIndex,
|
|
25
35
|
listSize,
|
|
26
36
|
style,
|
|
27
|
-
}: ComboboxMenuProps
|
|
37
|
+
}: ComboboxMenuProps<T>,
|
|
28
38
|
forwardedRef: ForwardedRef<IVirtualizedList>
|
|
29
39
|
) {
|
|
40
|
+
const hasCheckedItems = items
|
|
41
|
+
.filter((item) => item.type === undefined)
|
|
42
|
+
.some((item) => item.checked);
|
|
43
|
+
|
|
30
44
|
return (
|
|
31
45
|
<ListView.Root
|
|
32
46
|
ref={forwardedRef}
|
|
33
47
|
scrollable
|
|
34
|
-
keyExtractor={(item) =>
|
|
48
|
+
keyExtractor={(item: ScoredMenuItem<T>) => {
|
|
49
|
+
if (item.type === "sectionHeader") return item.id;
|
|
50
|
+
|
|
51
|
+
return item.value;
|
|
52
|
+
}}
|
|
35
53
|
data={items}
|
|
36
54
|
virtualized={listSize}
|
|
37
55
|
pressEventName="onPointerDown"
|
|
@@ -41,42 +59,57 @@ export const ComboboxMenu = memo(
|
|
|
41
59
|
if (item.type === "sectionHeader") {
|
|
42
60
|
return (
|
|
43
61
|
<ListView.Row key={item.id} isSectionHeader>
|
|
44
|
-
{item.
|
|
62
|
+
{item.title}
|
|
45
63
|
</ListView.Row>
|
|
46
64
|
);
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
const tokens = fuzzyTokenize({
|
|
50
|
-
item: item.
|
|
68
|
+
item: typeof item.title === "string" ? item.title : item.value,
|
|
51
69
|
itemScore: item,
|
|
52
70
|
});
|
|
53
71
|
|
|
54
72
|
return (
|
|
55
73
|
<ListView.Row
|
|
56
|
-
key={item.
|
|
74
|
+
key={item.value}
|
|
57
75
|
selected={i === selectedIndex}
|
|
58
|
-
|
|
76
|
+
disabled={item.disabled}
|
|
77
|
+
onPress={() => !item.disabled && onSelectItem(item)}
|
|
59
78
|
onHoverChange={(hovered) => {
|
|
60
|
-
if (hovered) {
|
|
79
|
+
if (hovered && !item.disabled) {
|
|
61
80
|
onHoverIndex(i);
|
|
62
81
|
}
|
|
63
82
|
}}
|
|
64
83
|
>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
<div className="flex flex-1 min-w-0 items-center">
|
|
85
|
+
{item.checked ? (
|
|
86
|
+
<div className={styles.itemIndicatorStyle}>
|
|
87
|
+
<CheckIcon />
|
|
88
|
+
</div>
|
|
89
|
+
) : hasCheckedItems ? (
|
|
90
|
+
<Spacer.Horizontal
|
|
91
|
+
size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET}
|
|
92
|
+
/>
|
|
93
|
+
) : null}
|
|
94
|
+
{tokens.map((token, j) => (
|
|
95
|
+
<span
|
|
96
|
+
key={`${item.value}-token-${j}`}
|
|
97
|
+
className={cx(
|
|
98
|
+
token.type === "match" ? "font-bold" : "font-normal",
|
|
99
|
+
"whitespace-pre truncate"
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
{token.text}
|
|
103
|
+
</span>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
{(item.shortcut || item.icon) && (
|
|
77
107
|
<>
|
|
78
|
-
|
|
79
|
-
|
|
108
|
+
{item.shortcut ? (
|
|
109
|
+
<KeyboardShortcut shortcut={item.shortcut} />
|
|
110
|
+
) : (
|
|
111
|
+
item.icon && renderIcon(item.icon)
|
|
112
|
+
)}
|
|
80
113
|
</>
|
|
81
114
|
)}
|
|
82
115
|
</ListView.Row>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Dialog } from "@noya-app/noya-designsystem";
|
|
2
|
+
import { AutoSizer } from "@noya-app/react-utils";
|
|
3
|
+
import React, { memo } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ISearchCompletionMenu,
|
|
6
|
+
SearchCompletionMenu,
|
|
7
|
+
} from "./SearchCompletionMenu";
|
|
8
|
+
|
|
9
|
+
export const CommandPalette = memo(function CommandPalette({
|
|
10
|
+
showCommandPalette,
|
|
11
|
+
setShowCommandPalette,
|
|
12
|
+
items,
|
|
13
|
+
onSelect,
|
|
14
|
+
onHover,
|
|
15
|
+
}: {
|
|
16
|
+
showCommandPalette: boolean;
|
|
17
|
+
setShowCommandPalette: (value: boolean) => void;
|
|
18
|
+
} & Pick<
|
|
19
|
+
React.ComponentProps<typeof SearchCompletionMenu>,
|
|
20
|
+
"items" | "onSelect" | "onHover"
|
|
21
|
+
>) {
|
|
22
|
+
const menuRef = React.useRef<ISearchCompletionMenu>(null);
|
|
23
|
+
|
|
24
|
+
const handleClose = React.useCallback(() => {
|
|
25
|
+
setShowCommandPalette(false);
|
|
26
|
+
}, [setShowCommandPalette]);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Dialog
|
|
30
|
+
style={{
|
|
31
|
+
display: "flex",
|
|
32
|
+
flexDirection: "column",
|
|
33
|
+
maxWidth: "500px",
|
|
34
|
+
minHeight: "100px",
|
|
35
|
+
padding: "1px",
|
|
36
|
+
top: "100px",
|
|
37
|
+
left: "50%",
|
|
38
|
+
transform: "translate(-50%, 0)",
|
|
39
|
+
}}
|
|
40
|
+
showCloseButton={false}
|
|
41
|
+
open={showCommandPalette}
|
|
42
|
+
onOpenChange={(value) => {
|
|
43
|
+
setShowCommandPalette(value);
|
|
44
|
+
}}
|
|
45
|
+
closeOnInteractOutside
|
|
46
|
+
onOpenAutoFocus={() => {
|
|
47
|
+
menuRef.current?.focus();
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
menuRef.current?.focus();
|
|
50
|
+
}, 0);
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<div className="flex flex-col flex-1">
|
|
54
|
+
<AutoSizer>
|
|
55
|
+
{(size) => (
|
|
56
|
+
<SearchCompletionMenu
|
|
57
|
+
ref={menuRef}
|
|
58
|
+
width={size.width}
|
|
59
|
+
items={items}
|
|
60
|
+
onSelect={onSelect}
|
|
61
|
+
onHover={onHover}
|
|
62
|
+
onClose={handleClose}
|
|
63
|
+
/>
|
|
64
|
+
)}
|
|
65
|
+
</AutoSizer>
|
|
66
|
+
</div>
|
|
67
|
+
</Dialog>
|
|
68
|
+
);
|
|
69
|
+
});
|