@noya-app/noya-designsystem 0.1.63 → 0.1.65

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.
Files changed (143) hide show
  1. package/.turbo/turbo-build.log +13 -10
  2. package/CHANGELOG.md +25 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +648 -333
  5. package/dist/index.d.ts +648 -333
  6. package/dist/index.js +13813 -9157
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +13949 -9269
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +10 -22
  11. package/src/__tests__/validateDropIndicator.test.ts +26 -41
  12. package/src/__tests__/workspaceLayout.test.ts +281 -0
  13. package/src/components/ActionMenu.tsx +2 -0
  14. package/src/components/ActivityIndicator.tsx +13 -6
  15. package/src/components/ActivityLog.tsx +197 -0
  16. package/src/components/AnimatePresence.tsx +43 -26
  17. package/src/components/Avatar.tsx +45 -26
  18. package/src/components/Banner.tsx +57 -11
  19. package/src/components/BaseToolbar.tsx +67 -34
  20. package/src/components/Breadcrumbs.tsx +4 -2
  21. package/src/components/Button.tsx +28 -23
  22. package/src/components/Checkbox.tsx +6 -35
  23. package/src/components/Chip.tsx +47 -33
  24. package/src/components/Collection.tsx +20 -12
  25. package/src/components/ColorSwatch.tsx +42 -22
  26. package/src/components/ColorSwatchControl.tsx +7 -5
  27. package/src/components/Combobox.tsx +8 -6
  28. package/src/components/ComboboxMenu.tsx +6 -5
  29. package/src/components/CommandPalette.tsx +3 -1
  30. package/src/components/ContextMenu.tsx +3 -1
  31. package/src/components/Dialog.tsx +9 -13
  32. package/src/components/DimensionInput.tsx +2 -0
  33. package/src/components/Divider.tsx +18 -14
  34. package/src/components/DraggableMenuButton.tsx +11 -5
  35. package/src/components/Drawer.tsx +14 -6
  36. package/src/components/DropdownMenu.tsx +4 -2
  37. package/src/components/EditableText.tsx +4 -2
  38. package/src/components/Fade.tsx +13 -13
  39. package/src/components/FileUploadIndicator.tsx +108 -0
  40. package/src/components/FloatingWindow.tsx +4 -2
  41. package/src/components/Grid.tsx +58 -42
  42. package/src/components/GridView.tsx +65 -36
  43. package/src/components/IVirtualizedList.tsx +5 -0
  44. package/src/components/IconButton.tsx +2 -0
  45. package/src/components/Icons.tsx +24 -4
  46. package/src/components/InputField.tsx +21 -19
  47. package/src/components/InspectorContainer.tsx +2 -2
  48. package/src/components/InspectorPrimitives.tsx +15 -9
  49. package/src/components/Label.tsx +4 -3
  50. package/src/components/LabeledElementView.tsx +9 -5
  51. package/src/components/LabeledField.tsx +8 -6
  52. package/src/components/List.tsx +112 -46
  53. package/src/components/ListMenu.tsx +91 -0
  54. package/src/components/ListNavigator.tsx +106 -0
  55. package/src/components/ListView.tsx +22 -1147
  56. package/src/components/MediaThumbnail.tsx +84 -14
  57. package/src/components/Message.tsx +11 -12
  58. package/src/components/Navigator.tsx +461 -0
  59. package/src/components/NoyaLogo.tsx +1 -1
  60. package/src/components/Popover.tsx +12 -6
  61. package/src/components/Progress.tsx +31 -8
  62. package/src/components/ResizableContainer.tsx +187 -0
  63. package/src/components/RingProgress.tsx +128 -0
  64. package/src/components/ScrollArea.tsx +7 -5
  65. package/src/components/ScrollableSidebar.tsx +10 -0
  66. package/src/components/SearchCompletionMenu.tsx +5 -3
  67. package/src/components/Section.tsx +109 -57
  68. package/src/components/SegmentedControl.tsx +51 -25
  69. package/src/components/SelectMenu.tsx +16 -21
  70. package/src/components/SelectionToolbar.tsx +7 -4
  71. package/src/components/Slider.tsx +12 -10
  72. package/src/components/Spacer.tsx +2 -2
  73. package/src/components/StackNavigator.tsx +378 -0
  74. package/src/components/Stepper.tsx +88 -0
  75. package/src/components/Switch.tsx +7 -5
  76. package/src/components/Tabs.tsx +6 -4
  77. package/src/components/Text.tsx +29 -18
  78. package/src/components/TextArea.tsx +5 -3
  79. package/src/components/Toast.tsx +5 -5
  80. package/src/components/Toolbar.tsx +57 -10
  81. package/src/components/ToolbarDrawer.tsx +29 -0
  82. package/src/components/Tooltip.tsx +2 -2
  83. package/src/components/TreeView.tsx +13 -4
  84. package/src/components/UserPointer.tsx +6 -4
  85. package/src/components/ai-assistant/AIAssistantLayout.tsx +10 -8
  86. package/src/components/blocks/ImageBlockComponent.tsx +162 -0
  87. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +6 -4
  88. package/src/components/file-explorer/FileExplorerLayout.tsx +22 -8
  89. package/src/components/internal/Checkmark.tsx +24 -0
  90. package/src/components/internal/Menu.tsx +24 -19
  91. package/src/components/internal/MenuViewport.tsx +15 -6
  92. package/src/components/internal/SelectItem.tsx +7 -26
  93. package/src/components/internal/TextInput.tsx +7 -1
  94. package/src/components/listView/ListViewContexts.tsx +68 -0
  95. package/src/components/listView/ListViewEditableRowTitle.tsx +55 -0
  96. package/src/components/listView/ListViewRoot.tsx +521 -0
  97. package/src/components/listView/ListViewRow.tsx +475 -0
  98. package/src/components/listView/ListViewRowTitle.tsx +21 -0
  99. package/src/components/listView/types.ts +11 -0
  100. package/src/components/pipeline/PipelineResultLayout.tsx +6 -6
  101. package/src/components/sorting/DragRegistration.tsx +10 -5
  102. package/src/components/sorting/SharedDragProvider.tsx +134 -27
  103. package/src/components/sorting/Sortable.tsx +90 -27
  104. package/src/components/sorting/createSharedDrag.tsx +7 -6
  105. package/src/components/sorting/sorting.ts +6 -9
  106. package/src/components/workspace/DrawerWorkspaceLayout.tsx +110 -29
  107. package/src/components/workspace/PanelWorkspaceLayout.tsx +148 -99
  108. package/src/components/workspace/VerticalTabMenu.tsx +46 -0
  109. package/src/components/workspace/WorkspaceLayout.tsx +76 -63
  110. package/src/components/workspace/constants.ts +4 -0
  111. package/src/components/workspace/panelStorage.ts +216 -0
  112. package/src/components/workspace/types.ts +11 -4
  113. package/src/contexts/DesignSystemConfiguration.tsx +12 -5
  114. package/src/contexts/DialogContext.tsx +19 -5
  115. package/src/contexts/FloatingWindowContext.tsx +2 -0
  116. package/src/contexts/GlobalInputBlurContext.tsx +2 -0
  117. package/src/contexts/ImageDataContext.tsx +2 -0
  118. package/src/contexts/LinkComponentContext.tsx +17 -0
  119. package/src/contexts/OpenPortalsContext.tsx +2 -0
  120. package/src/contexts/PortalScopeContext.tsx +2 -0
  121. package/src/hooks/mergeEventHandlers.ts +1 -1
  122. package/src/hooks/useHover.ts +30 -17
  123. package/src/hooks/useIndent.ts +5 -8
  124. package/src/hooks/useLabel.ts +2 -0
  125. package/src/hooks/useObjectURL.ts +5 -3
  126. package/src/hooks/usePlatform.ts +5 -3
  127. package/src/hooks/useTheme.ts +3 -1
  128. package/src/index.css +43 -10
  129. package/src/index.tsx +15 -7
  130. package/src/utils/classNames.ts +56 -9
  131. package/src/utils/combobox.ts +2 -0
  132. package/src/utils/editableBlockStyles.ts +2 -2
  133. package/src/utils/formatByteSize.ts +9 -0
  134. package/src/utils/inputs.ts +8 -6
  135. package/src/utils/sketchColor.ts +0 -34
  136. package/src/utils/withSeparatorElements.ts +1 -1
  137. package/tailwind.config.ts +1 -0
  138. package/src/components/FillInputField.tsx +0 -37
  139. package/src/components/FillPreviewBackground.tsx +0 -132
  140. package/src/components/GradientPicker.tsx +0 -88
  141. package/src/hooks/usePreservePanelSize.tsx +0 -183
  142. package/src/mediaQuery.ts +0 -13
  143. package/src/utils/getGradientBackground.tsx +0 -31
