@noya-app/noya-designsystem 0.1.43 → 0.1.45
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 +12 -9
- package/CHANGELOG.md +15 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +323 -80
- package/dist/index.d.ts +323 -80
- package/dist/index.js +2126 -1373
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2012 -1272
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/components/AnimatePresence.tsx +10 -1
- package/src/components/Avatar.tsx +2 -0
- package/src/components/Checkbox.tsx +6 -1
- package/src/components/Combobox.tsx +28 -14
- package/src/components/ComboboxMenu.tsx +30 -15
- package/src/components/CommandPalette.tsx +69 -0
- package/src/components/ContextMenu.tsx +33 -134
- package/src/components/DropdownMenu.tsx +47 -155
- package/src/components/Fade.tsx +62 -0
- package/src/components/InputField.tsx +109 -133
- package/src/components/Label.tsx +81 -7
- package/src/components/LabeledField.tsx +112 -0
- package/src/components/ListView.tsx +55 -52
- package/src/components/Popover.tsx +12 -1
- package/src/components/SearchCompletionMenu.tsx +206 -0
- package/src/components/SegmentedControl.tsx +5 -2
- package/src/components/SelectMenu.tsx +108 -124
- package/src/components/SidebarList.tsx +252 -0
- package/src/components/Slider.tsx +18 -26
- package/src/components/Text.tsx +1 -0
- package/src/components/TextArea.tsx +4 -1
- package/src/components/Toolbar.tsx +6 -2
- package/src/components/TreeView.tsx +3 -0
- package/src/components/WorkspaceLayout.tsx +38 -18
- package/src/components/internal/Menu.tsx +58 -10
- package/src/components/internal/MenuViewport.tsx +151 -0
- package/src/components/internal/SelectItem.tsx +111 -0
- package/src/hooks/useIndent.ts +12 -0
- package/src/hooks/useLabel.ts +51 -0
- package/src/hooks/usePreservePanelSize.tsx +50 -19
- package/src/index.tsx +12 -5
- package/src/utils/combobox.ts +4 -1
- package/src/utils/createSectionedMenu.ts +11 -8
- package/src/utils/selection.ts +36 -0
|
@@ -18,20 +18,19 @@ import React, {
|
|
|
18
18
|
useRef,
|
|
19
19
|
useState,
|
|
20
20
|
} from "react";
|
|
21
|
+
import { useLabel, useLabelPosition } from "../hooks/useLabel";
|
|
21
22
|
import { cx } from "../utils/classNames";
|
|
22
23
|
import handleNudge from "../utils/handleNudge";
|
|
23
|
-
import { Button } from "./Button";
|
|
24
|
+
import { Button, ButtonRootProps } from "./Button";
|
|
24
25
|
import { DropdownMenu } from "./DropdownMenu";
|
|
25
26
|
import { MenuItem } from "./internal/Menu";
|
|
26
27
|
import TextInput, { TextInputProps } from "./internal/TextInput";
|
|
28
|
+
import { InsetLabel, LabelPosition } from "./Label";
|
|
27
29
|
import { Popover } from "./Popover";
|
|
28
30
|
|
|
29
|
-
type LabelPosition = "start" | "end";
|
|
30
|
-
|
|
31
31
|
export type InputFieldSize = "small" | "medium" | "large";
|
|
32
32
|
|
|
33
33
|
type InputFieldContextValue = {
|
|
34
|
-
labelPosition: LabelPosition;
|
|
35
34
|
labelSize?: number;
|
|
36
35
|
buttonSize?: number;
|
|
37
36
|
/** @default medium */
|
|
@@ -43,51 +42,11 @@ type InputFieldContextValue = {
|
|
|
43
42
|
setLabelWidth?: (width: number) => void;
|
|
44
43
|
setButtonWidth?: (width: number) => void;
|
|
45
44
|
id?: string;
|
|
45
|
+
label?: ReactNode;
|
|
46
|
+
labelPosition?: LabelPosition;
|
|
46
47
|
};
|
|
47
48
|
|
|
48
|
-
const getLabelPosition = ({
|
|
49
|
-
labelPosition,
|
|
50
|
-
size,
|
|
51
|
-
buttonSize,
|
|
52
|
-
}: {
|
|
53
|
-
labelPosition: LabelPosition;
|
|
54
|
-
size: InputFieldSize;
|
|
55
|
-
buttonSize?: number;
|
|
56
|
-
}) => ({
|
|
57
|
-
left:
|
|
58
|
-
labelPosition === "start"
|
|
59
|
-
? size === "large"
|
|
60
|
-
? "6px"
|
|
61
|
-
: size === "medium"
|
|
62
|
-
? "2px"
|
|
63
|
-
: "0px"
|
|
64
|
-
: "",
|
|
65
|
-
right:
|
|
66
|
-
labelPosition === "end"
|
|
67
|
-
? size === "large"
|
|
68
|
-
? `${6 + (buttonSize ?? 0)}px`
|
|
69
|
-
: size === "medium"
|
|
70
|
-
? `${2 + (buttonSize ?? 0)}px`
|
|
71
|
-
: `${buttonSize ?? 0}px`
|
|
72
|
-
: "",
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const getInputFieldSpacing = ({
|
|
76
|
-
labelPosition,
|
|
77
|
-
buttonSize,
|
|
78
|
-
labelSize,
|
|
79
|
-
}: {
|
|
80
|
-
labelPosition: LabelPosition;
|
|
81
|
-
size?: InputFieldSize;
|
|
82
|
-
buttonSize?: number;
|
|
83
|
-
labelSize?: number;
|
|
84
|
-
}) => ({
|
|
85
|
-
paddingLeft: `${labelSize && labelPosition === "start" ? 6 + labelSize : 6}px`,
|
|
86
|
-
paddingRight: `${labelPosition === "end" ? 6 + (labelSize ?? 0) + (buttonSize ? buttonSize + 6 : 0) : 6}px`,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
49
|
const InputFieldContext = createContext<InputFieldContextValue>({
|
|
90
|
-
labelPosition: "end",
|
|
91
50
|
labelSize: undefined,
|
|
92
51
|
buttonSize: undefined,
|
|
93
52
|
size: "medium",
|
|
@@ -95,69 +54,37 @@ const InputFieldContext = createContext<InputFieldContextValue>({
|
|
|
95
54
|
onFocusChange: () => {},
|
|
96
55
|
});
|
|
97
56
|
|
|
57
|
+
export const useInputFieldContext = () => useContext(InputFieldContext);
|
|
58
|
+
|
|
98
59
|
const noop = () => {};
|
|
99
60
|
|
|
61
|
+
export const getFieldSpacing = ({
|
|
62
|
+
buttonSize = 0,
|
|
63
|
+
labelSize = 0,
|
|
64
|
+
variant,
|
|
65
|
+
}: {
|
|
66
|
+
buttonSize?: number;
|
|
67
|
+
labelSize?: number;
|
|
68
|
+
variant?: InputFieldVariant;
|
|
69
|
+
}) => ({
|
|
70
|
+
paddingLeft: variant === "bare" ? "0px" : "6px",
|
|
71
|
+
paddingRight: `${variant === "bare" ? 0 : labelSize + buttonSize + (labelSize || buttonSize ? 12 : 6)}px`,
|
|
72
|
+
});
|
|
73
|
+
|
|
100
74
|
/* ----------------------------------------------------------------------------
|
|
101
75
|
* Label
|
|
102
76
|
* ------------------------------------------------------------------------- */
|
|
103
77
|
|
|
104
|
-
interface LabelContainerProps {
|
|
105
|
-
labelPosition: LabelPosition;
|
|
106
|
-
size: InputFieldSize;
|
|
107
|
-
className?: string;
|
|
108
|
-
children?: React.ReactNode;
|
|
109
|
-
style?: React.CSSProperties;
|
|
110
|
-
htmlFor?: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const LabelContainer = forwardRef(function LabelContainer(
|
|
114
|
-
{
|
|
115
|
-
labelPosition: $labelPosition,
|
|
116
|
-
size: $size,
|
|
117
|
-
className,
|
|
118
|
-
style,
|
|
119
|
-
...props
|
|
120
|
-
}: LabelContainerProps,
|
|
121
|
-
ref: ForwardedRef<HTMLLabelElement>
|
|
122
|
-
) {
|
|
123
|
-
const { buttonSize } = useContext(InputFieldContext);
|
|
124
|
-
const spacingStyles = getLabelPosition({
|
|
125
|
-
labelPosition: $labelPosition,
|
|
126
|
-
size: $size,
|
|
127
|
-
buttonSize,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const memoizedStyles = useMemo(
|
|
131
|
-
() => ({ ...spacingStyles, ...style }),
|
|
132
|
-
[spacingStyles, style]
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
return (
|
|
136
|
-
<label
|
|
137
|
-
ref={ref}
|
|
138
|
-
className={cx(
|
|
139
|
-
"absolute top-0 bottom-0 flex items-center font-sans text-label font-bold uppercase text-text-disabled select-none z-label pointer-events-none",
|
|
140
|
-
$labelPosition === "start"
|
|
141
|
-
? `justify-start pl-1.5`
|
|
142
|
-
: $labelPosition === "end" && "justify-end pr-1.5",
|
|
143
|
-
className
|
|
144
|
-
)}
|
|
145
|
-
style={memoizedStyles}
|
|
146
|
-
{...props}
|
|
147
|
-
/>
|
|
148
|
-
);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
78
|
/**
|
|
152
|
-
* Inserts a Label at the
|
|
79
|
+
* Inserts a Label at the end of the input, unless overridden by LabeledField.
|
|
153
80
|
*/
|
|
154
81
|
const InputFieldLabel = memo(
|
|
155
82
|
forwardRef(function InputFieldLabel(
|
|
156
83
|
props: LabelHTMLAttributes<HTMLLabelElement>,
|
|
157
84
|
forwardedRef: ForwardedRef<HTMLLabelElement>
|
|
158
85
|
) {
|
|
159
|
-
const {
|
|
160
|
-
|
|
86
|
+
const { setLabelWidth, size, buttonSize, label, id, labelPosition } =
|
|
87
|
+
useInputFieldContext();
|
|
161
88
|
|
|
162
89
|
const ref = useRef<HTMLLabelElement | null>(null);
|
|
163
90
|
|
|
@@ -171,17 +98,21 @@ const InputFieldLabel = memo(
|
|
|
171
98
|
setLabelWidth(width);
|
|
172
99
|
}, [setLabelWidth]);
|
|
173
100
|
|
|
101
|
+
if (labelPosition !== "inset") return null;
|
|
102
|
+
|
|
174
103
|
return (
|
|
175
|
-
<
|
|
104
|
+
<InsetLabel
|
|
176
105
|
ref={(element) => {
|
|
177
106
|
ref.current = element;
|
|
178
107
|
assignRef(forwardedRef, element);
|
|
179
108
|
}}
|
|
180
109
|
size={size}
|
|
181
|
-
|
|
110
|
+
buttonSize={buttonSize}
|
|
182
111
|
htmlFor={id}
|
|
183
112
|
{...props}
|
|
184
|
-
|
|
113
|
+
>
|
|
114
|
+
{label}
|
|
115
|
+
</InsetLabel>
|
|
185
116
|
);
|
|
186
117
|
})
|
|
187
118
|
);
|
|
@@ -236,10 +167,11 @@ const InputFieldDropdownMenu = memoGeneric(function InputFieldDropdownMenu<
|
|
|
236
167
|
/** Inserts a Button at the end of the input. Should not be used with an InputField.Label */
|
|
237
168
|
const InputFieldButton = memo(
|
|
238
169
|
forwardRef(function InputFieldButton(
|
|
239
|
-
props: React.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
170
|
+
props: React.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
171
|
+
ButtonRootProps & {
|
|
172
|
+
buttonClassName?: string;
|
|
173
|
+
variant?: "floating" | "normal";
|
|
174
|
+
},
|
|
243
175
|
forwardedRef: ForwardedRef<HTMLButtonElement>
|
|
244
176
|
) {
|
|
245
177
|
const {
|
|
@@ -248,9 +180,10 @@ const InputFieldButton = memo(
|
|
|
248
180
|
className,
|
|
249
181
|
buttonClassName,
|
|
250
182
|
variant = "normal",
|
|
183
|
+
as = "button",
|
|
251
184
|
...rest
|
|
252
185
|
} = props;
|
|
253
|
-
const { size, inputRef, setButtonWidth } =
|
|
186
|
+
const { size, inputRef, setButtonWidth } = useInputFieldContext();
|
|
254
187
|
|
|
255
188
|
const ref = useRef<HTMLButtonElement | null>(null);
|
|
256
189
|
|
|
@@ -291,7 +224,7 @@ const InputFieldButton = memo(
|
|
|
291
224
|
className={cx(
|
|
292
225
|
"absolute",
|
|
293
226
|
size === "large"
|
|
294
|
-
? "right-
|
|
227
|
+
? "right-1 top-2.5"
|
|
295
228
|
: size === "medium"
|
|
296
229
|
? "right-1 top-1"
|
|
297
230
|
: size === "small"
|
|
@@ -303,6 +236,7 @@ const InputFieldButton = memo(
|
|
|
303
236
|
)}
|
|
304
237
|
>
|
|
305
238
|
<Button
|
|
239
|
+
as={as}
|
|
306
240
|
ref={(element) => {
|
|
307
241
|
ref.current = element;
|
|
308
242
|
assignRef(forwardedRef, element);
|
|
@@ -314,7 +248,7 @@ const InputFieldButton = memo(
|
|
|
314
248
|
size={size === "medium" ? "normal" : size}
|
|
315
249
|
className={cx(
|
|
316
250
|
buttonClassName,
|
|
317
|
-
variant === "floating" && "bg-transparent shadow-none
|
|
251
|
+
variant === "floating" && "bg-transparent shadow-none"
|
|
318
252
|
)}
|
|
319
253
|
{...rest}
|
|
320
254
|
>
|
|
@@ -368,14 +302,12 @@ const InputElement = forwardRef(
|
|
|
368
302
|
}: InputElementProps,
|
|
369
303
|
ref: ForwardedRef<any>
|
|
370
304
|
) => {
|
|
371
|
-
const { buttonSize, labelSize,
|
|
372
|
-
useContext(InputFieldContext);
|
|
305
|
+
const { buttonSize, labelSize, size, id } = useInputFieldContext();
|
|
373
306
|
|
|
374
|
-
const spacingStyles =
|
|
375
|
-
labelPosition,
|
|
376
|
-
size,
|
|
307
|
+
const spacingStyles = getFieldSpacing({
|
|
377
308
|
buttonSize,
|
|
378
309
|
labelSize,
|
|
310
|
+
variant: $variant,
|
|
379
311
|
});
|
|
380
312
|
|
|
381
313
|
const inputClassName = cx(
|
|
@@ -414,6 +346,7 @@ const InputElement = forwardRef(
|
|
|
414
346
|
<TextInput
|
|
415
347
|
ref={ref}
|
|
416
348
|
value={value ?? ""}
|
|
349
|
+
id={id}
|
|
417
350
|
onSubmit={onSubmit ?? (() => {})}
|
|
418
351
|
{...props}
|
|
419
352
|
className={inputClassName}
|
|
@@ -434,7 +367,7 @@ const InputFieldInput = forwardRef(function InputFieldInput(
|
|
|
434
367
|
{ textAlign, variant, ...rest }: InputFieldInputProps,
|
|
435
368
|
forwardedRef: ForwardedRef<HTMLInputElement>
|
|
436
369
|
) {
|
|
437
|
-
const { onFocusChange, setInputRef
|
|
370
|
+
const { onFocusChange, setInputRef } = useInputFieldContext();
|
|
438
371
|
|
|
439
372
|
const handleFocusChange = useCallback(
|
|
440
373
|
(isFocused: boolean) => {
|
|
@@ -453,7 +386,6 @@ const InputFieldInput = forwardRef(function InputFieldInput(
|
|
|
453
386
|
$variant={variant}
|
|
454
387
|
$textAlign={textAlign}
|
|
455
388
|
onFocusChange={handleFocusChange}
|
|
456
|
-
id={id}
|
|
457
389
|
{...rest}
|
|
458
390
|
/>
|
|
459
391
|
);
|
|
@@ -505,6 +437,7 @@ type InputFieldNumberInputProps = Omit<
|
|
|
505
437
|
onFocus?: FocusEventHandler;
|
|
506
438
|
onBlur?: FocusEventHandler;
|
|
507
439
|
readOnly?: boolean;
|
|
440
|
+
className?: string;
|
|
508
441
|
} & (
|
|
509
442
|
| {
|
|
510
443
|
onChange: (value: number) => void;
|
|
@@ -634,8 +567,6 @@ interface InputFieldRootProps {
|
|
|
634
567
|
id?: string;
|
|
635
568
|
children?: ReactNode;
|
|
636
569
|
width?: number;
|
|
637
|
-
/** @default end */
|
|
638
|
-
labelPosition?: LabelPosition;
|
|
639
570
|
labelSize?: number;
|
|
640
571
|
/** @default medium */
|
|
641
572
|
size?: InputFieldSize;
|
|
@@ -651,10 +582,9 @@ interface InputFieldRootProps {
|
|
|
651
582
|
}
|
|
652
583
|
|
|
653
584
|
function InputFieldRoot({
|
|
654
|
-
id,
|
|
585
|
+
id: idProp,
|
|
655
586
|
children,
|
|
656
587
|
width,
|
|
657
|
-
labelPosition = "end",
|
|
658
588
|
sideOffset = 3,
|
|
659
589
|
labelSize,
|
|
660
590
|
size = "medium",
|
|
@@ -663,7 +593,9 @@ function InputFieldRoot({
|
|
|
663
593
|
style,
|
|
664
594
|
className,
|
|
665
595
|
}: InputFieldRootProps) {
|
|
596
|
+
const { fieldId: id, label } = useLabel({ fieldId: idProp });
|
|
666
597
|
const randomId = useId();
|
|
598
|
+
const labelPosition = useLabelPosition();
|
|
667
599
|
|
|
668
600
|
const [isFocused, setIsFocused] = React.useState(false);
|
|
669
601
|
|
|
@@ -693,7 +625,6 @@ function InputFieldRoot({
|
|
|
693
625
|
|
|
694
626
|
const contextValue = useMemo(
|
|
695
627
|
(): InputFieldContextValue => ({
|
|
696
|
-
labelPosition,
|
|
697
628
|
labelSize: measuredLabelSize ?? labelSize,
|
|
698
629
|
buttonSize: measuredButtonSize,
|
|
699
630
|
size,
|
|
@@ -704,9 +635,10 @@ function InputFieldRoot({
|
|
|
704
635
|
setLabelWidth: setMeasuredLabelSize,
|
|
705
636
|
setButtonWidth: setMeasuredButtonSize,
|
|
706
637
|
id: id ?? randomId,
|
|
638
|
+
label,
|
|
639
|
+
labelPosition,
|
|
707
640
|
}),
|
|
708
641
|
[
|
|
709
|
-
labelPosition,
|
|
710
642
|
measuredLabelSize,
|
|
711
643
|
labelSize,
|
|
712
644
|
measuredButtonSize,
|
|
@@ -716,12 +648,15 @@ function InputFieldRoot({
|
|
|
716
648
|
inputRef,
|
|
717
649
|
id,
|
|
718
650
|
randomId,
|
|
651
|
+
label,
|
|
652
|
+
labelPosition,
|
|
719
653
|
]
|
|
720
654
|
);
|
|
721
655
|
|
|
722
656
|
const rootElement = (
|
|
723
657
|
<RootContainer $width={width} style={style} className={className}>
|
|
724
658
|
{children}
|
|
659
|
+
{label && <InputFieldLabel />}
|
|
725
660
|
</RootContainer>
|
|
726
661
|
);
|
|
727
662
|
|
|
@@ -776,11 +711,9 @@ const PrimitiveInputField = ({
|
|
|
776
711
|
React.InputHTMLAttributes<HTMLInputElement>,
|
|
777
712
|
HTMLInputElement
|
|
778
713
|
>) => {
|
|
779
|
-
const {
|
|
780
|
-
useContext(InputFieldContext);
|
|
714
|
+
const { id, labelSize, buttonSize } = useInputFieldContext();
|
|
781
715
|
|
|
782
|
-
const spacingStyles =
|
|
783
|
-
labelPosition,
|
|
716
|
+
const spacingStyles = getFieldSpacing({
|
|
784
717
|
buttonSize,
|
|
785
718
|
labelSize,
|
|
786
719
|
});
|
|
@@ -791,6 +724,7 @@ const PrimitiveInputField = ({
|
|
|
791
724
|
return (
|
|
792
725
|
<input
|
|
793
726
|
style={memoizedStyles}
|
|
727
|
+
id={id}
|
|
794
728
|
className={cx(
|
|
795
729
|
"flex w-0 flex-1 relative border-none outline-none min-w-0 self-stretch rounded py-1 bg-input-background select-all pointer-events-[all] focus:ring-2 focus:ring-primary font-sans font-normal text-heading5 placeholder:text-text-disabled focus:z-interactable transition-all",
|
|
796
730
|
readOnly
|
|
@@ -805,13 +739,55 @@ const PrimitiveInputField = ({
|
|
|
805
739
|
);
|
|
806
740
|
};
|
|
807
741
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
742
|
+
// Create a type for the main InputField component that combines Root and Input props
|
|
743
|
+
export type InputFieldProps = InputFieldRootProps & InputFieldInputProps;
|
|
744
|
+
|
|
745
|
+
// Create a map of all subcomponents
|
|
746
|
+
const InputFieldSubcomponents = {
|
|
747
|
+
Root: memo(InputFieldRoot),
|
|
748
|
+
Input: memo(InputFieldInput),
|
|
749
|
+
Typeahead: memo(InputFieldTypeahead),
|
|
750
|
+
NumberInput: memo(InputFieldNumberInput),
|
|
751
|
+
DropdownMenu: InputFieldDropdownMenu,
|
|
752
|
+
Button: InputFieldButton,
|
|
753
|
+
Label: InputFieldLabel,
|
|
754
|
+
PrimitiveElement: PrimitiveInputField,
|
|
755
|
+
} as const;
|
|
756
|
+
|
|
757
|
+
// Create the main InputField component
|
|
758
|
+
const InputFieldComponent = forwardRef<HTMLInputElement, InputFieldProps>(
|
|
759
|
+
function InputField({ children, ...props }: InputFieldProps, ref) {
|
|
760
|
+
const {
|
|
761
|
+
width,
|
|
762
|
+
labelSize,
|
|
763
|
+
size,
|
|
764
|
+
renderPopoverContent,
|
|
765
|
+
sideOffset,
|
|
766
|
+
onFocusChange,
|
|
767
|
+
style,
|
|
768
|
+
className,
|
|
769
|
+
...inputProps
|
|
770
|
+
} = props;
|
|
771
|
+
|
|
772
|
+
return (
|
|
773
|
+
<InputFieldSubcomponents.Root
|
|
774
|
+
width={width}
|
|
775
|
+
labelSize={labelSize}
|
|
776
|
+
size={size}
|
|
777
|
+
renderPopoverContent={renderPopoverContent}
|
|
778
|
+
sideOffset={sideOffset}
|
|
779
|
+
onFocusChange={onFocusChange}
|
|
780
|
+
style={style}
|
|
781
|
+
className={className}
|
|
782
|
+
>
|
|
783
|
+
<InputFieldSubcomponents.Input ref={ref} {...inputProps} />
|
|
784
|
+
{children}
|
|
785
|
+
</InputFieldSubcomponents.Root>
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
// Export both the namespace and the component
|
|
791
|
+
const InputField = Object.assign(InputFieldComponent, InputFieldSubcomponents);
|
|
792
|
+
|
|
793
|
+
export { InputField };
|
package/src/components/Label.tsx
CHANGED
|
@@ -1,22 +1,96 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React, {
|
|
2
|
+
CSSProperties,
|
|
3
|
+
LabelHTMLAttributes,
|
|
4
|
+
forwardRef,
|
|
5
|
+
memo,
|
|
6
|
+
useMemo,
|
|
7
|
+
} from "react";
|
|
2
8
|
import { cx } from "../utils/classNames";
|
|
9
|
+
import { InputFieldSize } from "./InputField";
|
|
3
10
|
|
|
4
|
-
|
|
11
|
+
export type LabelPosition = "inset" | "start" | "end" | "above";
|
|
5
12
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
export interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
|
|
14
|
+
/** @default start */
|
|
15
|
+
position?: Exclude<LabelPosition, "inset">;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type InsetLabelProps = {
|
|
19
|
+
/** @default medium */
|
|
20
|
+
size?: InputFieldSize;
|
|
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
|
+
buttonSize?: number;
|
|
23
|
+
} & LabelHTMLAttributes<HTMLLabelElement>;
|
|
24
|
+
|
|
25
|
+
// apply to <label
|
|
26
|
+
export const insetLabelTextStyles = `font-sans text-label uppercase text-text-disabled leading-[19px] font-bold text-[60%] truncate pointer-events-none text-right`;
|
|
27
|
+
|
|
28
|
+
// InsetLabelContainer
|
|
29
|
+
export const insetLabelStyles = `flex items-center absolute top-0 bottom-0 z-label`;
|
|
30
|
+
const getInsetLabelPosition = ({
|
|
31
|
+
buttonSize,
|
|
32
|
+
}: {
|
|
33
|
+
buttonSize?: number;
|
|
34
|
+
}): CSSProperties => ({
|
|
35
|
+
right: `${6 + (buttonSize ?? 0)}px`,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const InsetLabel = memo(
|
|
39
|
+
forwardRef<HTMLLabelElement, InsetLabelProps>(function InsetLabel(
|
|
40
|
+
{ children, buttonSize, size = "medium", className, style, ...props },
|
|
41
|
+
ref
|
|
42
|
+
) {
|
|
43
|
+
const styles = useMemo(
|
|
44
|
+
() => ({
|
|
45
|
+
...getInsetLabelPosition({
|
|
46
|
+
buttonSize,
|
|
47
|
+
}),
|
|
48
|
+
...style,
|
|
49
|
+
}),
|
|
50
|
+
[buttonSize, style]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<label
|
|
55
|
+
ref={ref}
|
|
56
|
+
style={styles}
|
|
57
|
+
className={cx(
|
|
58
|
+
insetLabelStyles,
|
|
59
|
+
insetLabelTextStyles,
|
|
60
|
+
size === "small" ? "min-h-[19px]" : "min-h-[27px]",
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</label>
|
|
67
|
+
);
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const labelStyles = "flex items-center select-none z-label relative";
|
|
72
|
+
const labelTextStyles =
|
|
73
|
+
"font-sans text-label uppercase leading-3 font-bold text-text-muted";
|
|
74
|
+
|
|
75
|
+
export const Label = memo(
|
|
76
|
+
forwardRef<HTMLLabelElement, LabelProps>(function Label(
|
|
77
|
+
{ className, position = "start", children, ...props },
|
|
9
78
|
ref
|
|
10
79
|
) {
|
|
11
80
|
return (
|
|
12
81
|
<label
|
|
13
82
|
ref={ref}
|
|
14
83
|
className={cx(
|
|
15
|
-
|
|
84
|
+
labelStyles,
|
|
85
|
+
labelTextStyles,
|
|
86
|
+
"text-left min-h-[27px]",
|
|
87
|
+
(position === "start" || position === "end") && "self-start",
|
|
16
88
|
className
|
|
17
89
|
)}
|
|
18
90
|
{...props}
|
|
19
|
-
|
|
91
|
+
>
|
|
92
|
+
{children}
|
|
93
|
+
</label>
|
|
20
94
|
);
|
|
21
95
|
})
|
|
22
96
|
);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React, { memo, useMemo } from "react";
|
|
2
|
+
import { useIndent } from "../hooks/useIndent";
|
|
3
|
+
import {
|
|
4
|
+
LabelContext,
|
|
5
|
+
LabelPositionContext,
|
|
6
|
+
LabelPositionContextValue,
|
|
7
|
+
useLabelWidth,
|
|
8
|
+
} from "../hooks/useLabel";
|
|
9
|
+
import { cx } from "../utils/classNames";
|
|
10
|
+
import { Label, LabelPosition } from "./Label";
|
|
11
|
+
|
|
12
|
+
export const labeledFieldStyles = (labelPosition?: LabelPosition) =>
|
|
13
|
+
cx(
|
|
14
|
+
"flex flex-row flex-1 flex-wrap items-center gap-x-1.5",
|
|
15
|
+
labelPosition === "end" && "flex-row-reverse justify-end",
|
|
16
|
+
labelPosition === "above" && "flex-col items-stretch justify-start"
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const labelPositionInset: LabelPositionContextValue = {
|
|
20
|
+
position: "inset",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const LabeledField = memo(function LabeledField({
|
|
24
|
+
children,
|
|
25
|
+
label,
|
|
26
|
+
labelPosition = "start",
|
|
27
|
+
fieldId,
|
|
28
|
+
minWidth = 100,
|
|
29
|
+
className,
|
|
30
|
+
style,
|
|
31
|
+
...props
|
|
32
|
+
}: {
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
label: React.ReactNode;
|
|
35
|
+
labelPosition?: LabelPosition;
|
|
36
|
+
/** Used for both the `id` of the field and the `htmlFor` of the label, unless `id` and `htmlFor` are explicitly provided */
|
|
37
|
+
fieldId?: string;
|
|
38
|
+
className?: string;
|
|
39
|
+
style?: React.CSSProperties;
|
|
40
|
+
/** @default 100 */
|
|
41
|
+
minWidth?: number;
|
|
42
|
+
}) {
|
|
43
|
+
const indentLevel = useIndent();
|
|
44
|
+
const labelWidthFromContext = useLabelWidth();
|
|
45
|
+
const labelWidth = labelWidthFromContext ?? minWidth;
|
|
46
|
+
|
|
47
|
+
const labeledFieldClasses = useMemo(
|
|
48
|
+
() => labeledFieldStyles(labelPosition),
|
|
49
|
+
[labelPosition]
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const labelContextValue = useMemo(
|
|
53
|
+
() => ({ fieldId, label }),
|
|
54
|
+
[fieldId, label]
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const indentStyle = useMemo(
|
|
58
|
+
() => ({
|
|
59
|
+
width: indentLevel * 8,
|
|
60
|
+
flexBasis: indentLevel * 8,
|
|
61
|
+
flexShrink: 0,
|
|
62
|
+
flexGrow: 0,
|
|
63
|
+
}),
|
|
64
|
+
[indentLevel]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const fieldStyle = useMemo(
|
|
68
|
+
() => ({
|
|
69
|
+
minWidth: `calc(100% - ${labelWidth + indentLevel * 8 + 6}px)`,
|
|
70
|
+
}),
|
|
71
|
+
[labelWidth, indentLevel]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const labelStyle = useMemo(
|
|
75
|
+
() => ({
|
|
76
|
+
minWidth: `calc(${labelWidth}px - ${indentLevel * 8}px)`,
|
|
77
|
+
}),
|
|
78
|
+
[labelWidth, indentLevel]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<LabelContext.Provider value={labelContextValue}>
|
|
83
|
+
{/* let components individually handle inset labels using info from context*/}
|
|
84
|
+
{labelPosition === "inset" ? (
|
|
85
|
+
<LabelPositionContext.Provider value={labelPositionInset}>
|
|
86
|
+
{children}
|
|
87
|
+
</LabelPositionContext.Provider>
|
|
88
|
+
) : (
|
|
89
|
+
<div className={cx("flex relative flex-1", className)} style={style}>
|
|
90
|
+
{Boolean(indentLevel) && <div style={indentStyle} />}
|
|
91
|
+
<div className={labeledFieldClasses}>
|
|
92
|
+
<Label
|
|
93
|
+
htmlFor={fieldId}
|
|
94
|
+
position={labelPosition}
|
|
95
|
+
style={labelStyle}
|
|
96
|
+
{...props}
|
|
97
|
+
>
|
|
98
|
+
{label}
|
|
99
|
+
</Label>
|
|
100
|
+
{labelPosition === "start" ? (
|
|
101
|
+
<div className="flex-auto flex" style={fieldStyle}>
|
|
102
|
+
{children}
|
|
103
|
+
</div>
|
|
104
|
+
) : (
|
|
105
|
+
children
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</LabelContext.Provider>
|
|
111
|
+
);
|
|
112
|
+
});
|