@noya-app/noya-designsystem 0.0.2

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 (73) hide show
  1. package/.turbo/turbo-build.log +17 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +1 -0
  4. package/dist/index.d.mts +1178 -0
  5. package/dist/index.d.ts +1178 -0
  6. package/dist/index.js +15651 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/index.mjs +15684 -0
  9. package/dist/index.mjs.map +1 -0
  10. package/package.json +49 -0
  11. package/src/__tests__/__snapshots__/fuzzyScorer.test.ts.snap +41 -0
  12. package/src/__tests__/fuzzyScorer.test.ts +85 -0
  13. package/src/components/ActivityIndicator.tsx +44 -0
  14. package/src/components/Avatar.tsx +64 -0
  15. package/src/components/Button.tsx +209 -0
  16. package/src/components/Chip.tsx +214 -0
  17. package/src/components/ContextMenu.tsx +224 -0
  18. package/src/components/Dialog.tsx +143 -0
  19. package/src/components/Divider.tsx +40 -0
  20. package/src/components/DropdownMenu.tsx +208 -0
  21. package/src/components/FillInputField.tsx +43 -0
  22. package/src/components/FillPreviewBackground.tsx +132 -0
  23. package/src/components/GradientPicker.tsx +88 -0
  24. package/src/components/Grid.tsx +54 -0
  25. package/src/components/GridView.tsx +468 -0
  26. package/src/components/IconButton.tsx +46 -0
  27. package/src/components/InputField.tsx +568 -0
  28. package/src/components/InputFieldWithCompletions.tsx +477 -0
  29. package/src/components/Label.tsx +62 -0
  30. package/src/components/LabeledElementView.tsx +185 -0
  31. package/src/components/ListView.tsx +950 -0
  32. package/src/components/Popover.tsx +113 -0
  33. package/src/components/Progress.tsx +57 -0
  34. package/src/components/RadioGroup.tsx +163 -0
  35. package/src/components/ScrollArea.tsx +70 -0
  36. package/src/components/Select.tsx +152 -0
  37. package/src/components/Slider.tsx +82 -0
  38. package/src/components/Sortable.tsx +296 -0
  39. package/src/components/Spacer.tsx +29 -0
  40. package/src/components/Stack.tsx +142 -0
  41. package/src/components/Switch.tsx +67 -0
  42. package/src/components/Text.tsx +164 -0
  43. package/src/components/Toast.tsx +86 -0
  44. package/src/components/Tooltip.tsx +43 -0
  45. package/src/components/TreeView.tsx +85 -0
  46. package/src/components/internal/Menu.tsx +222 -0
  47. package/src/components/internal/TextInput.tsx +296 -0
  48. package/src/components/internal/__tests__/TextInput.test.tsx +144 -0
  49. package/src/contexts/DesignSystemConfiguration.tsx +57 -0
  50. package/src/contexts/DialogContext.tsx +190 -0
  51. package/src/contexts/GlobalInputBlurContext.tsx +61 -0
  52. package/src/contexts/ImageDataContext.tsx +44 -0
  53. package/src/hooks/__tests__/mergeEventHandlers.test.ts +47 -0
  54. package/src/hooks/mergeEventHandlers.ts +55 -0
  55. package/src/hooks/useHover.ts +173 -0
  56. package/src/hooks/useObjectURL.ts +22 -0
  57. package/src/hooks/usePlatform.ts +13 -0
  58. package/src/index.tsx +77 -0
  59. package/src/mediaQuery.ts +13 -0
  60. package/src/theme/dark.ts +37 -0
  61. package/src/theme/index.ts +17 -0
  62. package/src/theme/light.ts +178 -0
  63. package/src/utils/breakpoints.ts +45 -0
  64. package/src/utils/completions.ts +21 -0
  65. package/src/utils/createSectionedMenu.ts +38 -0
  66. package/src/utils/fuzzyScorer.ts +105 -0
  67. package/src/utils/getGradientBackground.tsx +33 -0
  68. package/src/utils/handleNudge.ts +30 -0
  69. package/src/utils/mouseEvent.ts +7 -0
  70. package/src/utils/sketchColor.ts +34 -0
  71. package/src/utils/sketchPattern.ts +29 -0
  72. package/src/utils/withSeparatorElements.ts +31 -0
  73. package/tsconfig.json +3 -0