@@ -1,1153 +1,28 @@
1
- import { Size } from "@noya-app/noya-geometry";
2
- import { range } from "@noya-app/noya-utils";
3
- import { forwardRefGeneric, memoGeneric } from "@noya-app/react-utils";
4
- import { composeRefs } from "@radix-ui/react-compose-refs";
5
- import React, {
6
- Children,
7
- createContext,
8
- CSSProperties,
9
- ForwardedRef,
10
- forwardRef,
11
- HTMLAttributes,
12
- isValidElement,
13
- memo,
14
- ReactElement,
15
- ReactNode,
16
- Ref,
17
- useCallback,
18
- useContext,
19
- useImperativeHandle,
20
- useLayoutEffect,
21
- useMemo,
22
- useRef,
23
- } from "react";
24
- import { WindowScroller, WindowScrollerChildProps } from "react-virtualized";
25
- import { ListChildComponentProps, VariableSizeList } from "react-window";
26
- import { mergeEventHandlers } from "../hooks/mergeEventHandlers";
27
- import { useHover } from "../hooks/useHover";
28
- import { cssVars, INPUT_HEIGHT } from "../theme";
29
- import { cx, mergeConflictingClassNames } from "../utils/classNames";
30
- import { ContextMenu } from "./ContextMenu";
31
- import { InputField } from "./InputField";
32
- import { MenuItem } from "./internal/Menu";
33
- import { ScrollArea } from "./ScrollArea";
34
- import { SharedDragProps } from "./sorting/DragRegistration";
35
- import { Sortable } from "./sorting/Sortable";
36
- import {
37
- DropValidator,
38
- MoveDragItemHandler,
39
- normalizeListTargetIndex,
40
- RelativeDropPosition,
41
- SortableItemContextProps,
42
- } from "./sorting/sorting";
43
- import { Spacer } from "./Spacer";
44
-
45
- export type ListRowMarginType = "none" | "top" | "bottom" | "vertical";
46
- export type ListRowPosition = "only" | "first" | "middle" | "last";
47
-
48
- const ROW_HEIGHT = 31;
49
- const SECTION_HEADER_LABEL_HEIGHT = INPUT_HEIGHT;
50
-
51
- type ListColorScheme = "primary" | "secondary";
52
-
53
- type PressEventName = "onClick" | "onPointerDown";
54
-
55
- // TODO: Some of these could be passed in a single context value that's
56
- // the same for all rows. But doing this seems to cause an infinite loop and
57
- // prevents drag and drop from working.
58
- type ListRowContextValue = {
59
- marginType: ListRowMarginType;
60
- selectedPosition: ListRowPosition;
61
- sortable: boolean;
62
- expandable: boolean;
63
- divider: boolean;
64
- gap: number;
65
- variant: ListViewVariant;
66
- sectionHeaderVariant: ListViewSectionHeaderVariant;
67
- pressEventName: PressEventName;
68
- isSectionHeader: boolean;
69
- colorScheme: ListColorScheme;
70
- };
71
-
72
- // The dragged item isn't passed a context value (doing so causes an infinite loop),
73
- // so we pass this separately. Rows need the indentation to look correct when dragging,
74
- // and the dragged row doesn't have a ListRowContextValue.
75
- type ListViewDraggingContextValue = {
76
- indentation: number;
77
- isDragOverlay?: boolean;
78
- };
79
-
80
- const ListViewDraggingContext = createContext<ListViewDraggingContextValue>({
81
- indentation: 12,
82
- });
83
- (ListViewDraggingContext as any).displayName = "ListViewDraggingContext";
84
-
85
- const ListRowContext = createContext<ListRowContextValue>({
86
- marginType: "none",
87
- selectedPosition: "only",
88
- sortable: false,
89
- expandable: true,
90
- divider: true,
91
- gap: 0,
92
- variant: "normal",
93
- sectionHeaderVariant: "normal",
94
- pressEventName: "onClick",
95
- isSectionHeader: false,
96
- colorScheme: "primary",
97
- });
98
- (ListRowContext as any).displayName = "ListRowContext";
99
-
100
- /* ----------------------------------------------------------------------------
101
- * RowTitle
102
- * ------------------------------------------------------------------------- */
103
-
104
- const ListViewRowTitle = ({
105
- className,
106
- children,
107
- ...props
108
- }: {
109
- className?: string;
110
- children: React.ReactNode;
111
- } & HTMLAttributes<HTMLSpanElement>) => (
112
- <span
113
- className={cx(
114
- `flex-1 overflow-hidden text-ellipsis whitespace-pre `,
115
- className
116
- )}
117
- {...props}
118
- >
119
- {children}
120
- </span>
121
- );
122
-
123
- /* ----------------------------------------------------------------------------
124
- * EditableRowTitle
125
- * ------------------------------------------------------------------------- */
126
-
127
- export interface EditableRowProps {
128
- value: string;
129
- onSubmitEditing: (value: string) => void;
130
- autoFocus: boolean;
131
- placeholder?: string;
132
- className?: string;
133
- onKeyDown?: (e: React.KeyboardEvent) => void;
134
- }
135
-
136
- function ListViewEditableRowTitle({
137
- value,
138
- onSubmitEditing,
139
- autoFocus,
140
- placeholder,
141
- className,
142
- onKeyDown,
143
- }: EditableRowProps) {
144
- const inputRef = useRef<HTMLInputElement | null>(null);
145
-
146
- useLayoutEffect(() => {
147
- const element = inputRef.current;
148
-
149
- if (!element || !autoFocus) return;
150
-
151
- // Calling `focus` is necessary, in addition to `select`, to ensure
152
- // the `onBlur` fires correctly.
153
- element.focus();
154
-
155
- // must be a delay of 1ms or more to ensure any radix ui components have finished any focus behaviors
156
- setTimeout(() => {
157
- element.select();
158
- }, 1);
159
- }, [autoFocus]);
160
-
161
- return (
162
- <InputField.Input
163
- ref={inputRef}
164
- value={value}
165
- placeholder={placeholder}
166
- onSubmit={onSubmitEditing}
167
- allowSubmittingWithSameValue
168
- className={cx("-mx-1.5 -my-1 bg-listview-editing-background", className)}
169
- onKeyDown={onKeyDown}
170
- />
171
- );
172
- }
173
-
174
- /* ----------------------------------------------------------------------------
175
- * Row
176
- * ------------------------------------------------------------------------- */
177
-
178
- const RowContainer = forwardRef<
179
- HTMLDivElement,
180
- React.HTMLAttributes<HTMLDivElement> & {
181
- $marginType: ListRowMarginType;
182
- $selected: boolean;
183
- $selectedPosition: ListRowPosition;
184
- $disabled: boolean;
185
- $hovered: boolean;
186
- $variant: ListViewVariant;
187
- $divider: boolean;
188
- $isSectionHeader: boolean;
189
- $showsActiveState: boolean;
190
- $sectionHeaderVariant: ListViewSectionHeaderVariant;
191
- $colorScheme: ListColorScheme;
192
- $gap: number;
193
- $backgroundColor?: CSSProperties["backgroundColor"];
194
- $clickable?: boolean;
195
- }
196
- >(
197
- (
198
- {
199
- className,
200
- $marginType,
201
- $selected,
202
- $selectedPosition,
203
- $disabled,
204
- $hovered,
205
- $variant,
206
- $divider,
207
- $isSectionHeader,
208
- $showsActiveState,
209
- $sectionHeaderVariant,
210
- $colorScheme,
211
- $gap,
212
- $backgroundColor,
213
- style,
214
- $clickable,
215
- "aria-describedby": _, // Causes hydration warning
216
- role,
217
- "aria-label": ariaLabel,
218
- "aria-roledescription": ariaRoleDescription,
219
- "aria-selected": ariaSelected,
220
- ...props
221
- },
222
- ref
223
- ) => {
224
- return (
225
- <div
226
- ref={ref}
227
- role={role}
228
- aria-label={ariaLabel}
229
- aria-roledescription={ariaRoleDescription}
230
- aria-selected={ariaSelected ?? $selected}
231
- className={cx(
232
- $isSectionHeader && $sectionHeaderVariant === "label"
233
- ? "font-sans text-label uppercase leading-3 font-bold text-text-muted"
234
- : "font-sans text-heading5 font-normal",
235
- !$isSectionHeader &&
236
- $selected &&
237
- "text-selected-list-item-text active:bg-selected-list-item-background",
238
- !$isSectionHeader && $disabled && "text-text-disabled",
239
- $clickable ? "cursor-pointer" : "cursor-default",
240
- $isSectionHeader && "bg-listview-raised-background",
241
- "flex flex-[0_0_auto] select-none items-center relative",
242
- className
243
- )}
244
- style={{
245
- gap: $gap,
246
- ...($variant !== "bare" && {
247
- paddingTop: $selected ? "4px" : "6px",
248
- paddingRight: $selected ? "8px" : "12px",
249
- paddingBottom: $selected ? "4px" : "6px",
250
- paddingLeft: $selected ? "8px" : "12px",
251
- borderRadius: $selected ? "4px" : "",
252
- marginLeft: $selected ? "4px" : "",
253
- marginRight: $selected ? "4px" : "",
254
- marginTop: $selected ? "2px" : "",
255
- marginBottom: $selected ? "2px" : "",
256
- }),
257
- ...($selected &&
258
- !$isSectionHeader && {
259
- backgroundColor:
260
- $colorScheme === "primary"
261
- ? cssVars.colors["selectedListItemBackground"]
262
- : cssVars.colors[$colorScheme],
263
- }),
264
- ...($selected &&
265
- !$isSectionHeader &&
266
- ($selectedPosition === "middle" ||
267
- $selectedPosition === "last") && {
268
- borderTopRightRadius: "0px",
269
- borderTopLeftRadius: "0px",
270
- }),
271
- ...($selected &&
272
- !$isSectionHeader &&
273
- ($selectedPosition === "middle" ||
274
- $selectedPosition === "first") && {
275
- borderBottomRightRadius: "0px",
276
- borderBottomLeftRadius: "0px",
277
- }),
278
- ...($hovered &&
279
- !$selected && {
280
- background: cssVars.colors.listViewHoverBackground,
281
- }),
282
- ...($showsActiveState && {
283
- "&:active": {
284
- backgroundColor: $selected
285
- ? $colorScheme === "secondary"
286
- ? cssVars.colors.secondaryLight
287
- : cssVars.colors.selectedListItemBackground
288
- : cssVars.colors.activeBackground,
289
- },
290
- }),
291
- ...($divider && {
292
- borderBottom: `1px solid ${cssVars.colors.dividerSubtle}`,
293
- }),
294
- ...($backgroundColor && {
295
- backgroundColor: $backgroundColor,
296
- "&:hover": {
297
- backgroundColor: $backgroundColor,
298
- },
299
- "&:active": {
300
- backgroundColor: $backgroundColor,
301
- },
302
- }),
303
- ...style,
304
- }}
305
- {...props}
306
- />
307
- );
308
- }
309
- );
310
-
311
- const ListViewDragIndicatorElement = forwardRef<
312
- HTMLDivElement,
313
- React.HTMLAttributes<HTMLDivElement> & {
314
- $relativeDropPosition?: RelativeDropPosition;
315
- $gap: number;
316
- $offsetLeft: number;
317
- $colorScheme: ListColorScheme;
318
- }
319
- >(
320
- (
321
- {
322
- className,
323
- $relativeDropPosition,
324
- $offsetLeft,
325
- $colorScheme,
326
- $gap,
327
- style,
328
- ...props
329
- },
330
- ref
331
- ) => {
332
- return (
333
- <div
334
- ref={ref}
335
- {...props}
336
- className={className}
337
- style={{
338
- zIndex: 1000,
339
- position: "absolute",
340
- borderRadius: "3px",
341
- pointerEvents: "none",
342
- ...($relativeDropPosition === "inside"
343
- ? {
344
- inset: 2,
345
- boxShadow: `0 0 0 1px ${cssVars.colors.sidebarBackground}, 0 0 0 3px ${
346
- $colorScheme === "secondary"
347
- ? cssVars.colors.secondary
348
- : cssVars.colors.dragOutline
349
- }`,
350
- }
351
- : {
352
- top:
353
- $relativeDropPosition === "above"
354
- ? -(3 + $gap / 2)
355
- : undefined,
356
- bottom:
357
- $relativeDropPosition === "below"
358
- ? -(3 + $gap / 2)
359
- : undefined,
360
- left: $offsetLeft,
361
- right: 0,
362
- height: 6,
363
- backgroundColor:
364
- $colorScheme === "secondary"
365
- ? cssVars.colors.secondary
366
- : cssVars.colors.selectedListItemBackground,
367
- border: `2px solid white`,
368
- boxShadow: "0 0 2px rgba(0,0,0,0.5)",
369
- }),
370
- ...style,
371
- }}
372
- />
373
- );
374
- }
375
- );
376
-
377
- interface ListViewClickInfo {
378
- shiftKey: boolean;
379
- altKey: boolean;
380
- metaKey: boolean;
381
- ctrlKey: boolean;
382
- }
383
-
384
- export interface ListViewRowProps<MenuItemType extends string = string> {
385
- id?: string;
386
- tabIndex?: number;
387
- role?: string;
388
- selected?: boolean;
389
- /** @default 0 */
390
- depth?: number;
391
- disabled?: boolean;
392
- // draggable?: boolean;
393
- hovered?: boolean;
394
- sortable?: boolean;
395
- gap?: number;
396
- backgroundColor?: CSSProperties["backgroundColor"];
397
- onPress?: (info: ListViewClickInfo) => void;
398
- onDoubleClick?: () => void;
399
- onHoverChange?: (isHovering: boolean) => void;
400
- children?: ReactNode;
401
- isSectionHeader?: boolean;
402
- menuItems?: MenuItem<MenuItemType>[];
403
- onSelectMenuItem?: (value: MenuItemType) => void;
404
- onContextMenu?: () => void;
405
- onMenuOpenChange?: (isOpen: boolean) => void;
406
- onKeyDown?: (event: React.KeyboardEvent) => void;
407
- style?: CSSProperties;
408
- className?: string;
409
- dragIndicatorStyle?:
410
- | CSSProperties
411
- | ((props: DragIndicatorStyleProps) => CSSProperties);
412
- testRelativeDropPosition?: RelativeDropPosition;
413
- }
414
-
415
- export type DragIndicatorStyleProps = {
416
- depth: number;
417
- indentation: number;
418
- position: RelativeDropPosition;
419
- };
420
-
421
- const ListViewRow = forwardRefGeneric(function ListViewRow<
422
- MenuItemType extends string,
423
- >(
424
- {
425
- id,
426
- tabIndex = 0,
427
- role,
428
- gap,
429
- backgroundColor,
430
- selected = false,
431
- depth = 0,
432
- disabled = false,
433
- hovered = false,
434
- isSectionHeader = false,
435
- sortable: overrideSortable,
436
- onPress,
437
- onDoubleClick,
438
- onHoverChange,
439
- children,
440
- menuItems,
441
- onContextMenu,
442
- onSelectMenuItem,
443
- onMenuOpenChange,
444
- onKeyDown,
445
- style,
446
- dragIndicatorStyle,
447
- className,
448
- testRelativeDropPosition,
449
- }: ListViewRowProps<MenuItemType>,
450
- forwardedRef: ForwardedRef<HTMLElement>
451
- ) {
452
- depth = Math.max(0, depth);
453
-
454
- const {
455
- marginType,
456
- selectedPosition,
457
- sortable,
458
- pressEventName,
459
- variant,
460
- sectionHeaderVariant,
461
- divider,
462
- gap: listGap,
463
- colorScheme,
464
- } = useContext(ListRowContext);
465
- const { hoverProps } = useHover({
466
- onHoverChange,
467
- });
468
- const { indentation, isDragOverlay } = useContext(ListViewDraggingContext);
469
-
470
- const handlePress = useCallback(
471
- (event: React.MouseEvent) => {
472
- // We use preventDefault as a hack to mark this event as handled. We check for
473
- // this in the ListView.Root. We can't stopPropagation here or existing ContextMenus
474
- // won't close (onPointerDownOutside won't fire).
475
- event.preventDefault();
476
-
477
- // We only want to handle left clicks
478
- if (event.button !== 0) return;
479
-
480
- onPress?.(event);
481
- },
482
- [onPress]
483
- );
484
-
485
- const handleDoubleClick = useCallback(
486
- (event: React.MouseEvent) => {
487
- event.stopPropagation();
488
-
489
- onDoubleClick?.();
490
- },
491
- [onDoubleClick]
492
- );
493
-
494
- const mergedStyle = useMemo(() => {
495
- return isDragOverlay
496
- ? {
497
- // TODO: Where do these offsets actually come from?
498
- transform: `translate3d(-36px, -6px, 0)`,
499
- width: `calc(100% + 48px)`,
500
- ...style,
501
- }
502
- : style;
503
- }, [isDragOverlay, style]);
504
-
505
- const renderContent = (
506
- {
507
- relativeDropPosition: relativeDropPositionProp,
508
- ...renderProps
509
- }: React.ComponentProps<typeof RowContainer> & {
510
- relativeDropPosition?: RelativeDropPosition;
511
- },
512
- ref: Ref<HTMLElement>
513
- ) => {
514
- const Component = RowContainer;
515
- const relativeDropPosition =
516
- testRelativeDropPosition ?? relativeDropPositionProp;
517
-
518
- const element = (
519
- <Component
520
- ref={ref as any}
521
- $colorScheme={colorScheme}
522
- onContextMenu={onContextMenu}
523
- $isSectionHeader={isSectionHeader}
524
- id={id}
525
- role={role}
526
- $gap={gap ?? 0}
527
- $backgroundColor={backgroundColor}
528
- {...hoverProps}
529
- onDoubleClick={handleDoubleClick}
530
- $marginType={marginType ?? "none"}
531
- $disabled={disabled}
532
- $hovered={hovered}
533
- $selected={selected}
534
- $variant={variant}
535
- $sectionHeaderVariant={sectionHeaderVariant}
536
- $selectedPosition={selectedPosition}
537
- $showsActiveState={pressEventName === "onClick"}
538
- aria-selected={selected}
539
- $divider={!isDragOverlay && divider}
540
- onKeyDown={onKeyDown}
541
- style={mergedStyle}
542
- {...(renderProps as Partial<React.ComponentProps<typeof RowContainer>>)}
543
- {...mergeEventHandlers(
544
- { onPointerDown: renderProps.onPointerDown! },
545
- { [pressEventName]: handlePress }
546
- )}
547
- tabIndex={tabIndex}
548
- className={className}
549
- $clickable={!!onPress}
550
- >
551
- {relativeDropPosition && (
552
- <ListViewDragIndicatorElement
553
- key={relativeDropPosition}
554
- $colorScheme={colorScheme}
555
- $relativeDropPosition={relativeDropPosition}
556
- $offsetLeft={depth * indentation}
557
- $gap={listGap + (divider ? 1 : 0)}
558
- style={
559
- typeof dragIndicatorStyle === "function"
560
- ? dragIndicatorStyle({
561
- depth,
562
- indentation,
563
- position: relativeDropPosition,
564
- })
565
- : dragIndicatorStyle
566
- }
567
- />
568
- )}
569
- {depth > 0 && <Spacer.Horizontal size={depth * indentation} />}
570
- {children}
571
- </Component>
572
- );
573
-
574
- if (menuItems && onSelectMenuItem) {
575
- return (
576
- <ContextMenu<MenuItemType>
577
- items={menuItems}
578
- onSelect={onSelectMenuItem}
579
- onOpenChange={onMenuOpenChange}
580
- >
581
- {element}
582
- </ContextMenu>
583
- );
584
- }
585
-
586
- return element;
587
- };
588
-
589
- if (sortable && id) {
590
- return (
591
- <Sortable.Item<HTMLElement> id={id} disabled={overrideSortable === false}>
592
- {({ ref: sortableRef, ...sortableProps }) =>
593
- renderContent(
594
- sortableProps as any,
595
- composeRefs(sortableRef, forwardedRef)
596
- )
597
- }
598
- </Sortable.Item>
599
- );
600
- }
601
-
602
- return renderContent({} as any, forwardedRef);
603
- });
604
-
605
- /* ----------------------------------------------------------------------------
606
- * VirtualizedListRow
607
- * ------------------------------------------------------------------------- */
608
-
609
- const RenderItemContext = createContext<(index: number) => ReactNode>(
610
- () => null
611
- );
612
- (RenderItemContext as any).displayName = "RenderItemContext";
613
-
614
- const VirtualizedListRow = memo(function VirtualizedListRow({
615
- index,
616
- style,
617
- }: ListChildComponentProps) {
618
- const renderItem = useContext(RenderItemContext);
619
-
620
- return (
621
- <div key={index} style={style}>
622
- {renderItem(index)}
623
- </div>
624
- );
625
- });
1
+ "use client";
626
2
 
