@noya-app/noya-designsystem 0.0.15 → 0.1.0

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.
@@ -7,6 +7,7 @@ import React, {
7
7
  memo,
8
8
  useCallback,
9
9
  useEffect,
10
+ useImperativeHandle,
10
11
  useLayoutEffect,
11
12
  useMemo,
12
13
  useRef,
@@ -163,6 +164,12 @@ type Props = {
163
164
  hideMenuWhenEmptyValue?: boolean;
164
165
  };
165
166
 
167
+ export interface IInputFieldWithCompletions {
168
+ focus(): void;
169
+ setValue(value: string): void;
170
+ selectAllInputText(): void;
171
+ }
172
+
166
173
  export const InputFieldWithCompletions = memo(
167
174
  forwardRef(function InputFieldWithCompletions(
168
175
  {
@@ -183,10 +190,9 @@ export const InputFieldWithCompletions = memo(
183
190
  hideChildrenWhenFocused = false,
184
191
  hideMenuWhenEmptyValue = false,
185
192
  }: Props,
186
- forwardedRef: ForwardedRef<HTMLInputElement>
193
+ forwardedRef: ForwardedRef<IInputFieldWithCompletions>
187
194
  ) {
188
- const defaultRef = useRef<HTMLInputElement>(null);
189
- const ref = forwardedRef || defaultRef;
195
+ const ref = useRef<HTMLInputElement>(null);
190
196
 
191
197
  const [isFocused, setIsFocused] = useState(false);
192
198
  const [state, _setState] = useState({
@@ -338,12 +344,25 @@ export const InputFieldWithCompletions = memo(
338
344
 
339
345
  handled = true;
340
346
  break;
341
- case "Enter":
347
+ case "Enter": {
342
348
  const item = filteredItems[selectedIndex];
343
- selectItem(item);
349
+ if (item) {
350
+ selectItem(item);
351
+ }
352
+
353
+ handled = true;
354
+ break;
355
+ }
356
+ case "Tab": {
357
+ // Update the filter to the selected item
358
+ const item = filteredItems[selectedIndex];
359
+ if (item) {
360
+ handleChange(item.name);
361
+ }
344
362
 
345
363
  handled = true;
346
364
  break;
365
+ }
347
366
  case "Escape":
348
367
  if (ref && typeof ref === "object") {
349
368
  ref.current?.blur();
@@ -370,8 +389,9 @@ export const InputFieldWithCompletions = memo(
370
389
  handleIndexChange,
371
390
  filteredItems,
372
391
  selectedIndex,
373
- selectItem,
374
392
  ref,
393
+ selectItem,
394
+ handleChange,
375
395
  filter,
376
396
  onDeleteWhenEmpty,
377
397
  ]
@@ -379,6 +399,20 @@ export const InputFieldWithCompletions = memo(
379
399
 
380
400
  const display = !filter && hideMenuWhenEmptyValue ? "none" : "flex";
381
401
 
402
+ const typeaheadValue = filteredItems[selectedIndex]?.name;
403
+
404
+ useImperativeHandle(forwardedRef, () => ({
405
+ focus: () => {
406
+ ref.current?.focus();
407
+ },
408
+ setValue: (value: string) => {
409
+ handleChange(value);
410
+ },
411
+ selectAllInputText: () => {
412
+ ref.current?.setSelectionRange(0, ref.current.value.length);
413
+ },
414
+ }));
415
+
382
416
  return (
383
417
  <InputField.Root
384
418
  size={size}
@@ -434,6 +468,9 @@ export const InputFieldWithCompletions = memo(
434
468
  aria-expanded={isFocused && display !== "none"}
435
469
  aria-controls="component-listbox"
436
470
  />
471
+ {filter && typeaheadValue && typeaheadValue.startsWith(filter) && (
472
+ <InputField.Typeahead value={typeaheadValue} />
473
+ )}
437
474
  {(!isFocused || !hideChildrenWhenFocused) && children}
438
475
  {loading && isFocused && (
439
476
  <InputField.Label>
@@ -102,7 +102,7 @@ export const LabeledRow = memo(function LabeledRow({
102
102
  }: {
103
103
  id?: string;
104
104
  children: ReactNode;
105
- label: string;
105
+ label: ReactNode;
106
106
  labelTextStyle?: "small" | "heading5" | "heading4" | "heading3";
107
107
  gap?: CSSProperties["gap"];
108
108
  right?: ReactNode;
@@ -31,7 +31,7 @@ import { MenuItem } from "./internal/Menu";
31
31
  import { ScrollArea } from "./ScrollArea";
32
32
  import {
33
33
  DropValidator,
34
- normalizeListIndex,
34
+ normalizeListDestinationIndex,
35
35
  RelativeDropPosition,
36
36
  Sortable,
37
37
  } from "./Sortable";
@@ -47,6 +47,9 @@ type ListColorScheme = "primary" | "secondary";
47
47
 
48
48
  type PressEventName = "onClick" | "onPointerDown";
49
49
 
50
+ // TODO: Some of these could be passed in a single context value that's
51
+ // the same for all rows. But doing this seems to cause an infinite loop and
52
+ // prevents drag and drop from working.
50
53
  type ListRowContextValue = {
51
54
  marginType: ListRowMarginType;
52
55
  selectedPosition: ListRowPosition;
@@ -56,12 +59,23 @@ type ListRowContextValue = {
56
59
  gap: number;
57
60
  variant: ListViewVariant;
58
61
  sectionHeaderVariant: ListViewSectionHeaderVariant;
59
- indentation: number;
60
62
  pressEventName: PressEventName;
61
63
  isSectionHeader: boolean;
62
64
  colorScheme: ListColorScheme;
63
65
  };
64
66
 
67
+ // The dragged item isn't passed a context value (doing so causes an infinite loop),
68
+ // so we pass this separately. Rows need the indentation to look correct when dragging,
69
+ // and the dragged row doesn't have a ListRowContextValue.
70
+ type DraggingContextValue = {
71
+ indentation: number;
72
+ isDragging?: boolean;
73
+ };
74
+
75
+ const DraggingContext = createContext<DraggingContextValue>({
76
+ indentation: 12,
77
+ });
78
+
65
79
  const ListRowContext = createContext<ListRowContextValue>({
66
80
  marginType: "none",
67
81
  selectedPosition: "only",
@@ -71,7 +85,6 @@ const ListRowContext = createContext<ListRowContextValue>({
71
85
  gap: 0,
72
86
  variant: "normal",
73
87
  sectionHeaderVariant: "normal",
74
- indentation: 12,
75
88
  pressEventName: "onClick",
76
89
  isSectionHeader: false,
77
90
  colorScheme: "primary",
@@ -305,7 +318,7 @@ interface ListViewRowProps<MenuItemType extends string = string> {
305
318
  selected?: boolean;
306
319
  depth?: number;
307
320
  disabled?: boolean;
308
- draggable?: boolean;
321
+ // draggable?: boolean;
309
322
  hovered?: boolean;
310
323
  sortable?: boolean;
311
324
  gap?: number;
@@ -348,11 +361,12 @@ const ListViewRow = forwardRef(function ListViewRow<
348
361
  }: ListViewRowProps<MenuItemType>,
349
362
  forwardedRef: ForwardedRef<HTMLElement>
350
363
  ) {
364
+ depth = Math.max(0, depth);
365
+
351
366
  const {
352
367
  marginType,
353
368
  selectedPosition,
354
369
  sortable,
355
- indentation,
356
370
  pressEventName,
357
371
  variant,
358
372
  sectionHeaderVariant,
@@ -363,6 +377,7 @@ const ListViewRow = forwardRef(function ListViewRow<
363
377
  const { hoverProps } = useHover({
364
378
  onHoverChange,
365
379
  });
380
+ const { indentation, isDragging } = useContext(DraggingContext);
366
381
 
367
382
  const handlePress = useCallback(
368
383
  (event: React.MouseEvent) => {
@@ -387,6 +402,16 @@ const ListViewRow = forwardRef(function ListViewRow<
387
402
  [onDoubleClick]
388
403
  );
389
404
 
405
+ const isDraggingStyle = useMemo(() => {
406
+ return isDragging
407
+ ? {
408
+ // TODO: Where do these offsets actually come from?
409
+ transform: `translate3d(-36px, -6px, 0)`,
410
+ width: `calc(100% + 48px)`,
411
+ }
412
+ : undefined;
413
+ }, [isDragging]);
414
+
390
415
  const renderContent = (
391
416
  {
392
417
  relativeDropPosition,
@@ -416,8 +441,9 @@ const ListViewRow = forwardRef(function ListViewRow<
416
441
  selectedPosition={selectedPosition}
417
442
  showsActiveState={pressEventName === "onClick"}
418
443
  aria-selected={selected}
419
- divider={divider}
444
+ divider={!isDragging && divider}
420
445
  onKeyDown={onKeyDown}
446
+ style={isDraggingStyle}
421
447
  {...renderProps}
422
448
  {...mergeEventHandlers(
423
449
  { onPointerDown: renderProps.onPointerDown },
@@ -702,11 +728,6 @@ const ListViewRootInner = forwardRef(function ListViewRootInner<T>(
702
728
  [data, renderItem]
703
729
  );
704
730
 
705
- const renderOverlay = useCallback(
706
- (index: number) => renderItem(data[index], index, { isDragging: true }),
707
- [renderItem, data]
708
- );
709
-
710
731
  const getItemContextValue = useCallback(
711
732
  (i: number): ListRowContextValue | undefined => {
712
733
  const current = renderChild(i);
@@ -765,7 +786,6 @@ const ListViewRootInner = forwardRef(function ListViewRootInner<T>(
765
786
  sortable,
766
787
  expandable,
767
788
  divider,
768
- indentation,
769
789
  pressEventName,
770
790
  variant,
771
791
  sectionHeaderVariant,
@@ -780,7 +800,6 @@ const ListViewRootInner = forwardRef(function ListViewRootInner<T>(
780
800
  sortable,
781
801
  expandable,
782
802
  divider,
783
- indentation,
784
803
  pressEventName,
785
804
  variant,
786
805
  sectionHeaderVariant,
@@ -788,6 +807,22 @@ const ListViewRootInner = forwardRef(function ListViewRootInner<T>(
788
807
  ]
789
808
  );
790
809
 
810
+ const draggingContextOverlayValue = useMemo(
811
+ () => ({ indentation, isDragging: true }),
812
+ [indentation]
813
+ );
814
+
815
+ const renderOverlay = useCallback(
816
+ (index: number) => (
817
+ <div style={{ opacity: 0.25 }}>
818
+ <DraggingContext.Provider value={draggingContextOverlayValue}>
819
+ {renderItem(data[index], index, { isDragging: true })}
820
+ </DraggingContext.Provider>
821
+ </div>
822
+ ),
823
+ [draggingContextOverlayValue, renderItem, data]
824
+ );
825
+
791
826
  const renderWrappedChild = useCallback(
792
827
  (index: number) => {
793
828
  const contextValue = getItemContextValue(index);
@@ -845,32 +880,36 @@ const ListViewRootInner = forwardRef(function ListViewRootInner<T>(
845
880
  [data, keyExtractor]
846
881
  );
847
882
 
883
+ const draggingContextValue = useMemo(() => ({ indentation }), [indentation]);
884
+
848
885
  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)
886
+ <DraggingContext.Provider value={draggingContextValue}>
887
+ <RootContainer
888
+ {...{
889
+ [pressEventName]: handleClick,
890
+ }}
891
+ gap={gap}
892
+ scrollable={scrollable}
893
+ >
894
+ {withScrollable((scrollElementRef: HTMLDivElement | null) =>
895
+ withSortable(
896
+ virtualized ? (
897
+ <VirtualizedList<T>
898
+ ref={forwardedRef}
899
+ scrollElement={scrollElementRef!}
900
+ items={data}
901
+ size={virtualized}
902
+ getItemHeight={getItemHeight}
903
+ keyExtractor={getKey}
904
+ renderItem={renderWrappedChild}
905
+ />
906
+ ) : (
907
+ range(0, data.length).map(renderWrappedChild)
908
+ )
870
909
  )
871
- )
872
- )}
873
- </RootContainer>
910
+ )}
911
+ </RootContainer>
912
+ </DraggingContext.Provider>
874
913
  );
875
914
  });
876
915
 
@@ -946,5 +985,5 @@ export namespace ListView {
946
985
  (headerVariant === "label" ? sectionHeaderLabelHeight : rowHeight)
947
986
  );
948
987
  };
949
- export const normalizeIndex = normalizeListIndex;
988
+ export const normalizeDestinationIndex = normalizeListDestinationIndex;
950
989
  }
@@ -56,6 +56,7 @@ const SelectElement = styled.select<{ flex: CSSProperties["flex"] }>(
56
56
  appearance: "none",
57
57
  ...theme.textStyles.small,
58
58
  color: theme.colors.text,
59
+ lineHeight: "19px",
59
60
  width: "0px", // Reset intrinsic width
60
61
  flex: flex ?? "1 1 0px",
61
62
  position: "relative",
@@ -144,6 +145,7 @@ export const Select = memo(function Select<T extends string>({
144
145
  listeners.current.get(event.target.value)?.(),
145
146
  [listeners]
146
147
  )}
148
+ onPointerDown={(event) => event.stopPropagation()}
147
149
  >
148
150
  {optionElements}
149
151
  </SelectElement>
@@ -9,12 +9,12 @@ import {
9
9
  Translate,
10
10
  useSensor,
11
11
  useSensors,
12
- } from '@dnd-kit/core';
12
+ } from "@dnd-kit/core";
13
13
  import {
14
14
  SortableContext,
15
15
  useSortable,
16
16
  verticalListSortingStrategy,
17
- } from '@dnd-kit/sortable';
17
+ } from "@dnd-kit/sortable";
18
18
  import React, {
19
19
  createContext,
20
20
  memo,
@@ -25,32 +25,32 @@ import React, {
25
25
  useMemo,
26
26
  useRef,
27
27
  useState,
28
- } from 'react';
29
- import { createPortal } from 'react-dom';
28
+ } from "react";
29
+ import { createPortal } from "react-dom";
30
30
 
31
- export type RelativeDropPosition = 'above' | 'below' | 'inside';
31
+ export type RelativeDropPosition = "above" | "below" | "inside";
32
32
 
33
33
  export type DropValidator = (
34
34
  sourceIndex: number,
35
35
  destinationIndex: number,
36
- position: RelativeDropPosition,
36
+ position: RelativeDropPosition
37
37
  ) => boolean;
38
38
 
39
- export const normalizeListIndex = (
39
+ export const normalizeListDestinationIndex = (
40
40
  index: number,
41
- position: 'above' | 'below',
41
+ position: "above" | "below"
42
42
  ): number => {
43
- return position === 'above' ? index : index + 1;
43
+ return position === "above" ? index : index + 1;
44
44
  };
45
45
 
46
46
  const defaultAcceptsDrop: DropValidator = (
47
47
  sourceIndex,
48
48
  destinationIndex,
49
- position,
49
+ position
50
50
  ) => {
51
- if (position === 'inside') return false;
51
+ if (position === "inside") return false;
52
52
 
53
- const normalized = normalizeListIndex(destinationIndex, position);
53
+ const normalized = normalizeListDestinationIndex(destinationIndex, position);
54
54
 
55
55
  if (sourceIndex === normalized || sourceIndex + 1 === normalized) {
56
56
  return false;
@@ -64,11 +64,13 @@ const SortableItemContext = createContext<{
64
64
  position: Translate;
65
65
  acceptsDrop: DropValidator;
66
66
  setActivatorEvent: (event: PointerEvent) => void;
67
+ axis: "x" | "y";
67
68
  }>({
68
69
  keys: [],
69
70
  position: { x: 0, y: 0 },
70
71
  acceptsDrop: defaultAcceptsDrop,
71
72
  setActivatorEvent: () => {},
73
+ axis: "y",
72
74
  });
73
75
 
74
76
  function validateDropIndicator(
@@ -76,33 +78,33 @@ function validateDropIndicator(
76
78
  keys: string[],
77
79
  activeId: string,
78
80
  overId: string,
79
- offsetTop: number,
80
- elementTop: number,
81
- elementHeight: number,
81
+ offsetStart: number,
82
+ elementStart: number,
83
+ elementSize: number
82
84
  ): RelativeDropPosition | undefined {
83
85
  const activeIndex = keys.indexOf(activeId);
84
86
  const overIndex = keys.indexOf(overId);
85
87
 
86
- const acceptsDropInside = acceptsDrop(activeIndex, overIndex, 'inside');
88
+ const acceptsDropInside = acceptsDrop(activeIndex, overIndex, "inside");
87
89
 
88
90
  // If we're in the center of the element, prefer dropping inside
89
91
  if (
90
- offsetTop >= elementTop + elementHeight / 3 &&
91
- offsetTop <= elementTop + (elementHeight * 2) / 3 &&
92
+ offsetStart >= elementStart + elementSize / 3 &&
93
+ offsetStart <= elementStart + (elementSize * 2) / 3 &&
92
94
  acceptsDropInside
93
95
  )
94
- return 'inside';
96
+ return "inside";
95
97
 
96
98
  // Are we over the top or bottom half of the element?
97
99
  const indicator =
98
- offsetTop < elementTop + elementHeight / 2 ? 'above' : 'below';
100
+ offsetStart < elementStart + elementSize / 2 ? "above" : "below";
99
101
 
100
102
  // Drop above or below if possible, falling back to inside
101
103
  return acceptsDrop(activeIndex, overIndex, indicator)
102
104
  ? indicator
103
105
  : acceptsDropInside
104
- ? 'inside'
105
- : undefined;
106
+ ? "inside"
107
+ : undefined;
106
108
  }
107
109
 
108
110
  /* ----------------------------------------------------------------------------
@@ -119,7 +121,7 @@ interface ItemProps<T> {
119
121
  ref: Ref<T>;
120
122
  relativeDropPosition?: RelativeDropPosition;
121
123
  [key: string]: any;
122
- } & UseSortableReturnType['attributes'],
124
+ } & UseSortableReturnType["attributes"]
123
125
  ) => JSX.Element;
124
126
  }
125
127
 
@@ -128,7 +130,7 @@ function SortableItem<T extends HTMLElement>({
128
130
  disabled,
129
131
  children,
130
132
  }: ItemProps<T>) {
131
- const { keys, position, acceptsDrop, setActivatorEvent } =
133
+ const { keys, position, acceptsDrop, setActivatorEvent, axis } =
132
134
  useContext(SortableItemContext);
133
135
  const sortable = useSortable({ id, disabled });
134
136
 
@@ -148,7 +150,9 @@ function SortableItem<T extends HTMLElement>({
148
150
  setActivatorEvent(activatorEvent as PointerEvent);
149
151
  }
150
152
 
153
+ const eventX = (activatorEvent as PointerEvent | null)?.clientX ?? 0;
151
154
  const eventY = (activatorEvent as PointerEvent | null)?.clientY ?? 0;
155
+ const offsetLeft = eventX + position.x;
152
156
  const offsetTop = eventY + position.y;
153
157
 
154
158
  const ref = useCallback((node: T) => setNodeRef(node), [setNodeRef]);
@@ -164,9 +168,9 @@ function SortableItem<T extends HTMLElement>({
164
168
  keys,
165
169
  active.id,
166
170
  over.id,
167
- offsetTop,
168
- over.rect.offsetTop,
169
- over.rect.height,
171
+ axis === "x" ? offsetLeft : offsetTop,
172
+ axis === "x" ? over.rect.offsetLeft : over.rect.offsetTop,
173
+ axis === "x" ? over.rect.width : over.rect.height
170
174
  )
171
175
  : undefined,
172
176
  });
@@ -183,9 +187,10 @@ interface RootProps {
183
187
  onMoveItem?: (
184
188
  sourceIndex: number,
185
189
  destinationIndex: number,
186
- position: RelativeDropPosition,
190
+ position: RelativeDropPosition
187
191
  ) => void;
188
192
  acceptsDrop?: DropValidator;
193
+ axis?: "x" | "y";
189
194
  }
190
195
 
191
196
  function SortableRoot({
@@ -194,13 +199,14 @@ function SortableRoot({
194
199
  onMoveItem,
195
200
  renderOverlay,
196
201
  acceptsDrop = defaultAcceptsDrop,
202
+ axis = "y",
197
203
  }: RootProps) {
198
204
  const sensors = useSensors(
199
205
  useSensor(PointerSensor, {
200
206
  activationConstraint: {
201
207
  distance: 4,
202
208
  },
203
- }),
209
+ })
204
210
  );
205
211
 
206
212
  const [activeIndex, setActiveIndex] = useState<number | undefined>();
@@ -216,7 +222,7 @@ function SortableRoot({
216
222
  (event: DragStartEvent) => {
217
223
  setActiveIndex(keys.indexOf(event.active.id));
218
224
  },
219
- [keys],
225
+ [keys]
220
226
  );
221
227
 
222
228
  const handleDragMove = useCallback((event: DragMoveEvent) => {
@@ -233,7 +239,9 @@ function SortableRoot({
233
239
  const oldIndex = keys.indexOf(active.id);
234
240
  const newIndex = keys.indexOf(over.id);
235
241
 
242
+ const eventX = activatorEvent.current?.clientX ?? 0;
236
243
  const eventY = activatorEvent.current?.clientY ?? 0;
244
+ const offsetLeft = eventX + position.x;
237
245
  const offsetTop = eventY + position.y;
238
246
 
239
247
  const indicator = validateDropIndicator(
@@ -241,9 +249,9 @@ function SortableRoot({
241
249
  keys,
242
250
  active.id,
243
251
  over.id,
244
- offsetTop,
245
- over.rect.offsetTop,
246
- over.rect.height,
252
+ axis === "x" ? offsetLeft : offsetTop,
253
+ axis === "x" ? over.rect.offsetLeft : over.rect.offsetTop,
254
+ axis === "x" ? over.rect.width : over.rect.height
247
255
  );
248
256
 
249
257
  if (!indicator) return;
@@ -251,7 +259,7 @@ function SortableRoot({
251
259
  onMoveItem?.(oldIndex, newIndex, indicator);
252
260
  }
253
261
  },
254
- [acceptsDrop, keys, onMoveItem, position.y],
262
+ [acceptsDrop, axis, keys, onMoveItem, position.x, position.y]
255
263
  );
256
264
 
257
265
  return (
@@ -262,8 +270,9 @@ function SortableRoot({
262
270
  acceptsDrop,
263
271
  position,
264
272
  setActivatorEvent,
273
+ axis,
265
274
  }),
266
- [acceptsDrop, keys, position, setActivatorEvent],
275
+ [acceptsDrop, axis, keys, position, setActivatorEvent]
267
276
  )}
268
277
  >
269
278
  <DndContext
@@ -283,7 +292,7 @@ function SortableRoot({
283
292
  activeIndex >= 0 &&
284
293
  renderOverlay(activeIndex)}
285
294
  </DragOverlay>,
286
- document.body,
295
+ document.body
287
296
  )}
288
297
  </DndContext>
289
298
  </SortableItemContext.Provider>