@noya-app/noya-designsystem 0.1.47 → 0.1.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +10 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +205 -106
- package/dist/index.d.ts +205 -106
- package/dist/index.js +2108 -1589
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2078 -1566
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -7
- package/src/__tests__/validateDropIndicator.test.ts +263 -0
- package/src/components/ActionMenu.tsx +40 -0
- package/src/components/ActivityIndicator.tsx +7 -1
- package/src/components/Collection.tsx +12 -2
- package/src/components/Combobox.tsx +14 -1
- package/src/components/ComboboxMenu.tsx +14 -5
- package/src/components/Dialog.tsx +17 -12
- package/src/components/Drawer.tsx +98 -0
- package/src/components/Grid.tsx +57 -27
- package/src/components/GridView.tsx +39 -25
- package/src/components/InputField.tsx +87 -92
- package/src/components/Label.tsx +7 -7
- package/src/components/List.tsx +42 -16
- package/src/components/ListView.tsx +21 -17
- package/src/components/MediaThumbnail.tsx +117 -0
- package/src/components/SearchCompletionMenu.tsx +30 -16
- package/src/components/SelectMenu.tsx +19 -17
- package/src/components/Sortable.tsx +70 -44
- package/src/components/internal/Menu.tsx +38 -23
- package/src/components/internal/MenuViewport.tsx +7 -1
- package/src/components/internal/SelectItem.tsx +51 -27
- package/src/components/internal/__tests__/Menu.test.tsx +12 -0
- package/src/components/workspace/DrawerWorkspaceLayout.tsx +86 -0
- package/src/components/workspace/PanelWorkspaceLayout.tsx +102 -0
- package/src/components/{WorkspaceLayout.tsx → workspace/WorkspaceLayout.tsx} +109 -106
- package/src/components/workspace/types.ts +31 -0
- package/src/contexts/DialogContext.tsx +49 -34
- package/src/hooks/usePreservePanelSize.tsx +1 -5
- package/src/index.css +1 -0
- package/src/index.tsx +4 -1
- package/tailwind.config.ts +1 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DropdownChevronIcon } from "@noya-app/noya-icons";
|
|
2
|
-
import {
|
|
2
|
+
import { memoGeneric, useMergeRefs, useSize } from "@noya-app/react-utils";
|
|
3
3
|
import { Property } from "csstype";
|
|
4
4
|
import React, {
|
|
5
5
|
createContext,
|
|
@@ -11,11 +11,8 @@ import React, {
|
|
|
11
11
|
ReactNode,
|
|
12
12
|
useCallback,
|
|
13
13
|
useContext,
|
|
14
|
-
useEffect,
|
|
15
14
|
useId,
|
|
16
|
-
useLayoutEffect,
|
|
17
15
|
useMemo,
|
|
18
|
-
useRef,
|
|
19
16
|
useState,
|
|
20
17
|
} from "react";
|
|
21
18
|
import { useLabel, useLabelPosition } from "../hooks/useLabel";
|
|
@@ -31,27 +28,33 @@ import { Popover } from "./Popover";
|
|
|
31
28
|
export type InputFieldSize = "small" | "medium" | "large";
|
|
32
29
|
|
|
33
30
|
type InputFieldContextValue = {
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
labelWidth?: number;
|
|
32
|
+
buttonWidth?: number;
|
|
33
|
+
startWidth?: number;
|
|
36
34
|
/** @default medium */
|
|
37
35
|
size: InputFieldSize;
|
|
38
36
|
isFocused: boolean;
|
|
39
37
|
onFocusChange: (isFocused: boolean) => void;
|
|
40
|
-
inputRef
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
inputRef: ForwardedRef<HTMLInputElement>;
|
|
39
|
+
buttonRef: ForwardedRef<HTMLButtonElement>;
|
|
40
|
+
labelRef: ForwardedRef<HTMLLabelElement>;
|
|
41
|
+
startRef: ForwardedRef<HTMLSpanElement>;
|
|
44
42
|
id?: string;
|
|
45
43
|
label?: ReactNode;
|
|
46
44
|
labelPosition?: LabelPosition;
|
|
47
45
|
};
|
|
48
46
|
|
|
49
47
|
const InputFieldContext = createContext<InputFieldContextValue>({
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
labelWidth: undefined,
|
|
49
|
+
buttonWidth: undefined,
|
|
50
|
+
startWidth: undefined,
|
|
52
51
|
size: "medium",
|
|
53
52
|
isFocused: false,
|
|
54
53
|
onFocusChange: () => {},
|
|
54
|
+
inputRef: null,
|
|
55
|
+
buttonRef: null,
|
|
56
|
+
labelRef: null,
|
|
57
|
+
startRef: null,
|
|
55
58
|
});
|
|
56
59
|
|
|
57
60
|
export const useInputFieldContext = () => useContext(InputFieldContext);
|
|
@@ -59,16 +62,18 @@ export const useInputFieldContext = () => useContext(InputFieldContext);
|
|
|
59
62
|
const noop = () => {};
|
|
60
63
|
|
|
61
64
|
export const getFieldSpacing = ({
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
buttonWidth = 0,
|
|
66
|
+
labelWidth = 0,
|
|
67
|
+
startWidth,
|
|
64
68
|
variant,
|
|
65
69
|
}: {
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
buttonWidth?: number;
|
|
71
|
+
labelWidth?: number;
|
|
72
|
+
startWidth?: number;
|
|
68
73
|
variant?: InputFieldVariant;
|
|
69
74
|
}) => ({
|
|
70
|
-
paddingLeft: variant === "bare" ?
|
|
71
|
-
paddingRight: `${variant === "bare" ? 0 :
|
|
75
|
+
paddingLeft: `${variant === "bare" ? startWidth : startWidth ? startWidth + 12 : 6}px`,
|
|
76
|
+
paddingRight: `${variant === "bare" ? 0 : labelWidth + buttonWidth + (labelWidth || buttonWidth ? 12 : 6)}px`,
|
|
72
77
|
});
|
|
73
78
|
|
|
74
79
|
/* ----------------------------------------------------------------------------
|
|
@@ -83,31 +88,18 @@ const InputFieldLabel = memo(
|
|
|
83
88
|
props: LabelHTMLAttributes<HTMLLabelElement>,
|
|
84
89
|
forwardedRef: ForwardedRef<HTMLLabelElement>
|
|
85
90
|
) {
|
|
86
|
-
const {
|
|
91
|
+
const { labelRef, size, buttonWidth, label, id, labelPosition } =
|
|
87
92
|
useInputFieldContext();
|
|
88
93
|
|
|
89
|
-
const ref =
|
|
90
|
-
|
|
91
|
-
useLayoutEffect(() => {
|
|
92
|
-
if (!setLabelWidth) return;
|
|
93
|
-
|
|
94
|
-
const width = ref.current?.getBoundingClientRect().width;
|
|
95
|
-
|
|
96
|
-
if (!width) return;
|
|
97
|
-
|
|
98
|
-
setLabelWidth(width);
|
|
99
|
-
}, [setLabelWidth]);
|
|
94
|
+
const ref = useMergeRefs(labelRef, forwardedRef);
|
|
100
95
|
|
|
101
96
|
if (labelPosition !== "inset") return null;
|
|
102
97
|
|
|
103
98
|
return (
|
|
104
99
|
<InsetLabel
|
|
105
|
-
ref={
|
|
106
|
-
ref.current = element;
|
|
107
|
-
assignRef(forwardedRef, element);
|
|
108
|
-
}}
|
|
100
|
+
ref={ref}
|
|
109
101
|
size={size}
|
|
110
|
-
|
|
102
|
+
buttonWidth={buttonWidth}
|
|
111
103
|
htmlFor={id}
|
|
112
104
|
{...props}
|
|
113
105
|
>
|
|
@@ -170,6 +162,7 @@ const InputFieldButton = memo(
|
|
|
170
162
|
props: React.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
171
163
|
ButtonRootProps & {
|
|
172
164
|
buttonClassName?: string;
|
|
165
|
+
buttonStyle?: React.CSSProperties;
|
|
173
166
|
variant?: "floating" | "normal";
|
|
174
167
|
},
|
|
175
168
|
forwardedRef: ForwardedRef<HTMLButtonElement>
|
|
@@ -178,24 +171,16 @@ const InputFieldButton = memo(
|
|
|
178
171
|
children,
|
|
179
172
|
onClick,
|
|
180
173
|
className,
|
|
174
|
+
style,
|
|
181
175
|
buttonClassName,
|
|
176
|
+
buttonStyle,
|
|
182
177
|
variant = "normal",
|
|
183
178
|
as = "button",
|
|
184
179
|
...rest
|
|
185
180
|
} = props;
|
|
186
|
-
const { size, inputRef,
|
|
187
|
-
|
|
188
|
-
const ref = useRef<HTMLButtonElement | null>(null);
|
|
181
|
+
const { size, inputRef, buttonRef } = useInputFieldContext();
|
|
189
182
|
|
|
190
|
-
|
|
191
|
-
if (!setButtonWidth) return;
|
|
192
|
-
|
|
193
|
-
const width = ref.current?.getBoundingClientRect().width;
|
|
194
|
-
|
|
195
|
-
if (!width) return;
|
|
196
|
-
|
|
197
|
-
setButtonWidth(width);
|
|
198
|
-
}, [setButtonWidth]);
|
|
183
|
+
const ref = useMergeRefs(buttonRef, forwardedRef);
|
|
199
184
|
|
|
200
185
|
const defaultHandleClick = useCallback(
|
|
201
186
|
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
@@ -221,6 +206,7 @@ const InputFieldButton = memo(
|
|
|
221
206
|
|
|
222
207
|
return (
|
|
223
208
|
<span
|
|
209
|
+
style={style}
|
|
224
210
|
className={cx(
|
|
225
211
|
"absolute",
|
|
226
212
|
size === "large"
|
|
@@ -237,10 +223,7 @@ const InputFieldButton = memo(
|
|
|
237
223
|
>
|
|
238
224
|
<Button
|
|
239
225
|
as={as}
|
|
240
|
-
ref={
|
|
241
|
-
ref.current = element;
|
|
242
|
-
assignRef(forwardedRef, element);
|
|
243
|
-
}}
|
|
226
|
+
ref={ref}
|
|
244
227
|
variant="floating"
|
|
245
228
|
onClick={onClick ?? defaultHandleClick}
|
|
246
229
|
onPointerDown={handlePointerDown}
|
|
@@ -250,6 +233,7 @@ const InputFieldButton = memo(
|
|
|
250
233
|
buttonClassName,
|
|
251
234
|
variant === "floating" && "bg-transparent shadow-none"
|
|
252
235
|
)}
|
|
236
|
+
style={buttonStyle}
|
|
253
237
|
{...rest}
|
|
254
238
|
>
|
|
255
239
|
{children}
|
|
@@ -300,14 +284,18 @@ const InputElement = forwardRef(
|
|
|
300
284
|
style,
|
|
301
285
|
...props
|
|
302
286
|
}: InputElementProps,
|
|
303
|
-
|
|
287
|
+
forwardedRef: ForwardedRef<any>
|
|
304
288
|
) => {
|
|
305
|
-
const {
|
|
289
|
+
const { buttonWidth, inputRef, labelWidth, size, id, startWidth } =
|
|
290
|
+
useInputFieldContext();
|
|
291
|
+
|
|
292
|
+
const ref = useMergeRefs(inputRef, forwardedRef);
|
|
306
293
|
|
|
307
294
|
const spacingStyles = getFieldSpacing({
|
|
308
|
-
|
|
309
|
-
|
|
295
|
+
buttonWidth,
|
|
296
|
+
labelWidth,
|
|
310
297
|
variant: $variant,
|
|
298
|
+
startWidth,
|
|
311
299
|
});
|
|
312
300
|
|
|
313
301
|
const inputClassName = cx(
|
|
@@ -367,7 +355,9 @@ const InputFieldInput = forwardRef(function InputFieldInput(
|
|
|
367
355
|
{ textAlign, variant, ...rest }: InputFieldInputProps,
|
|
368
356
|
forwardedRef: ForwardedRef<HTMLInputElement>
|
|
369
357
|
) {
|
|
370
|
-
const { onFocusChange,
|
|
358
|
+
const { onFocusChange, inputRef } = useInputFieldContext();
|
|
359
|
+
|
|
360
|
+
const ref = useMergeRefs(inputRef, forwardedRef);
|
|
371
361
|
|
|
372
362
|
const handleFocusChange = useCallback(
|
|
373
363
|
(isFocused: boolean) => {
|
|
@@ -376,13 +366,9 @@ const InputFieldInput = forwardRef(function InputFieldInput(
|
|
|
376
366
|
[onFocusChange]
|
|
377
367
|
);
|
|
378
368
|
|
|
379
|
-
useLayoutEffect(() => {
|
|
380
|
-
setInputRef?.(forwardedRef);
|
|
381
|
-
}, [forwardedRef, setInputRef]);
|
|
382
|
-
|
|
383
369
|
return (
|
|
384
370
|
<InputElement
|
|
385
|
-
ref={
|
|
371
|
+
ref={ref}
|
|
386
372
|
$variant={variant}
|
|
387
373
|
$textAlign={textAlign}
|
|
388
374
|
onFocusChange={handleFocusChange}
|
|
@@ -563,11 +549,14 @@ const RootContainer = forwardRef<
|
|
|
563
549
|
/>
|
|
564
550
|
));
|
|
565
551
|
|
|
552
|
+
const startStyles = "absolute left-1.5 inset-y-0 flex items-center";
|
|
553
|
+
|
|
566
554
|
interface InputFieldRootProps {
|
|
567
555
|
id?: string;
|
|
568
556
|
children?: ReactNode;
|
|
569
557
|
width?: number;
|
|
570
|
-
|
|
558
|
+
labelWidth?: number;
|
|
559
|
+
startWidth?: number;
|
|
571
560
|
/** @default medium */
|
|
572
561
|
size?: InputFieldSize;
|
|
573
562
|
renderPopoverContent?: (options: { width: number }) => ReactNode;
|
|
@@ -579,6 +568,7 @@ interface InputFieldRootProps {
|
|
|
579
568
|
onFocusChange?: (isFocused: boolean) => void;
|
|
580
569
|
style?: React.CSSProperties;
|
|
581
570
|
className?: string;
|
|
571
|
+
start?: ReactNode;
|
|
582
572
|
}
|
|
583
573
|
|
|
584
574
|
function InputFieldRoot({
|
|
@@ -586,12 +576,14 @@ function InputFieldRoot({
|
|
|
586
576
|
children,
|
|
587
577
|
width,
|
|
588
578
|
sideOffset = 3,
|
|
589
|
-
|
|
579
|
+
labelWidth,
|
|
580
|
+
startWidth,
|
|
590
581
|
size = "medium",
|
|
591
582
|
renderPopoverContent,
|
|
592
583
|
onFocusChange,
|
|
593
584
|
style,
|
|
594
585
|
className,
|
|
586
|
+
start,
|
|
595
587
|
}: InputFieldRootProps) {
|
|
596
588
|
const { fieldId: id, label } = useLabel({ fieldId: idProp });
|
|
597
589
|
const randomId = useId();
|
|
@@ -599,9 +591,15 @@ function InputFieldRoot({
|
|
|
599
591
|
|
|
600
592
|
const [isFocused, setIsFocused] = React.useState(false);
|
|
601
593
|
|
|
602
|
-
const
|
|
603
|
-
const
|
|
604
|
-
const
|
|
594
|
+
const startRef = React.useRef<HTMLSpanElement>(null);
|
|
595
|
+
const labelRef = React.useRef<HTMLLabelElement>(null);
|
|
596
|
+
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
|
597
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
598
|
+
|
|
599
|
+
const measuredLabelSize = useSize(labelRef, "width");
|
|
600
|
+
const measuredButtonSize = useSize(buttonRef, "width");
|
|
601
|
+
const measuredInputSize = useSize(inputRef, "width");
|
|
602
|
+
const measuredStartSize = useSize(startRef, "width");
|
|
605
603
|
|
|
606
604
|
const handleFocusChange = useCallback(
|
|
607
605
|
(isFocused: boolean) => {
|
|
@@ -611,36 +609,25 @@ function InputFieldRoot({
|
|
|
611
609
|
[onFocusChange]
|
|
612
610
|
);
|
|
613
611
|
|
|
614
|
-
const [inputRef, setInputRef] =
|
|
615
|
-
React.useState<ForwardedRef<HTMLInputElement>>();
|
|
616
|
-
|
|
617
|
-
// eslint-disable-next-line @shopify/prefer-early-return
|
|
618
|
-
useEffect(() => {
|
|
619
|
-
if (inputRef && typeof inputRef !== "function") {
|
|
620
|
-
setMeasuredWidth?.(
|
|
621
|
-
isFocused ? inputRef?.current?.getBoundingClientRect().width : undefined
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
}, [inputRef, isFocused]);
|
|
625
|
-
|
|
626
612
|
const contextValue = useMemo(
|
|
627
613
|
(): InputFieldContextValue => ({
|
|
628
|
-
|
|
629
|
-
|
|
614
|
+
labelWidth: measuredLabelSize?.width ?? labelWidth,
|
|
615
|
+
buttonWidth: measuredButtonSize?.width,
|
|
616
|
+
startWidth: measuredStartSize?.width ?? startWidth,
|
|
617
|
+
inputRef,
|
|
618
|
+
buttonRef,
|
|
619
|
+
labelRef,
|
|
620
|
+
startRef,
|
|
630
621
|
size,
|
|
631
622
|
isFocused,
|
|
632
623
|
onFocusChange: handleFocusChange,
|
|
633
|
-
inputRef,
|
|
634
|
-
setInputRef,
|
|
635
|
-
setLabelWidth: setMeasuredLabelSize,
|
|
636
|
-
setButtonWidth: setMeasuredButtonSize,
|
|
637
624
|
id: id ?? randomId,
|
|
638
625
|
label,
|
|
639
626
|
labelPosition,
|
|
640
627
|
}),
|
|
641
628
|
[
|
|
642
629
|
measuredLabelSize,
|
|
643
|
-
|
|
630
|
+
labelWidth,
|
|
644
631
|
measuredButtonSize,
|
|
645
632
|
size,
|
|
646
633
|
isFocused,
|
|
@@ -650,19 +637,27 @@ function InputFieldRoot({
|
|
|
650
637
|
randomId,
|
|
651
638
|
label,
|
|
652
639
|
labelPosition,
|
|
640
|
+
measuredStartSize,
|
|
641
|
+
startWidth,
|
|
653
642
|
]
|
|
654
643
|
);
|
|
655
644
|
|
|
656
645
|
const rootElement = (
|
|
657
646
|
<RootContainer $width={width} style={style} className={className}>
|
|
658
647
|
{children}
|
|
648
|
+
{start && (
|
|
649
|
+
<span ref={startRef} className={startStyles}>
|
|
650
|
+
{start}
|
|
651
|
+
</span>
|
|
652
|
+
)}
|
|
659
653
|
{label && <InputFieldLabel />}
|
|
660
654
|
</RootContainer>
|
|
661
655
|
);
|
|
662
656
|
|
|
663
657
|
const measuredWidthObject = useMemo(
|
|
664
|
-
() =>
|
|
665
|
-
|
|
658
|
+
() =>
|
|
659
|
+
measuredInputSize ? { width: measuredInputSize.width + 4 } : undefined,
|
|
660
|
+
[measuredInputSize]
|
|
666
661
|
);
|
|
667
662
|
|
|
668
663
|
return (
|
|
@@ -711,11 +706,11 @@ const PrimitiveInputField = ({
|
|
|
711
706
|
React.InputHTMLAttributes<HTMLInputElement>,
|
|
712
707
|
HTMLInputElement
|
|
713
708
|
>) => {
|
|
714
|
-
const { id,
|
|
709
|
+
const { id, labelWidth, buttonWidth } = useInputFieldContext();
|
|
715
710
|
|
|
716
711
|
const spacingStyles = getFieldSpacing({
|
|
717
|
-
|
|
718
|
-
|
|
712
|
+
buttonWidth,
|
|
713
|
+
labelWidth,
|
|
719
714
|
});
|
|
720
715
|
const memoizedStyles = useMemo(
|
|
721
716
|
() => ({ ...spacingStyles, ...style }),
|
|
@@ -759,7 +754,7 @@ const InputFieldComponent = forwardRef<HTMLInputElement, InputFieldProps>(
|
|
|
759
754
|
function InputField({ children, ...props }: InputFieldProps, ref) {
|
|
760
755
|
const {
|
|
761
756
|
width,
|
|
762
|
-
|
|
757
|
+
labelWidth,
|
|
763
758
|
size,
|
|
764
759
|
renderPopoverContent,
|
|
765
760
|
sideOffset,
|
|
@@ -772,7 +767,7 @@ const InputFieldComponent = forwardRef<HTMLInputElement, InputFieldProps>(
|
|
|
772
767
|
return (
|
|
773
768
|
<InputFieldSubcomponents.Root
|
|
774
769
|
width={width}
|
|
775
|
-
|
|
770
|
+
labelWidth={labelWidth}
|
|
776
771
|
size={size}
|
|
777
772
|
renderPopoverContent={renderPopoverContent}
|
|
778
773
|
sideOffset={sideOffset}
|
package/src/components/Label.tsx
CHANGED
|
@@ -19,7 +19,7 @@ export type InsetLabelProps = {
|
|
|
19
19
|
/** @default medium */
|
|
20
20
|
size?: InputFieldSize;
|
|
21
21
|
/** when used with an InputField.Root and label position is inset, it will add padding to the right of the label to account for the button */
|
|
22
|
-
|
|
22
|
+
buttonWidth?: number;
|
|
23
23
|
} & LabelHTMLAttributes<HTMLLabelElement>;
|
|
24
24
|
|
|
25
25
|
// apply to <label
|
|
@@ -28,26 +28,26 @@ export const insetLabelTextStyles = `font-sans text-label uppercase text-text-di
|
|
|
28
28
|
// InsetLabelContainer
|
|
29
29
|
export const insetLabelStyles = `flex items-center absolute top-0 bottom-0 z-label`;
|
|
30
30
|
const getInsetLabelPosition = ({
|
|
31
|
-
|
|
31
|
+
buttonWidth,
|
|
32
32
|
}: {
|
|
33
|
-
|
|
33
|
+
buttonWidth?: number;
|
|
34
34
|
}): CSSProperties => ({
|
|
35
|
-
right: `${6 + (
|
|
35
|
+
right: `${6 + (buttonWidth ?? 0)}px`,
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
export const InsetLabel = memo(
|
|
39
39
|
forwardRef<HTMLLabelElement, InsetLabelProps>(function InsetLabel(
|
|
40
|
-
{ children,
|
|
40
|
+
{ children, buttonWidth, size = "medium", className, style, ...props },
|
|
41
41
|
ref
|
|
42
42
|
) {
|
|
43
43
|
const styles = useMemo(
|
|
44
44
|
() => ({
|
|
45
45
|
...getInsetLabelPosition({
|
|
46
|
-
|
|
46
|
+
buttonWidth,
|
|
47
47
|
}),
|
|
48
48
|
...style,
|
|
49
49
|
}),
|
|
50
|
-
[
|
|
50
|
+
[buttonWidth, style]
|
|
51
51
|
);
|
|
52
52
|
|
|
53
53
|
return (
|
package/src/components/List.tsx
CHANGED
|
@@ -7,10 +7,10 @@ import React, { useImperativeHandle, useRef, useState } from "react";
|
|
|
7
7
|
import { cssVars } from "../theme";
|
|
8
8
|
import { cx } from "../utils/classNames";
|
|
9
9
|
import { updateSelection } from "../utils/selection";
|
|
10
|
+
import { ActionMenu } from "./ActionMenu";
|
|
10
11
|
import { CollectionProps, CollectionRef } from "./Collection";
|
|
11
12
|
import { ListView } from "./ListView";
|
|
12
13
|
import { TreeView } from "./TreeView";
|
|
13
|
-
|
|
14
14
|
const cssVarOverrides = {
|
|
15
15
|
"--icon-selected": cssVars.colors.primary,
|
|
16
16
|
};
|
|
@@ -19,7 +19,7 @@ const emptyArray: string[] = [];
|
|
|
19
19
|
|
|
20
20
|
export const List = memoGeneric(
|
|
21
21
|
forwardRefGeneric(function List<T, M extends string = string>(
|
|
22
|
-
props: CollectionProps<T, M
|
|
22
|
+
props: CollectionProps<T, M>,
|
|
23
23
|
ref: React.ForwardedRef<CollectionRef>
|
|
24
24
|
) {
|
|
25
25
|
const {
|
|
@@ -51,11 +51,12 @@ export const List = memoGeneric(
|
|
|
51
51
|
onDoubleClickItem,
|
|
52
52
|
renamable,
|
|
53
53
|
selectedIds = emptyArray,
|
|
54
|
+
renderEmptyState,
|
|
54
55
|
} = props;
|
|
55
56
|
|
|
56
57
|
const [internalHoveredId, setHoveredId] = useState<string>();
|
|
57
58
|
const [internalRenamingId, setRenamingId] = useState<string>();
|
|
58
|
-
|
|
59
|
+
const [dropdownOpenId, setDropdownOpenId] = useState<string>();
|
|
59
60
|
const hoveredId = internalHoveredId ?? testHoveredId;
|
|
60
61
|
const renamingId = testRenamingId ?? internalRenamingId;
|
|
61
62
|
|
|
@@ -72,13 +73,17 @@ export const List = memoGeneric(
|
|
|
72
73
|
onSelectionChange(newSelectedIds, event);
|
|
73
74
|
};
|
|
74
75
|
|
|
75
|
-
const handleMenuAction = (action: M) => {
|
|
76
|
+
const handleMenuAction = (action: M, actionItem: T) => {
|
|
76
77
|
if (!onSelectMenuItem) return;
|
|
77
78
|
|
|
78
79
|
const selectedItems = items.filter((item) =>
|
|
79
80
|
selectedIds.includes(getId(item))
|
|
80
81
|
);
|
|
81
|
-
|
|
82
|
+
|
|
83
|
+
onSelectMenuItem(
|
|
84
|
+
action,
|
|
85
|
+
selectedItems.length === 0 ? [actionItem] : selectedItems
|
|
86
|
+
);
|
|
82
87
|
};
|
|
83
88
|
|
|
84
89
|
const ViewComponent = expandable ? TreeView : ListView;
|
|
@@ -98,7 +103,11 @@ export const List = memoGeneric(
|
|
|
98
103
|
return (
|
|
99
104
|
<ViewComponent.Root
|
|
100
105
|
variant="bare"
|
|
101
|
-
className={cx(
|
|
106
|
+
className={cx(
|
|
107
|
+
className,
|
|
108
|
+
isDropTargetActive && "bg-primary-pastel",
|
|
109
|
+
scrollable && "basis-0 min-h-0"
|
|
110
|
+
)}
|
|
102
111
|
containerRef={fileDropTargetRef}
|
|
103
112
|
scrollable={scrollable}
|
|
104
113
|
data={items}
|
|
@@ -110,6 +119,7 @@ export const List = memoGeneric(
|
|
|
110
119
|
sortable={expandable}
|
|
111
120
|
acceptsDrop={acceptsDrop}
|
|
112
121
|
onMoveItem={onMoveItem}
|
|
122
|
+
renderEmptyState={renderEmptyState}
|
|
113
123
|
{...dropTargetProps}
|
|
114
124
|
renderItem={(
|
|
115
125
|
item: T,
|
|
@@ -117,18 +127,39 @@ export const List = memoGeneric(
|
|
|
117
127
|
{ isDragging }: { isDragging: boolean }
|
|
118
128
|
) => {
|
|
119
129
|
const id = getId(item);
|
|
120
|
-
const
|
|
130
|
+
const isDropdownOpen = dropdownOpenId === id;
|
|
131
|
+
const isHovered = (hoveredId === id || isDropdownOpen) && !isDragging;
|
|
121
132
|
const expanded = getExpanded?.(item);
|
|
122
133
|
const itemIsRenamable = getRenamable?.(item) ?? renamable;
|
|
123
134
|
const isSelected = selectedIds.includes(id);
|
|
124
135
|
const isRenaming = itemIsRenamable && onRename && renamingId === id;
|
|
125
136
|
const isTestingRenaming = testRenamingId === id;
|
|
126
137
|
|
|
127
|
-
const thumbnail = renderThumbnail?.(
|
|
138
|
+
const thumbnail = renderThumbnail?.({
|
|
139
|
+
item,
|
|
140
|
+
selected: isSelected,
|
|
141
|
+
});
|
|
128
142
|
const action =
|
|
129
143
|
(isHovered || isSelected) &&
|
|
130
144
|
!isRenaming &&
|
|
131
|
-
|
|
145
|
+
(typeof renderAction === "function"
|
|
146
|
+
? renderAction(item, isSelected)
|
|
147
|
+
: renderAction === "menu" &&
|
|
148
|
+
menuItems && (
|
|
149
|
+
<ActionMenu
|
|
150
|
+
menuItems={menuItems}
|
|
151
|
+
onSelect={(action) => handleMenuAction(action, item)}
|
|
152
|
+
selected={isSelected}
|
|
153
|
+
onOpenChange={(open) => {
|
|
154
|
+
setDropdownOpenId(open ? id : undefined);
|
|
155
|
+
|
|
156
|
+
if (open && !isSelected) {
|
|
157
|
+
handleSelect(id);
|
|
158
|
+
}
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
));
|
|
162
|
+
|
|
132
163
|
const end =
|
|
133
164
|
action ||
|
|
134
165
|
(detailPosition === "end" && renderDetail?.(item, isSelected));
|
|
@@ -172,15 +203,10 @@ export const List = memoGeneric(
|
|
|
172
203
|
}
|
|
173
204
|
}}
|
|
174
205
|
menuItems={menuItems}
|
|
175
|
-
onSelectMenuItem={handleMenuAction}
|
|
206
|
+
onSelectMenuItem={(action) => handleMenuAction(action, item)}
|
|
176
207
|
onContextMenu={() => {
|
|
177
208
|
if (!isSelected) {
|
|
178
|
-
handleSelect(id
|
|
179
|
-
shiftKey: false,
|
|
180
|
-
altKey: false,
|
|
181
|
-
metaKey: false,
|
|
182
|
-
ctrlKey: false,
|
|
183
|
-
});
|
|
209
|
+
handleSelect(id);
|
|
184
210
|
}
|
|
185
211
|
}}
|
|
186
212
|
tabIndex={0}
|
|
@@ -778,6 +778,7 @@ export type ListViewRootProps = {
|
|
|
778
778
|
onDragEnter?: React.DragEventHandler<HTMLDivElement>;
|
|
779
779
|
onDragLeave?: React.DragEventHandler<HTMLDivElement>;
|
|
780
780
|
onDrop?: React.DragEventHandler<HTMLDivElement>;
|
|
781
|
+
renderEmptyState?: () => ReactNode;
|
|
781
782
|
};
|
|
782
783
|
|
|
783
784
|
const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
|
|
@@ -811,6 +812,7 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
|
|
|
811
812
|
onDragEnter,
|
|
812
813
|
onDragLeave,
|
|
813
814
|
onDrop,
|
|
815
|
+
renderEmptyState,
|
|
814
816
|
}: RenderProps<T> & ListViewRootProps,
|
|
815
817
|
forwardedRef: ForwardedRef<IVirtualizedList>
|
|
816
818
|
) {
|
|
@@ -1040,23 +1042,25 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
|
|
|
1040
1042
|
onDragLeave={onDragLeave}
|
|
1041
1043
|
onDrop={onDrop}
|
|
1042
1044
|
>
|
|
1043
|
-
{
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1045
|
+
{data.length === 0 && renderEmptyState
|
|
1046
|
+
? renderEmptyState()
|
|
1047
|
+
: withScrollable((scrollElementRef: HTMLDivElement | null) =>
|
|
1048
|
+
withSortable(
|
|
1049
|
+
virtualized ? (
|
|
1050
|
+
<VirtualizedList<T>
|
|
1051
|
+
ref={forwardedRef}
|
|
1052
|
+
scrollElement={scrollElementRef!}
|
|
1053
|
+
items={data}
|
|
1054
|
+
size={virtualized}
|
|
1055
|
+
getItemHeight={getItemHeight}
|
|
1056
|
+
keyExtractor={getKey}
|
|
1057
|
+
renderItem={renderWrappedChild}
|
|
1058
|
+
/>
|
|
1059
|
+
) : (
|
|
1060
|
+
range(0, data.length).map(renderWrappedChild)
|
|
1061
|
+
)
|
|
1062
|
+
)
|
|
1063
|
+
)}
|
|
1060
1064
|
</div>
|
|
1061
1065
|
</ListViewDraggingContext.Provider>
|
|
1062
1066
|
);
|