@rebasepro/ui 0.17.3 → 0.18.1
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/README.md +4 -0
- package/dist/components/MultiSelect.d.ts +10 -0
- package/dist/index.es.js +2 -1
- package/dist/index.es.js.map +1 -1
- package/package.json +32 -23
- package/src/components/Alert.tsx +0 -118
- package/src/components/Autocomplete.tsx +0 -93
- package/src/components/Avatar.tsx +0 -73
- package/src/components/Badge.tsx +0 -44
- package/src/components/BooleanSwitch.tsx +0 -116
- package/src/components/BooleanSwitchWithLabel.tsx +0 -120
- package/src/components/Button.tsx +0 -110
- package/src/components/Card.tsx +0 -43
- package/src/components/CenteredView.tsx +0 -35
- package/src/components/Checkbox.tsx +0 -129
- package/src/components/Chip.tsx +0 -131
- package/src/components/CircularProgress.tsx +0 -53
- package/src/components/CircularProgressCenter.tsx +0 -27
- package/src/components/Collapse.tsx +0 -66
- package/src/components/ColorPicker.tsx +0 -140
- package/src/components/Container.tsx +0 -51
- package/src/components/DateTimeField.tsx +0 -363
- package/src/components/DebouncedTextField.tsx +0 -76
- package/src/components/Dialog.tsx +0 -159
- package/src/components/DialogActions.tsx +0 -29
- package/src/components/DialogContent.tsx +0 -27
- package/src/components/DialogTitle.tsx +0 -41
- package/src/components/ErrorBoundary.tsx +0 -236
- package/src/components/ExpandablePanel.tsx +0 -130
- package/src/components/FileUpload.tsx +0 -103
- package/src/components/FilterChip.tsx +0 -89
- package/src/components/IconButton.tsx +0 -101
- package/src/components/InfoLabel.tsx +0 -23
- package/src/components/InputLabel.tsx +0 -40
- package/src/components/Label.tsx +0 -32
- package/src/components/LoadingButton.tsx +0 -32
- package/src/components/Markdown.tsx +0 -55
- package/src/components/Menu.tsx +0 -114
- package/src/components/Menubar.tsx +0 -355
- package/src/components/MultiSelect.tsx +0 -433
- package/src/components/Paper.tsx +0 -23
- package/src/components/Popover.tsx +0 -162
- package/src/components/RadioGroup.tsx +0 -74
- package/src/components/ResizablePanels.tsx +0 -189
- package/src/components/SearchBar.tsx +0 -136
- package/src/components/Select.tsx +0 -326
- package/src/components/Separator.tsx +0 -28
- package/src/components/Sheet.tsx +0 -135
- package/src/components/Skeleton.tsx +0 -54
- package/src/components/Slider.tsx +0 -110
- package/src/components/Table.tsx +0 -178
- package/src/components/Tabs.tsx +0 -185
- package/src/components/TextField.tsx +0 -270
- package/src/components/TextareaAutosize.tsx +0 -368
- package/src/components/ToggleButtonGroup.tsx +0 -69
- package/src/components/Tooltip.tsx +0 -153
- package/src/components/Typography.tsx +0 -132
- package/src/components/VirtualTable/VirtualTable.performance.test.tsx +0 -90
- package/src/components/VirtualTable/VirtualTable.tsx +0 -700
- package/src/components/VirtualTable/VirtualTableCell.tsx +0 -59
- package/src/components/VirtualTable/VirtualTableHeader.tsx +0 -327
- package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +0 -250
- package/src/components/VirtualTable/VirtualTableProps.tsx +0 -339
- package/src/components/VirtualTable/VirtualTableRow.tsx +0 -51
- package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +0 -44
- package/src/components/VirtualTable/fields/VirtualTableInput.tsx +0 -89
- package/src/components/VirtualTable/fields/VirtualTableNumberInput.tsx +0 -71
- package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +0 -107
- package/src/components/VirtualTable/fields/VirtualTableSwitch.tsx +0 -28
- package/src/components/VirtualTable/index.tsx +0 -10
- package/src/components/VirtualTable/selection/SelectionContext.tsx +0 -36
- package/src/components/VirtualTable/selection/SelectionStore.ts +0 -54
- package/src/components/VirtualTable/types.tsx +0 -47
- package/src/components/common/SelectInputLabel.tsx +0 -23
- package/src/components/index.tsx +0 -62
- package/src/hooks/PortalContainerContext.tsx +0 -50
- package/src/hooks/index.ts +0 -7
- package/src/hooks/useDebounceCallback.tsx +0 -21
- package/src/hooks/useDebounceValue.tsx +0 -20
- package/src/hooks/useDebouncedCallback.ts +0 -39
- package/src/hooks/useInjectStyles.tsx +0 -31
- package/src/hooks/useOutsideAlerter.tsx +0 -46
- package/src/icons/GitHubIcon.tsx +0 -21
- package/src/icons/HandleIcon.tsx +0 -23
- package/src/icons/Icon.tsx +0 -33
- package/src/icons/LucideIconByName.tsx +0 -117
- package/src/icons/cool_icon_keys.ts +0 -12
- package/src/icons/icon_keys.ts +0 -1936
- package/src/icons/index.ts +0 -179
- package/src/index.css +0 -169
- package/src/index.ts +0 -47
- package/src/scripts/fixIconSizes.cjs +0 -92
- package/src/scripts/fixIconSizesToConst.cjs +0 -106
- package/src/scripts/generateIconKeys.ts +0 -41
- package/src/scripts/generateIcons.ts +0 -4
- package/src/scripts/saveIconFiles.ts +0 -89
- package/src/styles.ts +0 -141
- package/src/theme.css +0 -168
- package/src/util/chip_colors.ts +0 -242
- package/src/util/cls.ts +0 -8
- package/src/util/debounce.ts +0 -28
- package/src/util/hash.ts +0 -11
- package/src/util/index.ts +0 -4
- package/src/util/key_to_icon_component.ts +0 -11
- package/src/util/lazy_chunk.ts +0 -101
- package/src/views/CardView.tsx +0 -281
- package/src/views/CollectionView/CollectionCardView.tsx +0 -265
- package/src/views/CollectionView/CollectionKanbanView.tsx +0 -246
- package/src/views/CollectionView/CollectionListView.tsx +0 -240
- package/src/views/CollectionView/CollectionTableView.tsx +0 -229
- package/src/views/CollectionView/CollectionView.tsx +0 -450
- package/src/views/CollectionView/CollectionViewToolbar.tsx +0 -242
- package/src/views/CollectionView/CollectionViewTypes.ts +0 -181
- package/src/views/CollectionView/DefaultCellRenderer.tsx +0 -404
- package/src/views/CollectionView/index.ts +0 -39
- package/src/views/CollectionView/resolveTitleProperty.ts +0 -52
- package/src/views/CollectionView/utils.ts +0 -14
- package/src/views/Kanban/Board.tsx +0 -384
- package/src/views/Kanban/BoardColumn.tsx +0 -138
- package/src/views/Kanban/BoardColumnTitle.tsx +0 -47
- package/src/views/Kanban/BoardSortableList.tsx +0 -182
- package/src/views/Kanban/board_types.ts +0 -80
- package/src/views/Kanban/index.tsx +0 -5
- package/src/views/Kanban/placement.ts +0 -62
- package/src/views/KanbanView.tsx +0 -32
- package/src/views/ListView.tsx +0 -296
- package/src/views/index.ts +0 -5
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import React, { ForwardedRef, forwardRef, useEffect, useId, useRef } from "react";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
fieldBackgroundDisabledMixin,
|
|
6
|
-
fieldBackgroundHoverMixin,
|
|
7
|
-
fieldBackgroundInvisibleMixin,
|
|
8
|
-
fieldBackgroundMixin,
|
|
9
|
-
focusedInvisibleMixin
|
|
10
|
-
} from "../styles";
|
|
11
|
-
import { InputLabel } from "./InputLabel";
|
|
12
|
-
import { cls } from "../util";
|
|
13
|
-
|
|
14
|
-
export type InputType =
|
|
15
|
-
| "text"
|
|
16
|
-
| "number"
|
|
17
|
-
| "phone"
|
|
18
|
-
| "email"
|
|
19
|
-
| "password"
|
|
20
|
-
| "search"
|
|
21
|
-
| "url"
|
|
22
|
-
| "date"
|
|
23
|
-
| "time"
|
|
24
|
-
| "datetime-local"
|
|
25
|
-
| "month"
|
|
26
|
-
| "week"
|
|
27
|
-
| "color";
|
|
28
|
-
|
|
29
|
-
export type TextFieldProps<T extends string | number> = {
|
|
30
|
-
type?: InputType;
|
|
31
|
-
value?: T;
|
|
32
|
-
onChange?: (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
|
33
|
-
label?: React.ReactNode;
|
|
34
|
-
multiline?: boolean;
|
|
35
|
-
disabled?: boolean;
|
|
36
|
-
invisible?: boolean;
|
|
37
|
-
error?: boolean;
|
|
38
|
-
endAdornment?: React.ReactNode;
|
|
39
|
-
autoFocus?: boolean;
|
|
40
|
-
placeholder?: string;
|
|
41
|
-
size?: "smallest" | "small" | "medium" | "large";
|
|
42
|
-
className?: string;
|
|
43
|
-
style?: React.CSSProperties;
|
|
44
|
-
inputClassName?: string;
|
|
45
|
-
inputStyle?: React.CSSProperties;
|
|
46
|
-
inputRef?: React.ForwardedRef<any>;
|
|
47
|
-
/**
|
|
48
|
-
* Maximum number of rows to display.
|
|
49
|
-
*/
|
|
50
|
-
maxRows?: number | string;
|
|
51
|
-
/**
|
|
52
|
-
* Minimum number of rows to display.
|
|
53
|
-
* @default 1
|
|
54
|
-
*/
|
|
55
|
-
minRows?: number | string;
|
|
56
|
-
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "value">;
|
|
57
|
-
|
|
58
|
-
export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | number>>(
|
|
59
|
-
<T extends string | number>(
|
|
60
|
-
{
|
|
61
|
-
value,
|
|
62
|
-
onChange,
|
|
63
|
-
label,
|
|
64
|
-
type = "text",
|
|
65
|
-
multiline = false,
|
|
66
|
-
invisible,
|
|
67
|
-
maxRows,
|
|
68
|
-
minRows,
|
|
69
|
-
disabled,
|
|
70
|
-
error,
|
|
71
|
-
endAdornment,
|
|
72
|
-
autoFocus,
|
|
73
|
-
placeholder,
|
|
74
|
-
size = "large",
|
|
75
|
-
className,
|
|
76
|
-
style,
|
|
77
|
-
inputClassName,
|
|
78
|
-
inputStyle,
|
|
79
|
-
inputRef: inputRefProp,
|
|
80
|
-
...inputProps
|
|
81
|
-
}: TextFieldProps<T>,
|
|
82
|
-
ref: ForwardedRef<HTMLDivElement>
|
|
83
|
-
) => {
|
|
84
|
-
|
|
85
|
-
const fallbackRef = useRef(null);
|
|
86
|
-
const inputRef = inputRefProp ?? fallbackRef;
|
|
87
|
-
const autoId = useId();
|
|
88
|
-
const inputId = inputProps.id ?? autoId;
|
|
89
|
-
const labelId = `${inputId}-label`;
|
|
90
|
-
|
|
91
|
-
const [focused, setFocused] = React.useState(false);
|
|
92
|
-
const hasValue = value !== undefined && value !== null && value !== "";
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
const element = inputRef && "current" in inputRef ? inputRef.current : null;
|
|
96
|
-
if (element && document.activeElement === element) {
|
|
97
|
-
setFocused(true);
|
|
98
|
-
}
|
|
99
|
-
}, []);
|
|
100
|
-
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
if (type !== "number") return;
|
|
103
|
-
const handleWheel = (event: any) => {
|
|
104
|
-
if (event.target instanceof HTMLElement) event.target.blur();
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const element = "current" in inputRef ? inputRef.current : inputRef;
|
|
108
|
-
|
|
109
|
-
element?.addEventListener("wheel", handleWheel);
|
|
110
|
-
|
|
111
|
-
return () => {
|
|
112
|
-
element?.removeEventListener("wheel", handleWheel);
|
|
113
|
-
};
|
|
114
|
-
}, [inputRef, type]);
|
|
115
|
-
|
|
116
|
-
const input = multiline ? (
|
|
117
|
-
<textarea
|
|
118
|
-
{...(inputProps as React.TextareaHTMLAttributes<HTMLTextAreaElement>)}
|
|
119
|
-
ref={inputRef}
|
|
120
|
-
id={inputId}
|
|
121
|
-
// Falls back to the caller's own value rather than to `undefined`:
|
|
122
|
-
// this sits after the spread, so hardcoding `undefined` silently
|
|
123
|
-
// erased an `aria-labelledby` a consumer had passed, and the
|
|
124
|
-
// unlabelled field it was meant to name stayed unnamed.
|
|
125
|
-
aria-labelledby={label ? labelId : inputProps["aria-labelledby"]}
|
|
126
|
-
aria-invalid={error || undefined}
|
|
127
|
-
aria-disabled={disabled || undefined}
|
|
128
|
-
placeholder={focused || hasValue || !label ? placeholder : undefined}
|
|
129
|
-
autoFocus={autoFocus}
|
|
130
|
-
rows={typeof minRows === "string" ? parseInt(minRows) : (minRows ?? 3)}
|
|
131
|
-
value={value ?? ""}
|
|
132
|
-
onChange={onChange}
|
|
133
|
-
onFocus={() => setFocused(true)}
|
|
134
|
-
onBlur={() => setFocused(false)}
|
|
135
|
-
style={inputStyle}
|
|
136
|
-
className={cls(
|
|
137
|
-
invisible ? focusedInvisibleMixin : "",
|
|
138
|
-
"rounded-lg resize-none w-full outline-none text-base bg-transparent min-h-[64px] px-3",
|
|
139
|
-
label ? "pt-8 pb-2" : "py-2",
|
|
140
|
-
disabled && "outline-none opacity-50 text-surface-accent-600 dark:text-surface-accent-500",
|
|
141
|
-
inputClassName
|
|
142
|
-
)}
|
|
143
|
-
/>
|
|
144
|
-
) : (
|
|
145
|
-
<input
|
|
146
|
-
{...inputProps}
|
|
147
|
-
ref={inputRef}
|
|
148
|
-
id={inputId}
|
|
149
|
-
// See the textarea branch: preserve a caller-supplied
|
|
150
|
-
// `aria-labelledby` instead of overwriting it with `undefined`.
|
|
151
|
-
aria-labelledby={label ? labelId : inputProps["aria-labelledby"]}
|
|
152
|
-
aria-invalid={error || undefined}
|
|
153
|
-
aria-disabled={disabled || undefined}
|
|
154
|
-
disabled={disabled}
|
|
155
|
-
style={inputStyle}
|
|
156
|
-
className={cls(
|
|
157
|
-
"w-full outline-none bg-transparent leading-normal px-3",
|
|
158
|
-
"rounded-lg",
|
|
159
|
-
"focused:text-text-primary focused:dark:text-text-primary-dark",
|
|
160
|
-
invisible ? focusedInvisibleMixin : "",
|
|
161
|
-
disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
|
|
162
|
-
{
|
|
163
|
-
"min-h-[28px]": size === "smallest",
|
|
164
|
-
"min-h-[32px]": size === "small",
|
|
165
|
-
"min-h-[40px]": size === "medium",
|
|
166
|
-
"min-h-[48px]": size === "large"
|
|
167
|
-
},
|
|
168
|
-
/* A shrunk label occupies roughly the first 16px of the
|
|
169
|
-
box (`top-[-1px]`, translated down 2px, ~15px tall at
|
|
170
|
-
scale-75). `pt-4` put the value's first line at exactly
|
|
171
|
-
16px, so label and content shared a row: a labelled
|
|
172
|
-
`small` field with a placeholder rendered "Name" on top
|
|
173
|
-
of "e.g. Analytics Pipeline". `large` was never affected
|
|
174
|
-
— it reserves `pt-8`. */
|
|
175
|
-
label
|
|
176
|
-
// Labelled fields sit exactly 16px above the unlabelled
|
|
177
|
-
// scale (44/48/56/64 against 28/32/40/48). smallest,
|
|
178
|
-
// small and medium all used to share `pt-5 pb-1.5`, so
|
|
179
|
-
// they rendered at an identical 50px and `size` looked
|
|
180
|
-
// inert whenever a label was present.
|
|
181
|
-
? { smallest: "pt-4 pb-1", small: "pt-5 pb-1", medium: "pt-6 pb-2", large: "pt-8 pb-2" }[size]
|
|
182
|
-
: size === "smallest"
|
|
183
|
-
? "py-0.5"
|
|
184
|
-
: size === "small"
|
|
185
|
-
? "py-1"
|
|
186
|
-
: "py-2",
|
|
187
|
-
endAdornment ? "pr-12" : "pr-3",
|
|
188
|
-
disabled &&
|
|
189
|
-
"outline-none opacity-65 dark:opacity-60 text-surface-accent-800 dark:text-white",
|
|
190
|
-
inputClassName
|
|
191
|
-
)}
|
|
192
|
-
placeholder={focused || hasValue || !label ? placeholder : undefined}
|
|
193
|
-
autoFocus={autoFocus}
|
|
194
|
-
onFocus={() => setFocused(true)}
|
|
195
|
-
onBlur={() => setFocused(false)}
|
|
196
|
-
type={type}
|
|
197
|
-
value={type === "number" && Number.isNaN(value) ? "" : value ?? ""}
|
|
198
|
-
onChange={onChange}
|
|
199
|
-
/>
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
return (
|
|
203
|
-
<div
|
|
204
|
-
ref={ref}
|
|
205
|
-
className={cls(
|
|
206
|
-
"rounded-lg relative max-w-full",
|
|
207
|
-
invisible ? fieldBackgroundInvisibleMixin : fieldBackgroundMixin,
|
|
208
|
-
disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
|
|
209
|
-
error ? "border border-red-500 dark:border-red-600" : "",
|
|
210
|
-
{
|
|
211
|
-
"min-h-[28px]": size === "smallest",
|
|
212
|
-
"min-h-[32px]": size === "small",
|
|
213
|
-
"min-h-[40px]": size === "medium",
|
|
214
|
-
"min-h-[48px]": size === "large"
|
|
215
|
-
},
|
|
216
|
-
className
|
|
217
|
-
)}
|
|
218
|
-
style={style}
|
|
219
|
-
>
|
|
220
|
-
{label && (
|
|
221
|
-
<InputLabel
|
|
222
|
-
id={labelId}
|
|
223
|
-
htmlFor={inputId}
|
|
224
|
-
className={cls(
|
|
225
|
-
"pointer-events-none absolute",
|
|
226
|
-
size === "large" ? "top-1" : "top-[-1px]",
|
|
227
|
-
!error
|
|
228
|
-
? focused
|
|
229
|
-
? "text-primary dark:text-primary"
|
|
230
|
-
: "text-text-secondary dark:text-text-secondary-dark"
|
|
231
|
-
: "text-red-500 dark:text-red-600",
|
|
232
|
-
// A disabled field's LABEL is the one part that must
|
|
233
|
-
// stay readable — it is what tells you which field
|
|
234
|
-
// you cannot edit. `opacity-50` halved the alpha of
|
|
235
|
-
// an already-muted #737373, landing near 2.2:1 over
|
|
236
|
-
// the field fill; the label was effectively gone.
|
|
237
|
-
//
|
|
238
|
-
// The value below it keeps its own `opacity-50`,
|
|
239
|
-
// which is the correct signal: the CONTENT is
|
|
240
|
-
// inactive. Naming the field is not content.
|
|
241
|
-
disabled ? "text-text-disabled dark:text-text-disabled-dark" : ""
|
|
242
|
-
)}
|
|
243
|
-
shrink={hasValue || focused}
|
|
244
|
-
>
|
|
245
|
-
{label}
|
|
246
|
-
</InputLabel>
|
|
247
|
-
)}
|
|
248
|
-
|
|
249
|
-
{input}
|
|
250
|
-
|
|
251
|
-
{endAdornment && (
|
|
252
|
-
<div
|
|
253
|
-
className={cls(
|
|
254
|
-
"flex flex-row justify-center items-center absolute h-full right-0 top-0",
|
|
255
|
-
{
|
|
256
|
-
"mr-4": size === "large",
|
|
257
|
-
"mr-3": size === "medium",
|
|
258
|
-
"mr-2": size === "small" || size === "smallest"
|
|
259
|
-
}
|
|
260
|
-
)}
|
|
261
|
-
>
|
|
262
|
-
{endAdornment}
|
|
263
|
-
</div>
|
|
264
|
-
)}
|
|
265
|
-
</div>
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
TextField.displayName = "TextField";
|
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
import { useLayoutEffect } from "react";
|
|
4
|
-
import * as ReactDOM from "react-dom";
|
|
5
|
-
import { cls, debounce } from "../util";
|
|
6
|
-
|
|
7
|
-
type State = {
|
|
8
|
-
outerHeightStyle: number;
|
|
9
|
-
overflow?: boolean | undefined;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
function getStyleValue(value: string) {
|
|
13
|
-
return parseInt(value, 10) || 0;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const styles: {
|
|
17
|
-
shadow: React.CSSProperties;
|
|
18
|
-
} = {
|
|
19
|
-
shadow: {
|
|
20
|
-
// Visibility needed to hide the extra text area on iPads
|
|
21
|
-
visibility: "hidden",
|
|
22
|
-
// Remove from the content flow
|
|
23
|
-
position: "absolute",
|
|
24
|
-
// Ignore the scrollbar width
|
|
25
|
-
overflow: "hidden",
|
|
26
|
-
height: 0,
|
|
27
|
-
top: 0,
|
|
28
|
-
left: 0,
|
|
29
|
-
// Create a new layer, increase the isolation of the computed values
|
|
30
|
-
transform: "translateZ(0)"
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
function isEmpty(obj: State) {
|
|
35
|
-
return (
|
|
36
|
-
obj === undefined ||
|
|
37
|
-
obj === null ||
|
|
38
|
-
Object.keys(obj).length === 0 ||
|
|
39
|
-
(obj.outerHeightStyle === 0 && !obj.overflow)
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const TextareaAutosize = React.forwardRef(function TextareaAutosize(
|
|
44
|
-
props: TextareaAutosizeProps,
|
|
45
|
-
ref: React.ForwardedRef<Element>
|
|
46
|
-
) {
|
|
47
|
-
const {
|
|
48
|
-
onChange,
|
|
49
|
-
onScroll,
|
|
50
|
-
onResize,
|
|
51
|
-
maxRows,
|
|
52
|
-
minRows = 1,
|
|
53
|
-
style,
|
|
54
|
-
value,
|
|
55
|
-
onFocus,
|
|
56
|
-
onBlur,
|
|
57
|
-
sizeRef,
|
|
58
|
-
ignoreBoxSizing,
|
|
59
|
-
...other
|
|
60
|
-
} = props;
|
|
61
|
-
|
|
62
|
-
const { current: isControlled } = React.useRef(value != null);
|
|
63
|
-
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
64
|
-
const handleRef = useForkRef(ref, inputRef);
|
|
65
|
-
const shadowRef = React.useRef<HTMLTextAreaElement>(null);
|
|
66
|
-
const renders = React.useRef(0);
|
|
67
|
-
const [state, setState] = React.useState<State>({
|
|
68
|
-
outerHeightStyle: 0
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const getUpdatedState = React.useCallback(() => {
|
|
72
|
-
|
|
73
|
-
const input = inputRef.current!;
|
|
74
|
-
if (typeof window === "undefined") {
|
|
75
|
-
return {
|
|
76
|
-
outerHeightStyle: 0
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const containerWindow = window;
|
|
81
|
-
const computedStyle = containerWindow.getComputedStyle(input);
|
|
82
|
-
|
|
83
|
-
// If input's width is shrunk and it's not visible, don't sync height.
|
|
84
|
-
if (computedStyle.width === "0px") {
|
|
85
|
-
return {
|
|
86
|
-
outerHeightStyle: 0
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const sizeReferenceElement = sizeRef?.current ?? shadowRef.current!;
|
|
91
|
-
const inputShallow = shadowRef.current!;
|
|
92
|
-
|
|
93
|
-
sizeReferenceElement.style.width = computedStyle.width;
|
|
94
|
-
inputShallow.value = input.value || props.placeholder || "x";
|
|
95
|
-
if (inputShallow.value.slice(-1) === "\n") {
|
|
96
|
-
// Certain fonts which overflow the line height will cause the textarea
|
|
97
|
-
// to report a different scrollHeight depending on whether the last line
|
|
98
|
-
// is empty. Make it non-empty to avoid this issue.
|
|
99
|
-
inputShallow.value += " ";
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const boxSizing = computedStyle.boxSizing;
|
|
103
|
-
const padding =
|
|
104
|
-
getStyleValue(computedStyle.paddingBottom) + getStyleValue(computedStyle.paddingTop);
|
|
105
|
-
const border =
|
|
106
|
-
getStyleValue(computedStyle.borderBottomWidth) + getStyleValue(computedStyle.borderTopWidth);
|
|
107
|
-
const minHeight = getStyleValue(computedStyle.minHeight);
|
|
108
|
-
|
|
109
|
-
// The height of the inner content
|
|
110
|
-
const innerHeight = sizeReferenceElement.scrollHeight;
|
|
111
|
-
|
|
112
|
-
// Measure height of a textarea with a single row
|
|
113
|
-
inputShallow.value = "x";
|
|
114
|
-
const singleRowHeight = sizeReferenceElement.scrollHeight;
|
|
115
|
-
|
|
116
|
-
// The height of the outer content
|
|
117
|
-
let outerHeight = innerHeight;
|
|
118
|
-
|
|
119
|
-
if (minRows) {
|
|
120
|
-
outerHeight = Math.max(Number(minRows) * singleRowHeight, outerHeight);
|
|
121
|
-
}
|
|
122
|
-
if (maxRows) {
|
|
123
|
-
outerHeight = Math.min(Number(maxRows) * singleRowHeight, outerHeight);
|
|
124
|
-
}
|
|
125
|
-
outerHeight = Math.max(outerHeight, singleRowHeight, minHeight);
|
|
126
|
-
|
|
127
|
-
// Take the box sizing into account for applying this value as a style.
|
|
128
|
-
const outerHeightStyle = outerHeight + (!ignoreBoxSizing && boxSizing === "border-box" ? padding + border : 0);
|
|
129
|
-
|
|
130
|
-
const overflow = Math.abs(outerHeight - innerHeight) <= 1;
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
outerHeightStyle,
|
|
134
|
-
overflow
|
|
135
|
-
};
|
|
136
|
-
}, [maxRows, minRows, props.placeholder]);
|
|
137
|
-
|
|
138
|
-
const updateState = React.useCallback((prevState: State, newState: State) => {
|
|
139
|
-
const {
|
|
140
|
-
outerHeightStyle,
|
|
141
|
-
overflow
|
|
142
|
-
} = newState;
|
|
143
|
-
// Need a large enough difference to update the height.
|
|
144
|
-
// This prevents infinite rendering loop.
|
|
145
|
-
if (
|
|
146
|
-
renders.current < 20 &&
|
|
147
|
-
((outerHeightStyle > 0 &&
|
|
148
|
-
Math.abs((prevState.outerHeightStyle || 0) - outerHeightStyle) > 1) ||
|
|
149
|
-
prevState.overflow !== overflow)
|
|
150
|
-
) {
|
|
151
|
-
renders.current += 1;
|
|
152
|
-
return {
|
|
153
|
-
overflow,
|
|
154
|
-
outerHeightStyle
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
if (process.env.NODE_ENV !== "production") {
|
|
158
|
-
if (renders.current === 20) {
|
|
159
|
-
console.error(
|
|
160
|
-
[
|
|
161
|
-
"MUI: Too many re-renders. The layout is unstable.",
|
|
162
|
-
"TextareaAutosize limits the number of renders to prevent an infinite loop."
|
|
163
|
-
].join("\n")
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return prevState;
|
|
168
|
-
}, []);
|
|
169
|
-
|
|
170
|
-
const syncHeight = React.useCallback(() => {
|
|
171
|
-
const newState = getUpdatedState();
|
|
172
|
-
|
|
173
|
-
if (isEmpty(newState)) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (onResize) {
|
|
177
|
-
onResize(newState);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
setState((prevState) => {
|
|
181
|
-
return updateState(prevState, newState);
|
|
182
|
-
});
|
|
183
|
-
}, [getUpdatedState, onResize, updateState]);
|
|
184
|
-
|
|
185
|
-
const syncHeightWithFlushSync = React.useCallback(() => {
|
|
186
|
-
const newState = getUpdatedState();
|
|
187
|
-
|
|
188
|
-
if (isEmpty(newState)) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// In React 18, state updates in a ResizeObserver's callback are happening after the paint which causes flickering
|
|
193
|
-
// when doing some visual updates in it. Using flushSync ensures that the dom will be painted after the states updates happen
|
|
194
|
-
// Related issue - https://github.com/facebook/react/issues/24331
|
|
195
|
-
ReactDOM.flushSync(() => {
|
|
196
|
-
setState((prevState) => {
|
|
197
|
-
return updateState(prevState, newState);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
}, [getUpdatedState, updateState]);
|
|
201
|
-
|
|
202
|
-
React.useEffect(() => {
|
|
203
|
-
const handleResize = debounce(() => {
|
|
204
|
-
renders.current = 0;
|
|
205
|
-
|
|
206
|
-
// If the TextareaAutosize component is replaced by Suspense with a fallback, the last
|
|
207
|
-
// ResizeObserver's handler that runs because of the change in the layout is trying to
|
|
208
|
-
// access a dom node that is no longer there (as the fallback component is being shown instead).
|
|
209
|
-
if (inputRef.current) {
|
|
210
|
-
syncHeightWithFlushSync();
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
let resizeObserver: ResizeObserver;
|
|
214
|
-
|
|
215
|
-
const input = inputRef.current!;
|
|
216
|
-
const containerWindow = window;
|
|
217
|
-
if (typeof window === "undefined") {
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
containerWindow.addEventListener("resize", handleResize);
|
|
222
|
-
|
|
223
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
224
|
-
resizeObserver = new ResizeObserver(handleResize);
|
|
225
|
-
resizeObserver.observe(input);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return () => {
|
|
229
|
-
handleResize.clear();
|
|
230
|
-
containerWindow.removeEventListener("resize", handleResize);
|
|
231
|
-
if (resizeObserver) {
|
|
232
|
-
resizeObserver.disconnect();
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
}, [syncHeightWithFlushSync]);
|
|
236
|
-
|
|
237
|
-
useLayoutEffect(() => {
|
|
238
|
-
syncHeight();
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
React.useEffect(() => {
|
|
242
|
-
renders.current = 0;
|
|
243
|
-
}, [value]);
|
|
244
|
-
|
|
245
|
-
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
246
|
-
renders.current = 0;
|
|
247
|
-
|
|
248
|
-
if (!isControlled) {
|
|
249
|
-
syncHeight();
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (onChange) {
|
|
253
|
-
onChange(event);
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
return (
|
|
258
|
-
<React.Fragment>
|
|
259
|
-
<textarea
|
|
260
|
-
value={value}
|
|
261
|
-
onChange={handleChange}
|
|
262
|
-
className={props.className}
|
|
263
|
-
ref={handleRef as React.Ref<HTMLTextAreaElement>}
|
|
264
|
-
onFocus={onFocus}
|
|
265
|
-
onBlur={onBlur}
|
|
266
|
-
// Apply the rows prop to get a "correct" first SSR paint
|
|
267
|
-
rows={minRows as number}
|
|
268
|
-
style={{
|
|
269
|
-
height: state.outerHeightStyle,
|
|
270
|
-
// Need a large enough difference to allow scrolling.
|
|
271
|
-
// This prevents infinite rendering loop.
|
|
272
|
-
overflow: state.overflow ? "hidden" : undefined,
|
|
273
|
-
...style
|
|
274
|
-
}}
|
|
275
|
-
onScroll={onScroll}
|
|
276
|
-
{...other}
|
|
277
|
-
/>
|
|
278
|
-
<textarea
|
|
279
|
-
aria-hidden
|
|
280
|
-
className={cls(props.className, props.shadowClassName)}
|
|
281
|
-
readOnly
|
|
282
|
-
ref={shadowRef}
|
|
283
|
-
tabIndex={-1}
|
|
284
|
-
style={{
|
|
285
|
-
padding: 0,
|
|
286
|
-
...styles.shadow,
|
|
287
|
-
...style
|
|
288
|
-
}}
|
|
289
|
-
/>
|
|
290
|
-
</React.Fragment>
|
|
291
|
-
);
|
|
292
|
-
}) as React.FC<TextareaAutosizeProps & { ref?: React.ForwardedRef<Element> }>;
|
|
293
|
-
|
|
294
|
-
export type TextareaAutosizeProps = Omit<React.InputHTMLAttributes<HTMLTextAreaElement>, "onResize"> & {
|
|
295
|
-
|
|
296
|
-
className?: string;
|
|
297
|
-
|
|
298
|
-
shadowClassName?: string;
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Maximum number of rows to display.
|
|
302
|
-
*/
|
|
303
|
-
maxRows?: number | string;
|
|
304
|
-
/**
|
|
305
|
-
* Minimum number of rows to display.
|
|
306
|
-
* @default 1
|
|
307
|
-
*/
|
|
308
|
-
minRows?: number | string;
|
|
309
|
-
/**
|
|
310
|
-
* @ignore
|
|
311
|
-
*/
|
|
312
|
-
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
|
313
|
-
/**
|
|
314
|
-
* @ignore
|
|
315
|
-
*/
|
|
316
|
-
placeholder?: string;
|
|
317
|
-
/**
|
|
318
|
-
* @ignore
|
|
319
|
-
*/
|
|
320
|
-
style?: object;
|
|
321
|
-
/**
|
|
322
|
-
* @ignore
|
|
323
|
-
*/
|
|
324
|
-
value?: string[] | number | string;
|
|
325
|
-
|
|
326
|
-
sizeRef?: React.RefObject<HTMLDivElement>;
|
|
327
|
-
|
|
328
|
-
onScroll?: (event: React.UIEvent<HTMLTextAreaElement>) => void;
|
|
329
|
-
|
|
330
|
-
onResize?: (state: State) => void;
|
|
331
|
-
|
|
332
|
-
autoFocus?: boolean;
|
|
333
|
-
|
|
334
|
-
ignoreBoxSizing?: boolean;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function useForkRef<Instance>(
|
|
338
|
-
...refs: Array<React.Ref<Instance> | undefined>
|
|
339
|
-
): React.RefCallback<Instance> | null {
|
|
340
|
-
/**
|
|
341
|
-
* This will create a new function if the refs passed to this hook change and are all defined.
|
|
342
|
-
* This means react will call the old forkRef with `null` and the new forkRef
|
|
343
|
-
* with the ref. Cleanup naturally emerges from this behavior.
|
|
344
|
-
*/
|
|
345
|
-
return React.useMemo(() => {
|
|
346
|
-
if (refs.every((ref) => ref == null)) {
|
|
347
|
-
return null;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return (instance) => {
|
|
351
|
-
refs.forEach((ref) => {
|
|
352
|
-
setRef(ref, instance);
|
|
353
|
-
});
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
}, refs);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function setRef<T>(
|
|
360
|
-
ref: React.MutableRefObject<T | null> | ((instance: T | null) => void) | null | undefined,
|
|
361
|
-
value: T | null
|
|
362
|
-
): void {
|
|
363
|
-
if (typeof ref === "function") {
|
|
364
|
-
ref(value);
|
|
365
|
-
} else if (ref) {
|
|
366
|
-
ref.current = value;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { cls } from "../util";
|
|
3
|
-
|
|
4
|
-
export type ToggleButtonOption<T extends string = string> = {
|
|
5
|
-
value: T;
|
|
6
|
-
label: string;
|
|
7
|
-
icon?: React.ReactNode;
|
|
8
|
-
disabled?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type ToggleButtonGroupProps<T extends string = string> = {
|
|
12
|
-
/**
|
|
13
|
-
* Currently selected value
|
|
14
|
-
*/
|
|
15
|
-
value: T;
|
|
16
|
-
/**
|
|
17
|
-
* Callback when value changes
|
|
18
|
-
*/
|
|
19
|
-
onValueChange: (value: T) => void;
|
|
20
|
-
/**
|
|
21
|
-
* Options to display
|
|
22
|
-
*/
|
|
23
|
-
options: ToggleButtonOption<T>[];
|
|
24
|
-
/**
|
|
25
|
-
* Additional class names for the container
|
|
26
|
-
*/
|
|
27
|
-
className?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* A toggle button group component for selecting one option from a set.
|
|
32
|
-
* Displays options as buttons in a horizontal row with active state styling.
|
|
33
|
-
*/
|
|
34
|
-
export function ToggleButtonGroup<T extends string = string>({
|
|
35
|
-
value,
|
|
36
|
-
onValueChange,
|
|
37
|
-
options,
|
|
38
|
-
className
|
|
39
|
-
}: ToggleButtonGroupProps<T>) {
|
|
40
|
-
return (
|
|
41
|
-
<div role="group" aria-label="Toggle options" className={cls("inline-flex flex-row bg-surface-100 dark:bg-surface-900 rounded-lg p-1 gap-1", className)}>
|
|
42
|
-
{options.map((option) => (
|
|
43
|
-
<button
|
|
44
|
-
key={option.value}
|
|
45
|
-
type="button"
|
|
46
|
-
onClick={(e) => {
|
|
47
|
-
e.stopPropagation();
|
|
48
|
-
if (!option.disabled) {
|
|
49
|
-
onValueChange(option.value);
|
|
50
|
-
}
|
|
51
|
-
}}
|
|
52
|
-
disabled={option.disabled}
|
|
53
|
-
aria-pressed={value === option.value}
|
|
54
|
-
aria-disabled={option.disabled || undefined}
|
|
55
|
-
className={cls(
|
|
56
|
-
"flex flex-row items-center justify-center gap-2 py-3 px-4 rounded-md transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50",
|
|
57
|
-
value === option.value
|
|
58
|
-
? "bg-white dark:bg-surface-700 text-primary dark:text-primary-300 shadow-sm"
|
|
59
|
-
: "text-text-secondary dark:text-text-secondary-dark hover:bg-surface-200 dark:hover:bg-surface-800",
|
|
60
|
-
option.disabled && "opacity-50 cursor-not-allowed"
|
|
61
|
-
)}
|
|
62
|
-
>
|
|
63
|
-
{option.icon}
|
|
64
|
-
<span className="text-sm font-medium">{option.label}</span>
|
|
65
|
-
</button>
|
|
66
|
-
))}
|
|
67
|
-
</div>
|
|
68
|
-
);
|
|
69
|
-
}
|