@@ -0,0 +1,950 @@
1
+ import { Size } from "@noya-app/noya-geometry";
2
+ import { range } from "@noya-app/noya-utils";
3
+ import { composeRefs } from "@radix-ui/react-compose-refs";
4
+ import React, {
5
+ Children,
6
+ createContext,
7
+ CSSProperties,
8
+ ForwardedRef,
9
+ forwardRef,
10
+ isValidElement,
11
+ memo,
12
+ ReactElement,
13
+ ReactNode,
14
+ Ref,
15
+ useCallback,
16
+ useContext,
17
+ useImperativeHandle,
18
+ useLayoutEffect,
19
+ useMemo,
20
+ useRef,
21
+ } from "react";
22
+ import { WindowScroller, WindowScrollerChildProps } from "react-virtualized";
23
+ import { ListChildComponentProps, VariableSizeList } from "react-window";
24
+ import styled from "styled-components";
25
+ import { mergeEventHandlers } from "../hooks/mergeEventHandlers";
26
+ import { useHover } from "../hooks/useHover";
27
+ import { isLeftButtonClicked } from "../utils/mouseEvent";
28
+ import { ContextMenu } from "./ContextMenu";
29
+ import { InputField } from "./InputField";
30
+ import { MenuItem } from "./internal/Menu";
31
+ import { ScrollArea } from "./ScrollArea";
32
+ import {
33
+ DropValidator,
34
+ normalizeListIndex,
35
+ RelativeDropPosition,
36
+ Sortable,
37
+ } from "./Sortable";
38
+ import { Spacer } from "./Spacer";
39
+
40
+ export type ListRowMarginType = "none" | "top" | "bottom" | "vertical";
41
+ export type ListRowPosition = "only" | "first" | "middle" | "last";
42
+
43
+ const ROW_HEIGHT = 31;
44
+ const SECTION_HEADER_LABEL_HEIGHT = 27;
45
+
46
+ type ListColorScheme = "primary" | "secondary";
47
+
48
+ type PressEventName = "onClick" | "onPointerDown";
49
+
50
+ type ListRowContextValue = {
51
+ marginType: ListRowMarginType;
52
+ selectedPosition: ListRowPosition;
53
+ sortable: boolean;
54
+ expandable: boolean;
55
+ divider: boolean;
56
+ gap: number;
57
+ variant: ListViewVariant;
58
+ sectionHeaderVariant: ListViewSectionHeaderVariant;
59
+ indentation: number;
60
+ pressEventName: PressEventName;
61
+ isSectionHeader: boolean;
62
+ colorScheme: ListColorScheme;
63
+ };
64
+
65
+ const ListRowContext = createContext<ListRowContextValue>({
66
+ marginType: "none",
67
+ selectedPosition: "only",
68
+ sortable: false,
69
+ expandable: true,
70
+ divider: true,
71
+ gap: 0,
72
+ variant: "normal",
73
+ sectionHeaderVariant: "normal",
74
+ indentation: 12,
75
+ pressEventName: "onClick",
76
+ isSectionHeader: false,
77
+ colorScheme: "primary",
78
+ });
79
+
80
+ /* ----------------------------------------------------------------------------
81
+ * RowTitle
82
+ * ------------------------------------------------------------------------- */
83
+
84
+ const ListViewRowTitle = styled.span(({ theme }) => ({
85
+ flex: "1 1 0",
86
+ overflow: "hidden",
87
+ textOverflow: "ellipsis",
88
+ whiteSpace: "pre",
89
+ }));
90
+
91
+ /* ----------------------------------------------------------------------------
92
+ * EditableRowTitle
93
+ * ------------------------------------------------------------------------- */
94
+
95
+ const ListViewEditableRowTitleElement = styled(InputField.Input)(
96
+ ({ theme }) => ({
97
+ background: theme.colors.listView.editingBackground,
98
+ })
99
+ ) as typeof InputField.Input;
100
+
101
+ export interface EditableRowProps {
102
+ value: string;
103
+ onSubmitEditing: (value: string) => void;
104
+ autoFocus: boolean;
105
+ placeholder?: string;
106
+ }
107
+
108
+ function ListViewEditableRowTitle({
109
+ value,
110
+ onSubmitEditing,
111
+ autoFocus,
112
+ placeholder,
113
+ }: EditableRowProps) {
114
+ const inputRef = useRef<HTMLInputElement | null>(null);
115
+
116
+ useLayoutEffect(() => {
117
+ const element = inputRef.current;
118
+
119
+ if (!element || !autoFocus) return;
120
+
121
+ // Calling `focus` is necessary, in addition to `select`, to ensure
122
+ // the `onBlur` fires correctly.
123
+ element.focus();
124
+
125
+ setTimeout(() => {
126
+ element.select();
127
+ }, 0);
128
+ }, [autoFocus]);
129
+
130
+ return (
131
+ <ListViewEditableRowTitleElement
132
+ ref={inputRef}
133
+ variant="bare"
134
+ value={value}
135
+ placeholder={placeholder}
136
+ onSubmit={onSubmitEditing}
137
+ allowSubmittingWithSameValue
138
+ />
139
+ );
140
+ }
141
+
142
+ function getPositionMargin(marginType: ListRowMarginType) {
143
+ return {
144
+ top: marginType === "top" || marginType === "vertical" ? 8 : 0,
145
+ bottom: marginType === "bottom" || marginType === "vertical" ? 8 : 0,
146
+ };
147
+ }
148
+
149
+ /* ----------------------------------------------------------------------------
150
+ * Row
151
+ * ------------------------------------------------------------------------- */
152
+
153
+ const RowContainer = styled.div<{
154
+ marginType: ListRowMarginType;
155
+ selected: boolean;
156
+ selectedPosition: ListRowPosition;
157
+ disabled: boolean;
158
+ hovered: boolean;
159
+ variant: ListViewVariant;
160
+ divider: boolean;
161
+ isSectionHeader: boolean;
162
+ showsActiveState: boolean;
163
+ sectionHeaderVariant: ListViewSectionHeaderVariant;
164
+ colorScheme: ListColorScheme;
165
+ gap: number;
166
+ backgroundColor?: CSSProperties["backgroundColor"];
167
+ }>(
168
+ ({
169
+ theme,
170
+ marginType,
171
+ selected,
172
+ selectedPosition,
173
+ disabled,
174
+ hovered,
175
+ variant,
176
+ divider,
177
+ isSectionHeader,
178
+ showsActiveState,
179
+ sectionHeaderVariant,
180
+ colorScheme,
181
+ gap,
182
+ backgroundColor,
183
+ }) => {
184
+ const margin = getPositionMargin(marginType);
185
+
186
+ return {
187
+ ...(isSectionHeader && sectionHeaderVariant === "label"
188
+ ? theme.textStyles.label
189
+ : theme.textStyles.small),
190
+ ...(isSectionHeader && { fontWeight: 500 }),
191
+ gap,
192
+ flex: "0 0 auto",
193
+ userSelect: "none",
194
+ cursor: "default",
195
+ ...(variant !== "bare" && {
196
+ paddingTop: "6px",
197
+ paddingRight: "12px",
198
+ paddingBottom: "6px",
199
+ paddingLeft: "12px",
200
+ ...(variant === "padded" && {
201
+ borderRadius: "2px",
202
+ marginLeft: "8px",
203
+ marginRight: "8px",
204
+ marginTop: `${margin.top}px`,
205
+ marginBottom: `${margin.bottom}px`,
206
+ }),
207
+ }),
208
+ color: theme.colors.textMuted,
209
+ ...(isSectionHeader && {
210
+ backgroundColor: theme.colors.listView.raisedBackground,
211
+ ...(sectionHeaderVariant === "label" && {
212
+ color: theme.colors.textDisabled,
213
+ }),
214
+ }),
215
+ ...(disabled && {
216
+ color: theme.colors.textDisabled,
217
+ }),
218
+ ...(selected && {
219
+ color: "white",
220
+ backgroundColor: theme.colors[colorScheme],
221
+ }),
222
+ display: "flex",
223
+ alignItems: "center",
224
+ ...(selected &&
225
+ !isSectionHeader &&
226
+ (selectedPosition === "middle" || selectedPosition === "last") && {
227
+ borderTopRightRadius: "0px",
228
+ borderTopLeftRadius: "0px",
229
+ }),
230
+ ...(selected &&
231
+ !isSectionHeader &&
232
+ (selectedPosition === "middle" || selectedPosition === "first") && {
233
+ borderBottomRightRadius: "0px",
234
+ borderBottomLeftRadius: "0px",
235
+ }),
236
+ position: "relative",
237
+ ...(hovered && {
238
+ boxShadow: `0 0 0 1px ${theme.colors[colorScheme]} inset`,
239
+ }),
240
+ ...(showsActiveState && {
241
+ "&:active": {
242
+ backgroundColor: selected
243
+ ? colorScheme === "secondary"
244
+ ? theme.colors.secondaryLight
245
+ : theme.colors.primaryLight
246
+ : theme.colors.activeBackground,
247
+ },
248
+ }),
249
+ ...(divider && {
250
+ borderBottom: `1px solid ${theme.colors.dividerSubtle}`,
251
+ }),
252
+ ...(backgroundColor && {
253
+ backgroundColor,
254
+ "&:hover": {
255
+ backgroundColor,
256
+ },
257
+ "&:active": {
258
+ backgroundColor,
259
+ },
260
+ }),
261
+ };
262
+ }
263
+ );
264
+
265
+ const ListViewDragIndicatorElement = styled.div<{
266
+ relativeDropPosition: RelativeDropPosition;
267
+ gap: number;
268
+ offsetLeft: number;
269
+ colorScheme: ListColorScheme;
270
+ }>(({ theme, relativeDropPosition, offsetLeft, colorScheme, gap }) => ({
271
+ zIndex: 1,
272
+ position: "absolute",
273
+ borderRadius: "3px",
274
+ ...(relativeDropPosition === "inside"
275
+ ? {
276
+ inset: 2,
277
+ boxShadow: `0 0 0 1px ${theme.colors.sidebar.background}, 0 0 0 3px ${
278
+ colorScheme === "secondary"
279
+ ? theme.colors.secondary
280
+ : theme.colors.dragOutline
281
+ }`,
282
+ }
283
+ : {
284
+ top: relativeDropPosition === "above" ? -(3 + gap / 2) : undefined,
285
+ bottom: relativeDropPosition === "below" ? -(3 + gap / 2) : undefined,
286
+ left: offsetLeft,
287
+ right: 0,
288
+ height: 6,
289
+ background: theme.colors[colorScheme],
290
+ border: `2px solid white`,
291
+ boxShadow: "0 0 2px rgba(0,0,0,0.5)",
292
+ }),
293
+ }));
294
+
295
+ interface ListViewClickInfo {
296
+ shiftKey: boolean;
297
+ altKey: boolean;
298
+ metaKey: boolean;
299
+ ctrlKey: boolean;
300
+ }
301
+
302
+ interface ListViewRowProps<MenuItemType extends string = string> {
303
+ id?: string;
304
+ tabIndex?: number;
305
+ selected?: boolean;
306
+ depth?: number;
307
+ disabled?: boolean;
308
+ draggable?: boolean;
309
+ hovered?: boolean;
310
+ sortable?: boolean;
311
+ gap?: number;
312
+ backgroundColor?: CSSProperties["backgroundColor"];
313
+ onPress?: (info: ListViewClickInfo) => void;
314
+ onDoubleClick?: () => void;
315
+ onHoverChange?: (isHovering: boolean) => void;
316
+ children?: ReactNode;
317
+ isSectionHeader?: boolean;
318
+ menuItems?: MenuItem<MenuItemType>[];
319
+ onSelectMenuItem?: (value: MenuItemType) => void;
320
+ onContextMenu?: () => void;
321
+ onMenuOpenChange?: (isOpen: boolean) => void;
322
+ onKeyDown?: (event: React.KeyboardEvent) => void;
323
+ }
324
+
325
+ const ListViewRow = forwardRef(function ListViewRow<
326
+ MenuItemType extends string,
327
+ >(
328
+ {
329
+ id,
330
+ tabIndex = 0,
331
+ gap,
332
+ backgroundColor,
333
+ selected = false,
334
+ depth = 0,
335
+ disabled = false,
336
+ hovered = false,
337
+ isSectionHeader = false,
338
+ sortable: overrideSortable,
339
+ onPress,
340
+ onDoubleClick,
341
+ onHoverChange,
342
+ children,
343
+ menuItems,
344
+ onContextMenu,
345
+ onSelectMenuItem,
346
+ onMenuOpenChange,
347
+ onKeyDown,
348
+ }: ListViewRowProps<MenuItemType>,
349
+ forwardedRef: ForwardedRef<HTMLElement>
350
+ ) {
351
+ const {
352
+ marginType,
353
+ selectedPosition,
354
+ sortable,
355
+ indentation,
356
+ pressEventName,
357
+ variant,
358
+ sectionHeaderVariant,
359
+ divider,
360
+ gap: listGap,
361
+ colorScheme,
362
+ } = useContext(ListRowContext);
363
+ const { hoverProps } = useHover({
364
+ onHoverChange,
365
+ });
366
+
367
+ const handlePress = useCallback(
368
+ (event: React.MouseEvent) => {
369
+ // We use preventDefault as a hack to mark this event as handled. We check for
370
+ // this in the ListView.Root. We can't stopPropagation here or existing ContextMenus
371
+ // won't close (onPointerDownOutside won't fire).
372
+ event.preventDefault();
373
+
374
+ if (!isLeftButtonClicked(event)) return;
375
+
376
+ onPress?.(event);
377
+ },
378
+ [onPress]
379
+ );
380
+
381
+ const handleDoubleClick = useCallback(
382
+ (event: React.MouseEvent) => {
383
+ event.stopPropagation();
384
+
385
+ onDoubleClick?.();
386
+ },
387
+ [onDoubleClick]
388
+ );
389
+
390
+ const renderContent = (
391
+ {
392
+ relativeDropPosition,
393
+ ...renderProps
394
+ }: React.ComponentProps<typeof RowContainer> & {
395
+ relativeDropPosition?: RelativeDropPosition;
396
+ },
397
+ ref: Ref<HTMLElement>
398
+ ) => {
399
+ const element = (
400
+ <RowContainer
401
+ ref={ref}
402
+ colorScheme={colorScheme}
403
+ onContextMenu={onContextMenu}
404
+ isSectionHeader={isSectionHeader}
405
+ id={id}
406
+ gap={gap}
407
+ backgroundColor={backgroundColor}
408
+ {...hoverProps}
409
+ onDoubleClick={handleDoubleClick}
410
+ marginType={marginType}
411
+ disabled={disabled}
412
+ hovered={hovered}
413
+ selected={selected}
414
+ variant={variant}
415
+ sectionHeaderVariant={sectionHeaderVariant}
416
+ selectedPosition={selectedPosition}
417
+ showsActiveState={pressEventName === "onClick"}
418
+ aria-selected={selected}
419
+ divider={divider}
420
+ onKeyDown={onKeyDown}
421
+ {...renderProps}
422
+ {...mergeEventHandlers(
423
+ { onPointerDown: renderProps.onPointerDown },
424
+ { [pressEventName]: handlePress }
425
+ )}
426
+ tabIndex={tabIndex}
427
+ >
428
+ {relativeDropPosition && (
429
+ <ListViewDragIndicatorElement
430
+ colorScheme={colorScheme}
431
+ relativeDropPosition={relativeDropPosition}
432
+ offsetLeft={33 + depth * indentation}
433
+ gap={listGap}
434
+ />
435
+ )}
436
+ {depth > 0 && <Spacer.Horizontal size={depth * indentation} />}
437
+ {children}
438
+ </RowContainer>
439
+ );
440
+
441
+ if (menuItems && onSelectMenuItem) {
442
+ return (
443
+ <ContextMenu<MenuItemType>
444
+ items={menuItems}
445
+ onSelect={onSelectMenuItem}
446
+ onOpenChange={onMenuOpenChange}
447
+ >
448
+ {element}
449
+ </ContextMenu>
450
+ );
451
+ }
452
+
453
+ return element;
454
+ };
455
+
456
+ if (sortable && id) {
457
+ return (
458
+ <Sortable.Item<HTMLElement> id={id} disabled={overrideSortable === false}>
459
+ {({ ref: sortableRef, ...sortableProps }) =>
460
+ renderContent(sortableProps, composeRefs(sortableRef, forwardedRef))
461
+ }
462
+ </Sortable.Item>
463
+ );
464
+ }
465
+
466
+ return renderContent({}, forwardedRef);
467
+ });
468
+
469
+ /* ----------------------------------------------------------------------------
470
+ * VirtualizedListRow
471
+ * ------------------------------------------------------------------------- */
472
+
473
+ const RenderItemContext = createContext<(index: number) => ReactNode>(
474
+ () => null
475
+ );
476
+
477
+ const VirtualizedListRow = memo(function VirtualizedListRow({
478
+ index,
479
+ style,
480
+ }: ListChildComponentProps) {
481
+ const renderItem = useContext(RenderItemContext);
482
+
483
+ return (
484
+ <div key={index} style={style}>
485
+ {renderItem(index)}
486
+ </div>
487
+ );
488
+ });
489
+
490
+ /* ----------------------------------------------------------------------------
491
+ * VirtualizedList
492
+ * ------------------------------------------------------------------------- */
493
+
494
+ interface VirtualizedListProps<T> {
495
+ size: Size;
496
+ scrollElement: HTMLDivElement;
497
+ items: T[];
498
+ getItemHeight: (index: number) => number;
499
+ keyExtractor: (index: number) => string;
500
+ renderItem: (index: number) => ReactNode;
501
+ }
502
+
503
+ export interface IVirtualizedList {
504
+ scrollToIndex(index: number): void;
505
+ }
506
+
507
+ const VirtualizedListInner = forwardRef(function VirtualizedListInner<T>(
508
+ {
509
+ size,
510
+ scrollElement,
511
+ items,
512
+ getItemHeight,
513
+ keyExtractor,
514
+ renderItem,
515
+ }: VirtualizedListProps<T>,
516
+ ref: ForwardedRef<IVirtualizedList>
517
+ ) {
518
+ const listRef = useRef<VariableSizeList<T> | null>(null);
519
+
520
+ useImperativeHandle(ref, () => ({
521
+ scrollToIndex(index) {
522
+ listRef.current?.scrollToItem(index);
523
+ },
524
+ }));
525
+
526
+ useLayoutEffect(() => {
527
+ listRef.current?.resetAfterIndex(0);
528
+ }, [
529
+ // When items change, we need to re-render the virtualized list,
530
+ // since it doesn't currently support row height changes
531
+ items,
532
+ ]);
533
+
534
+ // Internally, react-virtualized updates these properties. We always want
535
+ // to use our custom scroll element, so we override them. It may update
536
+ // overflowX/Y individually in addition to `overflow`, so we include all 3.
537
+ const listStyle = useMemo(
538
+ (): CSSProperties => ({
539
+ overflowX: "initial",
540
+ overflowY: "initial",
541
+ overflow: "initial",
542
+ }),
543
+ []
544
+ );
545
+
546
+ return (
547
+ <RenderItemContext.Provider value={renderItem}>
548
+ <WindowScroller
549
+ scrollElement={scrollElement}
550
+ style={useMemo(() => ({ flex: "1 1 auto" }), [])}
551
+ >
552
+ {useCallback(
553
+ ({
554
+ registerChild,
555
+ onChildScroll,
556
+ scrollTop,
557
+ }: WindowScrollerChildProps & {
558
+ // Added when noya-designsystem moved to a separate package.
559
+ // I think this is a hack to get around the fact that react-virtualized
560
+ // doesn't update on scroll unless we force it to by changing ref.
561
+ // Either we're not using this the intended way or the types are wrong.
562
+ registerChild: (element: any) => void;
563
+ }) => (
564
+ <div ref={registerChild}>
565
+ <VariableSizeList<T>
566
+ ref={listRef}
567
+ // The list won't update on scroll unless we force it to by changing key
568
+ key={scrollTop}
569
+ style={listStyle}
570
+ itemKey={keyExtractor}
571
+ onScroll={({ scrollOffset }: { scrollOffset: number }) => {
572
+ onChildScroll({ scrollTop: scrollOffset });
573
+ }}
574
+ initialScrollOffset={scrollTop}
575
+ width={size.width}
576
+ height={size.height}
577
+ itemCount={items.length}
578
+ itemSize={getItemHeight}
579
+ estimatedItemSize={ROW_HEIGHT}
580
+ >
581
+ {VirtualizedListRow}
582
+ </VariableSizeList>
583
+ </div>
584
+ ),
585
+ [
586
+ listStyle,
587
+ keyExtractor,
588
+ size.width,
589
+ size.height,
590
+ items.length,
591
+ getItemHeight,
592
+ ]
593
+ )}
594
+ </WindowScroller>
595
+ </RenderItemContext.Provider>
596
+ );
597
+ });
598
+
599
+ const VirtualizedList = memo(
600
+ VirtualizedListInner
601
+ ) as typeof VirtualizedListInner;
602
+
603
+ /* ----------------------------------------------------------------------------
604
+ * Root
605
+ * ------------------------------------------------------------------------- */
606
+
607
+ const RootContainer = styled.div<{
608
+ scrollable?: boolean;
609
+ gap?: number;
610
+ }>(({ theme, scrollable, gap }) => ({
611
+ flex: scrollable ? "1 0 0" : "0 0 auto",
612
+ display: "flex",
613
+ flexDirection: "column",
614
+ flexWrap: "nowrap",
615
+ color: theme.colors.textMuted,
616
+ gap,
617
+ }));
618
+
619
+ type ListViewItemInfo = {
620
+ isDragging: boolean;
621
+ };
622
+
623
+ type ChildrenProps = {
624
+ children: ReactNode;
625
+ };
626
+
627
+ type RenderProps<T> = {
628
+ data: T[];
629
+ renderItem: (item: T, index: number, info: ListViewItemInfo) => ReactNode;
630
+ keyExtractor: (item: T, index: number) => string;
631
+ /**
632
+ * Each item must have an `id` in order to be sortable
633
+ */
634
+ sortable?: boolean;
635
+ virtualized?: Size;
636
+ };
637
+
638
+ type ListViewVariant = "normal" | "padded" | "bare";
639
+
640
+ type ListViewSectionHeaderVariant = "normal" | "label";
641
+
642
+ type ListViewRootProps = {
643
+ onPress?: () => void;
644
+ scrollable?: boolean;
645
+ expandable?: boolean;
646
+ onMoveItem?: (
647
+ sourceIndex: number,
648
+ destinationIndex: number,
649
+ position: RelativeDropPosition
650
+ ) => void;
651
+ indentation?: number;
652
+ acceptsDrop?: DropValidator;
653
+ pressEventName?: PressEventName;
654
+ variant?: ListViewVariant;
655
+ sectionHeaderVariant?: ListViewSectionHeaderVariant;
656
+ divider?: boolean;
657
+ gap?: number;
658
+ colorScheme?: ListColorScheme;
659
+ };
660
+
661
+ const ListViewRootInner = forwardRef(function ListViewRootInner<T>(
662
+ {
663
+ onPress,
664
+ scrollable = false,
665
+ expandable = true,
666
+ sortable = false,
667
+ divider = false,
668
+ onMoveItem,
669
+ indentation = 12,
670
+ acceptsDrop,
671
+ data,
672
+ renderItem,
673
+ keyExtractor,
674
+ virtualized,
675
+ variant = "normal",
676
+ sectionHeaderVariant = "normal",
677
+ pressEventName = "onClick",
678
+ colorScheme = "primary",
679
+ gap = 0,
680
+ }: RenderProps<T> & ListViewRootProps,
681
+ forwardedRef: ForwardedRef<IVirtualizedList>
682
+ ) {
683
+ const handleClick = useCallback(
684
+ (event: React.MouseEvent) => {
685
+ if (
686
+ event.target instanceof HTMLElement &&
687
+ event.target.classList.contains("scroll-component")
688
+ )
689
+ return;
690
+
691
+ // As a hack, we call preventDefault in a row if the event was handled.
692
+ // If the event wasn't handled already, we call onPress here.
693
+ if (!event.isDefaultPrevented()) {
694
+ onPress?.();
695
+ }
696
+ },
697
+ [onPress]
698
+ );
699
+
700
+ const renderChild = useCallback(
701
+ (index: number) => renderItem(data[index], index, { isDragging: false }),
702
+ [data, renderItem]
703
+ );
704
+
705
+ const renderOverlay = useCallback(
706
+ (index: number) => renderItem(data[index], index, { isDragging: true }),
707
+ [renderItem, data]
708
+ );
709
+
710
+ const getItemContextValue = useCallback(
711
+ (i: number): ListRowContextValue | undefined => {
712
+ const current = renderChild(i);
713
+
714
+ if (!isValidElement(current)) return;
715
+
716
+ const prevChild = i - 1 >= 0 && renderChild(i - 1);
717
+ const nextChild = i + 1 < data.length && renderChild(i + 1);
718
+
719
+ const next: ReactElement | undefined = isValidElement(nextChild)
720
+ ? nextChild
721
+ : undefined;
722
+ const prev: ReactElement | undefined = isValidElement(prevChild)
723
+ ? prevChild
724
+ : undefined;
725
+
726
+ const hasMarginTop = !prev;
727
+ const hasMarginBottom =
728
+ !next ||
729
+ current.props.isSectionHeader ||
730
+ (next && next.props.isSectionHeader);
731
+
732
+ let marginType: ListRowMarginType;
733
+
734
+ if (hasMarginTop && hasMarginBottom) {
735
+ marginType = "vertical";
736
+ } else if (hasMarginBottom) {
737
+ marginType = "bottom";
738
+ } else if (hasMarginTop) {
739
+ marginType = "top";
740
+ } else {
741
+ marginType = "none";
742
+ }
743
+
744
+ let selectedPosition: ListRowPosition = "only";
745
+
746
+ if (current.props.selected) {
747
+ const nextSelected =
748
+ next && !next.props.isSectionHeader && next.props.selected;
749
+ const prevSelected =
750
+ prev && !prev.props.isSectionHeader && prev.props.selected;
751
+
752
+ if (nextSelected && prevSelected) {
753
+ selectedPosition = "middle";
754
+ } else if (nextSelected && !prevSelected) {
755
+ selectedPosition = "first";
756
+ } else if (!nextSelected && prevSelected) {
757
+ selectedPosition = "last";
758
+ }
759
+ }
760
+
761
+ return {
762
+ colorScheme,
763
+ marginType,
764
+ selectedPosition,
765
+ sortable,
766
+ expandable,
767
+ divider,
768
+ indentation,
769
+ pressEventName,
770
+ variant,
771
+ sectionHeaderVariant,
772
+ isSectionHeader: current.props.isSectionHeader,
773
+ gap,
774
+ };
775
+ },
776
+ [
777
+ renderChild,
778
+ data.length,
779
+ colorScheme,
780
+ sortable,
781
+ expandable,
782
+ divider,
783
+ indentation,
784
+ pressEventName,
785
+ variant,
786
+ sectionHeaderVariant,
787
+ gap,
788
+ ]
789
+ );
790
+
791
+ const renderWrappedChild = useCallback(
792
+ (index: number) => {
793
+ const contextValue = getItemContextValue(index);
794
+ const current = renderChild(index);
795
+
796
+ if (!contextValue || !isValidElement(current)) return null;
797
+
798
+ return (
799
+ <ListRowContext.Provider key={current.key} value={contextValue}>
800
+ {current}
801
+ </ListRowContext.Provider>
802
+ );
803
+ },
804
+ [getItemContextValue, renderChild]
805
+ );
806
+
807
+ const ids = useMemo(() => data.map(keyExtractor), [keyExtractor, data]);
808
+
809
+ const withSortable = (children: ReactNode) =>
810
+ sortable ? (
811
+ <Sortable.Root
812
+ onMoveItem={onMoveItem}
813
+ keys={ids}
814
+ renderOverlay={renderOverlay}
815
+ acceptsDrop={acceptsDrop}
816
+ >
817
+ {children}
818
+ </Sortable.Root>
819
+ ) : (
820
+ children
821
+ );
822
+
823
+ const withScrollable = (
824
+ children: (scrollElementRef: HTMLDivElement | null) => ReactNode
825
+ ) => (scrollable ? <ScrollArea>{children}</ScrollArea> : children(null));
826
+
827
+ const getItemHeight = useCallback(
828
+ (index: number) => {
829
+ const child = getItemContextValue(index);
830
+ const margin = child?.marginType
831
+ ? getPositionMargin(child.marginType)
832
+ : { top: 0, bottom: 0 };
833
+ const height =
834
+ (child?.isSectionHeader && child.sectionHeaderVariant === "label"
835
+ ? SECTION_HEADER_LABEL_HEIGHT
836
+ : ROW_HEIGHT) +
837
+ (variant === "padded" ? margin.top + margin.bottom : 0);
838
+ return height;
839
+ },
840
+ [getItemContextValue, variant]
841
+ );
842
+
843
+ const getKey = useCallback(
844
+ (index: number) => keyExtractor(data[index], index),
845
+ [data, keyExtractor]
846
+ );
847
+
848
+ return (
849
+ <RootContainer
850
+ {...{
851
+ [pressEventName]: handleClick,
852
+ }}
853
+ gap={gap}
854
+ scrollable={scrollable}
855
+ >
856
+ {withScrollable((scrollElementRef: HTMLDivElement | null) =>
857
+ withSortable(
858
+ virtualized ? (
859
+ <VirtualizedList<T>
860
+ ref={forwardedRef}
861
+ scrollElement={scrollElementRef!}
862
+ items={data}
863
+ size={virtualized}
864
+ getItemHeight={getItemHeight}
865
+ keyExtractor={getKey}
866
+ renderItem={renderWrappedChild}
867
+ />
868
+ ) : (
869
+ range(0, data.length).map(renderWrappedChild)
870
+ )
871
+ )
872
+ )}
873
+ </RootContainer>
874
+ );
875
+ });
876
+
877
+ const ListViewRoot = memo(ListViewRootInner) as typeof ListViewRootInner;
878
+
879
+ const ChildrenListViewInner = forwardRef(function ChildrenListViewInner(
880
+ { children, ...rest }: ChildrenProps & ListViewRootProps,
881
+ forwardedRef: ForwardedRef<IVirtualizedList>
882
+ ) {
883
+ const items: ReactElement[] = useMemo(
884
+ () =>
885
+ Children.toArray(children).flatMap((child) =>
886
+ isValidElement(child) ? [child] : []
887
+ ),
888
+ [children]
889
+ );
890
+
891
+ return (
892
+ <ListViewRoot
893
+ ref={forwardedRef}
894
+ {...rest}
895
+ data={items}
896
+ keyExtractor={useCallback(
897
+ ({ key }: { key: string | number | null }, index: number) =>
898
+ typeof key === "string" ? key : (key ?? index).toString(),
899
+ []
900
+ )}
901
+ renderItem={useCallback((item: ReactElement) => item, [])}
902
+ />
903
+ );
904
+ });
905
+
906
+ const ChildrenListView = memo(ChildrenListViewInner);
907
+
908
+ const SimpleListViewInner = forwardRef(function SimpleListViewInner<T = any>(
909
+ props: (ChildrenProps | RenderProps<T>) & ListViewRootProps,
910
+ forwardedRef: ForwardedRef<IVirtualizedList>
911
+ ) {
912
+ if ("children" in props) {
913
+ return <ChildrenListView ref={forwardedRef} {...props} />;
914
+ } else {
915
+ return <ListViewRoot ref={forwardedRef} {...props} />;
916
+ }
917
+ });
918
+
919
+ /**
920
+ * A ListView can be created either with `children` or render props
921
+ */
922
+ const SimpleListView = memo(SimpleListViewInner);
923
+
924
+ export namespace ListView {
925
+ export const RowTitle = memo(ListViewRowTitle);
926
+ export const EditableRowTitle = memo(ListViewEditableRowTitle);
927
+ export const Row = memo(ListViewRow);
928
+ export const Root = SimpleListView;
929
+ export const RowContext = ListRowContext;
930
+ export type ClickInfo = ListViewClickInfo;
931
+ export type ItemInfo = ListViewItemInfo;
932
+ export type RowProps<MenuItemType extends string = string> =
933
+ ListViewRowProps<MenuItemType>;
934
+ export type VirtualizedList = IVirtualizedList;
935
+ export const DragIndicator = ListViewDragIndicatorElement;
936
+ export const rowHeight = ROW_HEIGHT;
937
+ export const sectionHeaderLabelHeight = SECTION_HEADER_LABEL_HEIGHT;
938
+ export const calculateHeight = (
939
+ items: number,
940
+ headerCount: number,
941
+ headerVariant: ListViewSectionHeaderVariant
942
+ ) => {
943
+ return (
944
+ items * rowHeight +
945
+ headerCount *
946
+ (headerVariant === "label" ? sectionHeaderLabelHeight : rowHeight)
947
+ );
948
+ };
949
+ export const normalizeIndex = normalizeListIndex;
950
+ }