627
- /* ----------------------------------------------------------------------------
628
- * VirtualizedList
629
- * ------------------------------------------------------------------------- */
630
-
631
- interface VirtualizedListProps<T> {
632
- size: Size;
633
- scrollElement: HTMLDivElement;
634
- items: T[];
635
- getItemHeight: (index: number) => number;
636
- keyExtractor: (index: number) => string;
637
- renderItem: (index: number) => ReactNode;
638
- }
639
-
640
- export interface IVirtualizedList {
641
- scrollToIndex(index: number): void;
642
- }
643
-
644
- const VirtualizedListInner = forwardRefGeneric(function VirtualizedListInner<T>(
645
- {
646
- size,
647
- scrollElement,
648
- items,
649
- getItemHeight,
650
- keyExtractor,
651
- renderItem,
652
- }: VirtualizedListProps<T>,
653
- ref: ForwardedRef<IVirtualizedList>
654
- ) {
655
- const listRef = useRef<VariableSizeList<T> | null>(null);
656
-
657
- useImperativeHandle(ref, () => ({
658
- scrollToIndex(index) {
659
- listRef.current?.scrollToItem(index);
660
- },
661
- }));
662
-
663
- useLayoutEffect(() => {
664
- listRef.current?.resetAfterIndex(0);
665
- }, [
666
- // When items change, we need to re-render the virtualized list,
667
- // since it doesn't currently support row height changes
668
- items,
669
- ]);
670
-
671
- // Internally, react-virtualized updates these properties. We always want
672
- // to use our custom scroll element, so we override them. It may update
673
- // overflowX/Y individually in addition to `overflow`, so we include all 3.
674
- const listStyle = useMemo(
675
- (): CSSProperties => ({
676
- overflowX: "initial",
677
- overflowY: "initial",
678
- overflow: "initial",
679
- }),
680
- []
681
- );
682
-
683
- return (
684
- <RenderItemContext.Provider value={renderItem}>
685
- <WindowScroller
686
- scrollElement={scrollElement}
687
- style={useMemo(() => ({ flex: "1 1 auto" }), [])}
688
- >
689
- {useCallback(
690
- ({
691
- registerChild,
692
- onChildScroll,
693
- scrollTop,
694
- }: WindowScrollerChildProps & {
695
- // Added when noya-designsystem moved to a separate package.
696
- // I think this is a hack to get around the fact that react-virtualized
697
- // doesn't update on scroll unless we force it to by changing ref.
698
- // Either we're not using this the intended way or the types are wrong.
699
- registerChild: (element: any) => void;
700
- }) => (
701
- <div ref={registerChild}>
702
- <VariableSizeList<T>
703
- ref={listRef}
704
- // The list won't update on scroll unless we force it to by changing key
705
- key={scrollTop}
706
- style={listStyle}
707
- itemKey={keyExtractor}
708
- onScroll={({ scrollOffset }: { scrollOffset: number }) => {
709
- onChildScroll({ scrollTop: scrollOffset });
710
- }}
711
- initialScrollOffset={scrollTop}
712
- width={size.width}
713
- height={size.height}
714
- itemCount={items.length}
715
- itemSize={getItemHeight}
716
- estimatedItemSize={ROW_HEIGHT}
717
- >
718
- {VirtualizedListRow as any}
719
- </VariableSizeList>
720
- </div>
721
- ),
722
- [
723
- listStyle,
724
- keyExtractor,
725
- size.width,
726
- size.height,
727
- items.length,
728
- getItemHeight,
729
- ]
730
- )}
731
- </WindowScroller>
732
- </RenderItemContext.Provider>
733
- );
734
- });
735
-
736
- const VirtualizedList = memo(
737
- VirtualizedListInner
738
- ) as typeof VirtualizedListInner;
739
-
740
- /* ----------------------------------------------------------------------------
741
- * Root
742
- * ------------------------------------------------------------------------- */
743
-
744
- export type ListViewItemInfo = {
745
- isDragOverlay: boolean;
746
- activeDragIndex?: number;
747
- };
748
-
749
- type ChildrenProps = {
750
- children: ReactNode;
751
- };
752
-
753
- type RenderProps<T> = {
754
- data: T[];
755
- renderItem: (item: T, index: number, info: ListViewItemInfo) => ReactNode;
756
- keyExtractor: (item: T, index: number) => string;
757
- /**
758
- * Each item must have an `id` in order to be sortable.
759
- *
760
- * Additionally, the key extracted with `keyExtractor` must match the ListItem.Row `id`.
761
- */
762
- sortable?: boolean;
763
- virtualized?: Size;
764
- };
765
-
766
- type ListViewVariant = "normal" | "bare";
767
-
768
- type ListViewSectionHeaderVariant = "normal" | "label";
769
-
770
- export type ListViewRootProps = {
771
- id?: string;
772
- className?: string;
773
- style?: CSSProperties;
774
- role?: string;
775
- "aria-labelledby"?: string;
776
- "aria-orientation"?: "vertical" | "horizontal";
777
- "aria-multiselectable"?: boolean;
778
- onPress?: () => void;
779
- scrollable?: boolean;
780
- expandable?: boolean;
781
- onMoveItem?: MoveDragItemHandler;
782
- indentation?: number;
783
- acceptsDrop?: DropValidator;
784
- pressEventName?: PressEventName;
785
- variant?: ListViewVariant;
786
- sectionHeaderVariant?: ListViewSectionHeaderVariant;
787
- divider?: boolean;
788
- gap?: number;
789
- colorScheme?: ListColorScheme;
790
- containerRef?: React.RefObject<HTMLDivElement>;
791
- onDragOver?: React.DragEventHandler<HTMLDivElement>;
792
- onDragEnter?: React.DragEventHandler<HTMLDivElement>;
793
- onDragLeave?: React.DragEventHandler<HTMLDivElement>;
794
- onDrop?: React.DragEventHandler<HTMLDivElement>;
795
- renderEmptyState?: () => ReactNode;
796
- getDropTargetParentIndex?: SortableItemContextProps["getDropTargetParentIndex"];
797
- sharedDragProps?: SharedDragProps;
798
- sortableId?: string;
799
- };
800
-
801
- const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
802
- {
803
- id,
804
- className,
805
- style,
806
- role,
807
- "aria-labelledby": ariaLabelledby,
808
- "aria-orientation": ariaOrientation,
809
- "aria-multiselectable": ariaMultiselectable,
810
- onPress,
811
- scrollable = false,
812
- expandable = true,
813
- sortable = false,
814
- divider = false,
815
- onMoveItem,
816
- indentation = 12,
817
- acceptsDrop,
818
- data,
819
- renderItem,
820
- keyExtractor,
821
- virtualized,
822
- variant = "normal",
823
- sectionHeaderVariant = "normal",
824
- pressEventName = "onClick",
825
- colorScheme = "primary",
826
- gap = 0,
827
- containerRef,
828
- onDragOver,
829
- onDragEnter,
830
- onDragLeave,
831
- onDrop,
832
- renderEmptyState,
833
- getDropTargetParentIndex,
834
- sharedDragProps,
835
- sortableId,
836
- }: RenderProps<T> & ListViewRootProps,
837
- forwardedRef: ForwardedRef<IVirtualizedList>
838
- ) {
839
- const handleClick = useCallback(
840
- (event: React.MouseEvent) => {
841
- if (
842
- event.target instanceof HTMLElement &&
843
- event.target.classList.contains("scroll-component")
844
- )
845
- return;
846
-
847
- // As a hack, we call preventDefault in a row if the event was handled.
848
- // If the event wasn't handled already, we call onPress here.
849
- if (!event.isDefaultPrevented()) {
850
- onPress?.();
851
- }
852
- },
853
- [onPress]
854
- );
855
-
856
- const renderChild = useCallback(
857
- (index: number) => renderItem(data[index], index, { isDragOverlay: false }),
858
- [data, renderItem]
859
- );
860
-
861
- // TODO: Do we still use section header properties anywhere?
862
- const defaultContextValue = useMemo(
863
- (): ListRowContextValue => ({
864
- colorScheme,
865
- marginType: "none",
866
- selectedPosition: "only",
867
- sortable,
868
- expandable,
869
- divider,
870
- pressEventName,
871
- variant,
872
- gap,
873
- sectionHeaderVariant,
874
- isSectionHeader: false,
875
- }),
876
- [
877
- colorScheme,
878
- sortable,
879
- expandable,
880
- divider,
881
- pressEventName,
882
- variant,
883
- gap,
884
- sectionHeaderVariant,
885
- ]
886
- );
887
-
888
- const getItemContextValue = useCallback(
889
- (i: number): ListRowContextValue | undefined => {
890
- if (variant === "bare" || variant === "normal")
891
- return defaultContextValue;
892
-
893
- const current = renderChild(i);
894
-
895
- if (!isValidElement(current)) return;
896
-
897
- const prevChild = i - 1 >= 0 && renderChild(i - 1);
898
- const nextChild = i + 1 < data.length && renderChild(i + 1);
899
-
900
- const next: ReactElement | undefined = isValidElement(nextChild)
901
- ? nextChild
902
- : undefined;
903
- const prev: ReactElement | undefined = isValidElement(prevChild)
904
- ? prevChild
905
- : undefined;
906
-
907
- const hasMarginTop = !prev;
908
- const hasMarginBottom =
909
- !next ||
910
- current.props.isSectionHeader ||
911
- (next && next.props.isSectionHeader);
912
-
913
- let marginType: ListRowMarginType;
914
-
915
- if (hasMarginTop && hasMarginBottom) {
916
- marginType = "vertical";
917
- } else if (hasMarginBottom) {
918
- marginType = "bottom";
919
- } else if (hasMarginTop) {
920
- marginType = "top";
921
- } else {
922
- marginType = "none";
923
- }
924
-
925
- let selectedPosition: ListRowPosition = "only";
926
-
927
- if (current.props.selected) {
928
- const nextSelected =
929
- next && !next.props.isSectionHeader && next.props.selected;
930
- const prevSelected =
931
- prev && !prev.props.isSectionHeader && prev.props.selected;
932
-
933
- if (nextSelected && prevSelected) {
934
- selectedPosition = "middle";
935
- } else if (nextSelected && !prevSelected) {
936
- selectedPosition = "first";
937
- } else if (!nextSelected && prevSelected) {
938
- selectedPosition = "last";
939
- }
940
- }
941
-
942
- return {
943
- colorScheme,
944
- marginType,
945
- selectedPosition,
946
- sortable,
947
- expandable,
948
- divider,
949
- pressEventName,
950
- variant,
951
- sectionHeaderVariant,
952
- isSectionHeader: current.props.isSectionHeader,
953
- gap,
954
- };
955
- },
956
- [
957
- variant,
958
- defaultContextValue,
959
- renderChild,
960
- data.length,
961
- colorScheme,
962
- sortable,
963
- expandable,
964
- divider,
965
- pressEventName,
966
- sectionHeaderVariant,
967
- gap,
968
- ]
969
- );
970
-
971
- const draggingContextOverlayValue = useMemo(
972
- (): ListViewDraggingContextValue => ({ indentation, isDragOverlay: true }),
973
- [indentation]
974
- );
975
-
976
- const renderOverlay = useCallback(
977
- (index: number) => (
978
- <div style={{ opacity: 0.25 }}>
979
- <ListViewDraggingContext.Provider value={draggingContextOverlayValue}>
980
- {renderItem(data[index], index, { isDragOverlay: true })}
981
- </ListViewDraggingContext.Provider>
982
- </div>
983
- ),
984
- [draggingContextOverlayValue, renderItem, data]
985
- );
986
-
987
- const renderWrappedChild = useCallback(
988
- (index: number) => {
989
- const contextValue = getItemContextValue(index);
990
- const current = renderChild(index);
991
-
992
- if (!contextValue || !isValidElement(current)) return null;
993
-
994
- return (
995
- <ListRowContext.Provider key={current.key} value={contextValue}>
996
- {current}
997
- </ListRowContext.Provider>
998
- );
999
- },
1000
- [getItemContextValue, renderChild]
1001
- );
1002
-
1003
- const ids = useMemo(() => data.map(keyExtractor), [keyExtractor, data]);
1004
-
1005
- const withSortable = (children: ReactNode) =>
1006
- sortable ? (
1007
- <Sortable.Root
1008
- id={sortableId}
1009
- onMoveItem={onMoveItem}
1010
- keys={ids}
1011
- renderOverlay={renderOverlay}
1012
- acceptsDrop={acceptsDrop}
1013
- getDropTargetParentIndex={getDropTargetParentIndex}
1014
- sharedDragProps={sharedDragProps}
1015
- >
1016
- {children}
1017
- </Sortable.Root>
1018
- ) : (
1019
- children
1020
- );
1021
-
1022
- const withScrollable = (
1023
- children: (scrollElementRef: HTMLDivElement | null) => ReactNode
1024
- ) => (scrollable ? <ScrollArea>{children}</ScrollArea> : children(null));
1025
-
1026
- const getItemHeight = useCallback(
1027
- (index: number) => {
1028
- const child = getItemContextValue(index);
1029
- const height =
1030
- child?.isSectionHeader && child.sectionHeaderVariant === "label"
1031
- ? SECTION_HEADER_LABEL_HEIGHT
1032
- : ROW_HEIGHT;
1033
- return height;
1034
- },
1035
- [getItemContextValue]
1036
- );
1037
-
1038
- const getKey = useCallback(
1039
- (index: number) => keyExtractor(data[index], index),
1040
- [data, keyExtractor]
1041
- );
1042
-
1043
- const draggingContextValue = useMemo(() => ({ indentation }), [indentation]);
1044
- const gapStyle = useMemo(() => ({ gap: `${gap}px` }), [gap]);
1045
-
1046
- return (
1047
- <ListViewDraggingContext.Provider value={draggingContextValue}>
1048
- <div
1049
- id={id}
1050
- role={role}
1051
- aria-labelledby={ariaLabelledby}
1052
- aria-orientation={ariaOrientation}
1053
- aria-multiselectable={ariaMultiselectable}
1054
- className={mergeConflictingClassNames(
1055
- [
1056
- `flex flex-col text-text-muted ${scrollable ? "flex-1" : "flex-none"} `,
1057
- className,
1058
- ],
1059
- {
1060
- categories: ["flex"],
1061
- }
1062
- )}
1063
- style={{ ...gapStyle, ...style }}
1064
- {...{
1065
- [pressEventName]: handleClick,
1066
- }}
1067
- ref={containerRef}
1068
- onDragOver={onDragOver}
1069
- onDragEnter={onDragEnter}
1070
- onDragLeave={onDragLeave}
1071
- onDrop={onDrop}
1072
- >
1073
- {data.length === 0 && renderEmptyState
1074
- ? renderEmptyState()
1075
- : withScrollable((scrollElementRef: HTMLDivElement | null) =>
1076
- withSortable(
1077
- virtualized ? (
1078
- <VirtualizedList<T>
1079
- ref={forwardedRef}
1080
- scrollElement={scrollElementRef!}
1081
- items={data}
1082
- size={virtualized}
1083
- getItemHeight={getItemHeight}
1084
- keyExtractor={getKey}
1085
- renderItem={renderWrappedChild}
1086
- />
1087
- ) : (
1088
- range(0, data.length).map(renderWrappedChild)
1089
- )
1090
- )
1091
- )}
1092
- </div>
1093
- </ListViewDraggingContext.Provider>
1094
- );
1095
- });
1096
-
1097
- const ListViewRoot = memoGeneric(ListViewRootInner) as typeof ListViewRootInner;
1098
-
1099
- const ChildrenListViewInner = forwardRef(function ChildrenListViewInner(
1100
- { children, ...rest }: ChildrenProps & ListViewRootProps,
1101
- forwardedRef: ForwardedRef<IVirtualizedList>
1102
- ) {
1103
- const items: ReactElement[] = useMemo(
1104
- () =>
1105
- Children.toArray(children).flatMap((child) =>
1106
- isValidElement(child) ? [child] : []
1107
- ),
1108
- [children]
1109
- );
1110
-
1111
- return (
1112
- <ListViewRoot
1113
- ref={forwardedRef}
1114
- {...rest}
1115
- data={items}
1116
- keyExtractor={useCallback(
1117
- ({ key }: { key: string | number | null }, index: number) =>
1118
- typeof key === "string" ? key : (key ?? index).toString(),
1119
- []
1120
- )}
1121
- renderItem={useCallback((item: ReactElement) => item, [])}
1122
- />
1123
- );
1124
- });
1125
-
1126
- const ChildrenListView = memo(ChildrenListViewInner);
1127
-
1128
- const SimpleListViewInner = forwardRefGeneric(function SimpleListViewInner<
1129
- T = any,
1130
- >(
1131
- props: (ChildrenProps | RenderProps<T>) & ListViewRootProps,
1132
- forwardedRef: ForwardedRef<IVirtualizedList>
1133
- ) {
1134
- if ("children" in props) {
1135
- return <ChildrenListView ref={forwardedRef} {...props} />;
1136
- } else {
1137
- return <ListViewRoot ref={forwardedRef} {...props} />;
1138
- }
1139
- });
1140
-
1141
- /**
1142
- * A ListView can be created either with `children` or render props
1143
- */
1144
- const SimpleListView = memoGeneric(SimpleListViewInner);
3
+ import { IVirtualizedList } from "./IVirtualizedList";
4
+ import { ListRowContext } from "./listView/ListViewContexts";
5
+ import { ListViewEditableRowTitle } from "./listView/ListViewEditableRowTitle";
6
+ import {
7
+ ListViewItemInfo,
8
+ ListViewRoot,
9
+ ROW_HEIGHT,
10
+ SECTION_HEADER_LABEL_HEIGHT,
11
+ } from "./listView/ListViewRoot";
12
+ import {
13
+ ListViewClickInfo,
14
+ ListViewDragIndicatorElement,
15
+ ListViewRow,
16
+ ListViewRowProps,
17
+ } from "./listView/ListViewRow";
18
+ import { ListViewRowTitle } from "./listView/ListViewRowTitle";
19
+ import { normalizeListTargetIndex } from "./sorting/sorting";
1145
20
 
1146
21
  export namespace ListView {
1147
- export const RowTitle = memo(ListViewRowTitle);
1148
- export const EditableRowTitle = memo(ListViewEditableRowTitle);
1149
- export const Row = memoGeneric(ListViewRow);
1150
- export const Root = SimpleListView;
22
+ export const RowTitle = ListViewRowTitle;
23
+ export const EditableRowTitle = ListViewEditableRowTitle;
24
+ export const Row = ListViewRow;
25
+ export const Root = ListViewRoot;
1151
26
  export const RowContext = ListRowContext;
1152
27
  export type ClickInfo = ListViewClickInfo;
1153
28
  export type ItemInfo = ListViewItemInfo;