@alpic-ai/ui 1.170.0 → 1.172.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.
- package/dist/components/area-chart.d.mts +3 -3
- package/dist/components/avatar.d.mts +1 -1
- package/dist/components/badge.d.mts +1 -1
- package/dist/components/bar-chart.d.mts +2 -2
- package/dist/components/bar-chart.mjs +1 -1
- package/dist/components/bar-list.d.mts +2 -2
- package/dist/components/button.d.mts +1 -1
- package/dist/components/chart-card.d.mts +2 -2
- package/dist/components/chart-legend.d.mts +3 -3
- package/dist/components/chart-primitives.d.mts +9 -10
- package/dist/components/chart-tooltip.d.mts +3 -3
- package/dist/components/donut-chart.d.mts +2 -2
- package/dist/components/editable.d.mts +101 -0
- package/dist/components/editable.mjs +509 -0
- package/dist/components/form.d.mts +10 -8
- package/dist/components/form.mjs +18 -8
- package/dist/components/heatmap-chart.d.mts +2 -2
- package/dist/components/input.d.mts +3 -1
- package/dist/components/input.mjs +5 -3
- package/dist/components/line-chart.d.mts +2 -2
- package/dist/components/select-trigger-variants.d.mts +2 -3
- package/dist/components/shimmer-text.d.mts +2 -3
- package/dist/components/skeleton.d.mts +1 -1
- package/dist/components/spinner.d.mts +2 -2
- package/dist/components/stat.d.mts +3 -3
- package/dist/components/typography.d.mts +5 -6
- package/dist/components/visually-hidden-input.d.mts +12 -0
- package/dist/components/visually-hidden-input.mjs +109 -0
- package/dist/hooks/use-as-ref.d.mts +5 -0
- package/dist/hooks/use-as-ref.mjs +12 -0
- package/dist/hooks/use-chart-theme.d.mts +3 -4
- package/dist/hooks/use-copy-to-clipboard.d.mts +2 -3
- package/dist/hooks/use-isomorphic-layout-effect.d.mts +5 -0
- package/dist/hooks/use-isomorphic-layout-effect.mjs +5 -0
- package/dist/hooks/use-lazy-ref.d.mts +6 -0
- package/dist/hooks/use-lazy-ref.mjs +9 -0
- package/dist/hooks/use-mobile.d.mts +2 -3
- package/dist/lib/chart-palette.d.mts +13 -14
- package/dist/lib/chart.d.mts +3 -4
- package/dist/lib/cn.d.mts +2 -3
- package/dist/lib/compose-refs.mjs +23 -0
- package/package.json +9 -13
- package/src/components/bar-chart.tsx +1 -1
- package/src/components/editable.tsx +871 -0
- package/src/components/form.tsx +16 -6
- package/src/components/input.tsx +8 -0
- package/src/components/visually-hidden-input.tsx +148 -0
- package/src/hooks/use-as-ref.ts +15 -0
- package/src/hooks/use-isomorphic-layout-effect.ts +5 -0
- package/src/hooks/use-lazy-ref.ts +13 -0
- package/src/lib/compose-refs.ts +38 -0
- package/src/stories/input.stories.tsx +3 -0
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { useAsRef } from "../hooks/use-as-ref";
|
|
7
|
+
import { useIsomorphicLayoutEffect } from "../hooks/use-isomorphic-layout-effect";
|
|
8
|
+
import { useLazyRef } from "../hooks/use-lazy-ref";
|
|
9
|
+
import { cn } from "../lib/cn";
|
|
10
|
+
import { useComposedRefs } from "../lib/compose-refs";
|
|
11
|
+
import { VisuallyHiddenInput } from "./visually-hidden-input";
|
|
12
|
+
|
|
13
|
+
const ROOT_NAME = "Editable";
|
|
14
|
+
const LABEL_NAME = "EditableLabel";
|
|
15
|
+
const AREA_NAME = "EditableArea";
|
|
16
|
+
const PREVIEW_NAME = "EditablePreview";
|
|
17
|
+
const INPUT_NAME = "EditableInput";
|
|
18
|
+
const TRIGGER_NAME = "EditableTrigger";
|
|
19
|
+
const TOOLBAR_NAME = "EditableToolbar";
|
|
20
|
+
const CANCEL_NAME = "EditableCancel";
|
|
21
|
+
const SUBMIT_NAME = "EditableSubmit";
|
|
22
|
+
|
|
23
|
+
type Direction = "ltr" | "rtl";
|
|
24
|
+
|
|
25
|
+
interface DivProps extends React.ComponentProps<"div"> {
|
|
26
|
+
asChild?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type RootElement = React.ComponentRef<typeof Editable>;
|
|
30
|
+
type PreviewElement = React.ComponentRef<typeof EditablePreview>;
|
|
31
|
+
type SubmitElement = React.ComponentRef<typeof EditableSubmit>;
|
|
32
|
+
type InputElement = React.ComponentRef<typeof EditableInput>;
|
|
33
|
+
|
|
34
|
+
interface StoreState {
|
|
35
|
+
value: string;
|
|
36
|
+
editing: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Store {
|
|
40
|
+
subscribe: (callback: () => void) => () => void;
|
|
41
|
+
getState: () => StoreState;
|
|
42
|
+
setState: <K extends keyof StoreState>(key: K, value: StoreState[K]) => void;
|
|
43
|
+
notify: () => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const StoreContext = React.createContext<Store | null>(null);
|
|
47
|
+
|
|
48
|
+
function useStoreContext(consumerName: string) {
|
|
49
|
+
const context = React.useContext(StoreContext);
|
|
50
|
+
if (!context) {
|
|
51
|
+
throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
|
|
52
|
+
}
|
|
53
|
+
return context;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function useStore<T>(selector: (state: StoreState) => T, ogStore?: Store | null): T {
|
|
57
|
+
const contextStore = React.useContext(StoreContext);
|
|
58
|
+
|
|
59
|
+
const store = ogStore ?? contextStore;
|
|
60
|
+
|
|
61
|
+
if (!store) {
|
|
62
|
+
throw new Error(`\`useStore\` must be used within \`${ROOT_NAME}\``);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector]);
|
|
66
|
+
|
|
67
|
+
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface EditableContextValue {
|
|
71
|
+
rootId: string;
|
|
72
|
+
inputId: string;
|
|
73
|
+
labelId: string;
|
|
74
|
+
defaultValue: string;
|
|
75
|
+
onCancel: () => void;
|
|
76
|
+
onEdit: () => void;
|
|
77
|
+
onSubmit: (value: string) => void;
|
|
78
|
+
onEnterKeyDown?: (event: KeyboardEvent) => void;
|
|
79
|
+
onEscapeKeyDown?: (event: KeyboardEvent) => void;
|
|
80
|
+
dir?: Direction;
|
|
81
|
+
maxLength?: number;
|
|
82
|
+
placeholder?: string;
|
|
83
|
+
triggerMode: "click" | "dblclick" | "focus";
|
|
84
|
+
autosize: boolean;
|
|
85
|
+
multiline: boolean;
|
|
86
|
+
disabled?: boolean;
|
|
87
|
+
readOnly?: boolean;
|
|
88
|
+
required?: boolean;
|
|
89
|
+
invalid?: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const EditableContext = React.createContext<EditableContextValue | null>(null);
|
|
93
|
+
|
|
94
|
+
function useEditableContext(consumerName: string) {
|
|
95
|
+
const context = React.useContext(EditableContext);
|
|
96
|
+
if (!context) {
|
|
97
|
+
throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
|
|
98
|
+
}
|
|
99
|
+
return context;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface EditableProps extends Omit<DivProps, "onSubmit"> {
|
|
103
|
+
id?: string;
|
|
104
|
+
defaultValue?: string;
|
|
105
|
+
value?: string;
|
|
106
|
+
onValueChange?: (value: string) => void;
|
|
107
|
+
defaultEditing?: boolean;
|
|
108
|
+
editing?: boolean;
|
|
109
|
+
onEditingChange?: (editing: boolean) => void;
|
|
110
|
+
onCancel?: () => void;
|
|
111
|
+
onEdit?: () => void;
|
|
112
|
+
onSubmit?: (value: string) => void;
|
|
113
|
+
onEscapeKeyDown?: (event: KeyboardEvent) => void;
|
|
114
|
+
onEnterKeyDown?: (event: KeyboardEvent) => void;
|
|
115
|
+
dir?: Direction;
|
|
116
|
+
maxLength?: number;
|
|
117
|
+
name?: string;
|
|
118
|
+
placeholder?: string;
|
|
119
|
+
triggerMode?: EditableContextValue["triggerMode"];
|
|
120
|
+
autosize?: boolean;
|
|
121
|
+
multiline?: boolean;
|
|
122
|
+
disabled?: boolean;
|
|
123
|
+
readOnly?: boolean;
|
|
124
|
+
required?: boolean;
|
|
125
|
+
invalid?: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function Editable(props: EditableProps) {
|
|
129
|
+
const {
|
|
130
|
+
value: valueProp,
|
|
131
|
+
defaultValue = "",
|
|
132
|
+
defaultEditing,
|
|
133
|
+
editing: editingProp,
|
|
134
|
+
onValueChange,
|
|
135
|
+
onEditingChange,
|
|
136
|
+
onCancel: onCancelProp,
|
|
137
|
+
onEdit: onEditProp,
|
|
138
|
+
onSubmit: onSubmitProp,
|
|
139
|
+
onEscapeKeyDown,
|
|
140
|
+
onEnterKeyDown,
|
|
141
|
+
dir: dirProp,
|
|
142
|
+
maxLength,
|
|
143
|
+
name,
|
|
144
|
+
placeholder,
|
|
145
|
+
triggerMode = "click",
|
|
146
|
+
asChild,
|
|
147
|
+
autosize = false,
|
|
148
|
+
multiline = false,
|
|
149
|
+
disabled,
|
|
150
|
+
required,
|
|
151
|
+
readOnly,
|
|
152
|
+
invalid,
|
|
153
|
+
className,
|
|
154
|
+
id,
|
|
155
|
+
ref,
|
|
156
|
+
...rootProps
|
|
157
|
+
} = props;
|
|
158
|
+
|
|
159
|
+
const instanceId = React.useId();
|
|
160
|
+
const rootId = id ?? instanceId;
|
|
161
|
+
|
|
162
|
+
const inputId = React.useId();
|
|
163
|
+
const labelId = React.useId();
|
|
164
|
+
|
|
165
|
+
const dir = dirProp ?? "ltr";
|
|
166
|
+
|
|
167
|
+
const previousValueRef = React.useRef(defaultValue);
|
|
168
|
+
|
|
169
|
+
const [formTrigger, setFormTrigger] = React.useState<RootElement | null>(null);
|
|
170
|
+
const composedRef = useComposedRefs(ref, (node) => setFormTrigger(node));
|
|
171
|
+
const isFormControl = formTrigger ? !!formTrigger.closest("form") : true;
|
|
172
|
+
|
|
173
|
+
const listenersRef = useLazyRef(() => new Set<() => void>());
|
|
174
|
+
const stateRef = useLazyRef<StoreState>(() => ({
|
|
175
|
+
value: valueProp ?? defaultValue,
|
|
176
|
+
editing: editingProp ?? defaultEditing ?? false,
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
const propsRef = useAsRef({
|
|
180
|
+
onValueChange,
|
|
181
|
+
onEditingChange,
|
|
182
|
+
onCancel: onCancelProp,
|
|
183
|
+
onEdit: onEditProp,
|
|
184
|
+
onSubmit: onSubmitProp,
|
|
185
|
+
onEscapeKeyDown,
|
|
186
|
+
onEnterKeyDown,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const store = React.useMemo<Store>(() => {
|
|
190
|
+
return {
|
|
191
|
+
subscribe: (cb) => {
|
|
192
|
+
listenersRef.current.add(cb);
|
|
193
|
+
return () => listenersRef.current.delete(cb);
|
|
194
|
+
},
|
|
195
|
+
getState: () => stateRef.current,
|
|
196
|
+
setState: (key, value) => {
|
|
197
|
+
if (Object.is(stateRef.current[key], value)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (key === "value" && typeof value === "string") {
|
|
202
|
+
stateRef.current.value = value;
|
|
203
|
+
propsRef.current.onValueChange?.(value);
|
|
204
|
+
} else if (key === "editing" && typeof value === "boolean") {
|
|
205
|
+
stateRef.current.editing = value;
|
|
206
|
+
propsRef.current.onEditingChange?.(value);
|
|
207
|
+
} else {
|
|
208
|
+
stateRef.current[key] = value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
store.notify();
|
|
212
|
+
},
|
|
213
|
+
notify: () => {
|
|
214
|
+
for (const cb of listenersRef.current) {
|
|
215
|
+
cb();
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}, [listenersRef, stateRef, propsRef]);
|
|
220
|
+
|
|
221
|
+
const value = useStore((state) => state.value, store);
|
|
222
|
+
|
|
223
|
+
useIsomorphicLayoutEffect(() => {
|
|
224
|
+
if (valueProp !== undefined) {
|
|
225
|
+
store.setState("value", valueProp);
|
|
226
|
+
}
|
|
227
|
+
}, [valueProp]);
|
|
228
|
+
|
|
229
|
+
useIsomorphicLayoutEffect(() => {
|
|
230
|
+
if (editingProp !== undefined) {
|
|
231
|
+
store.setState("editing", editingProp);
|
|
232
|
+
}
|
|
233
|
+
}, [editingProp]);
|
|
234
|
+
|
|
235
|
+
const onCancel = React.useCallback(() => {
|
|
236
|
+
const prevValue = previousValueRef.current;
|
|
237
|
+
store.setState("value", prevValue);
|
|
238
|
+
store.setState("editing", false);
|
|
239
|
+
propsRef.current.onCancel?.();
|
|
240
|
+
}, [store, propsRef]);
|
|
241
|
+
|
|
242
|
+
const onEdit = React.useCallback(() => {
|
|
243
|
+
const currentValue = store.getState().value;
|
|
244
|
+
previousValueRef.current = currentValue;
|
|
245
|
+
store.setState("editing", true);
|
|
246
|
+
propsRef.current.onEdit?.();
|
|
247
|
+
}, [store, propsRef]);
|
|
248
|
+
|
|
249
|
+
const onSubmit = React.useCallback(
|
|
250
|
+
(newValue: string) => {
|
|
251
|
+
store.setState("value", newValue);
|
|
252
|
+
store.setState("editing", false);
|
|
253
|
+
propsRef.current.onSubmit?.(newValue);
|
|
254
|
+
},
|
|
255
|
+
[store, propsRef],
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const contextValue = React.useMemo<EditableContextValue>(
|
|
259
|
+
() => ({
|
|
260
|
+
rootId,
|
|
261
|
+
inputId,
|
|
262
|
+
labelId,
|
|
263
|
+
defaultValue,
|
|
264
|
+
onSubmit,
|
|
265
|
+
onEdit,
|
|
266
|
+
onCancel,
|
|
267
|
+
onEscapeKeyDown,
|
|
268
|
+
onEnterKeyDown,
|
|
269
|
+
dir,
|
|
270
|
+
maxLength,
|
|
271
|
+
placeholder,
|
|
272
|
+
triggerMode,
|
|
273
|
+
autosize,
|
|
274
|
+
multiline,
|
|
275
|
+
disabled,
|
|
276
|
+
readOnly,
|
|
277
|
+
required,
|
|
278
|
+
invalid,
|
|
279
|
+
}),
|
|
280
|
+
[
|
|
281
|
+
rootId,
|
|
282
|
+
inputId,
|
|
283
|
+
labelId,
|
|
284
|
+
defaultValue,
|
|
285
|
+
onSubmit,
|
|
286
|
+
onCancel,
|
|
287
|
+
onEdit,
|
|
288
|
+
onEscapeKeyDown,
|
|
289
|
+
onEnterKeyDown,
|
|
290
|
+
dir,
|
|
291
|
+
maxLength,
|
|
292
|
+
placeholder,
|
|
293
|
+
triggerMode,
|
|
294
|
+
autosize,
|
|
295
|
+
multiline,
|
|
296
|
+
disabled,
|
|
297
|
+
required,
|
|
298
|
+
readOnly,
|
|
299
|
+
invalid,
|
|
300
|
+
],
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const RootPrimitive = asChild ? Slot : "div";
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<StoreContext.Provider value={store}>
|
|
307
|
+
<EditableContext.Provider value={contextValue}>
|
|
308
|
+
<RootPrimitive
|
|
309
|
+
data-slot="editable"
|
|
310
|
+
{...rootProps}
|
|
311
|
+
id={id}
|
|
312
|
+
ref={composedRef}
|
|
313
|
+
className={cn("flex min-w-0 flex-col gap-2", className)}
|
|
314
|
+
/>
|
|
315
|
+
{isFormControl && (
|
|
316
|
+
<VisuallyHiddenInput
|
|
317
|
+
type="hidden"
|
|
318
|
+
control={formTrigger}
|
|
319
|
+
name={name}
|
|
320
|
+
value={value}
|
|
321
|
+
disabled={disabled}
|
|
322
|
+
readOnly={readOnly}
|
|
323
|
+
required={required}
|
|
324
|
+
/>
|
|
325
|
+
)}
|
|
326
|
+
</EditableContext.Provider>
|
|
327
|
+
</StoreContext.Provider>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
interface EditableLabelProps extends React.ComponentProps<"label"> {
|
|
332
|
+
asChild?: boolean;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function EditableLabel(props: EditableLabelProps) {
|
|
336
|
+
const { asChild, className, children, ref, ...labelProps } = props;
|
|
337
|
+
const context = useEditableContext(LABEL_NAME);
|
|
338
|
+
|
|
339
|
+
const LabelPrimitive = asChild ? Slot : "label";
|
|
340
|
+
|
|
341
|
+
return (
|
|
342
|
+
<LabelPrimitive
|
|
343
|
+
data-disabled={context.disabled ? "" : undefined}
|
|
344
|
+
data-invalid={context.invalid ? "" : undefined}
|
|
345
|
+
data-required={context.required ? "" : undefined}
|
|
346
|
+
data-slot="editable-label"
|
|
347
|
+
{...labelProps}
|
|
348
|
+
ref={ref}
|
|
349
|
+
id={context.labelId}
|
|
350
|
+
htmlFor={context.inputId}
|
|
351
|
+
className={cn(
|
|
352
|
+
"text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70 data-required:after:ml-0.5 data-required:after:text-destructive data-required:after:content-['*']",
|
|
353
|
+
className,
|
|
354
|
+
)}
|
|
355
|
+
>
|
|
356
|
+
{children}
|
|
357
|
+
</LabelPrimitive>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
interface EditableAreaProps extends React.ComponentProps<"div"> {
|
|
362
|
+
asChild?: boolean;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function EditableArea(props: EditableAreaProps) {
|
|
366
|
+
const { asChild, className, ref, ...areaProps } = props;
|
|
367
|
+
const context = useEditableContext(AREA_NAME);
|
|
368
|
+
const editing = useStore((state) => state.editing);
|
|
369
|
+
|
|
370
|
+
const AreaPrimitive = asChild ? Slot : "div";
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<AreaPrimitive
|
|
374
|
+
role="group"
|
|
375
|
+
data-disabled={context.disabled ? "" : undefined}
|
|
376
|
+
data-editing={editing ? "" : undefined}
|
|
377
|
+
data-slot="editable-area"
|
|
378
|
+
dir={context.dir}
|
|
379
|
+
{...areaProps}
|
|
380
|
+
ref={ref}
|
|
381
|
+
className={cn(
|
|
382
|
+
"relative inline-block min-w-0 data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
383
|
+
className,
|
|
384
|
+
)}
|
|
385
|
+
/>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
interface EditablePreviewProps extends React.ComponentProps<"div"> {
|
|
390
|
+
asChild?: boolean;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function EditablePreview(props: EditablePreviewProps) {
|
|
394
|
+
const {
|
|
395
|
+
onClick: onClickProp,
|
|
396
|
+
onDoubleClick: onDoubleClickProp,
|
|
397
|
+
onFocus: onFocusProp,
|
|
398
|
+
onKeyDown: onKeyDownProp,
|
|
399
|
+
asChild,
|
|
400
|
+
className,
|
|
401
|
+
ref,
|
|
402
|
+
...previewProps
|
|
403
|
+
} = props;
|
|
404
|
+
|
|
405
|
+
const context = useEditableContext(PREVIEW_NAME);
|
|
406
|
+
const value = useStore((state) => state.value);
|
|
407
|
+
const editing = useStore((state) => state.editing);
|
|
408
|
+
|
|
409
|
+
const propsRef = useAsRef({
|
|
410
|
+
onClick: onClickProp,
|
|
411
|
+
onDoubleClick: onDoubleClickProp,
|
|
412
|
+
onFocus: onFocusProp,
|
|
413
|
+
onKeyDown: onKeyDownProp,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const onTrigger = React.useCallback(() => {
|
|
417
|
+
if (context.disabled || context.readOnly) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
context.onEdit();
|
|
421
|
+
}, [context.onEdit, context.disabled, context.readOnly]);
|
|
422
|
+
|
|
423
|
+
const onClick = React.useCallback(
|
|
424
|
+
(event: React.MouseEvent<PreviewElement>) => {
|
|
425
|
+
propsRef.current.onClick?.(event);
|
|
426
|
+
if (event.defaultPrevented || context.triggerMode !== "click") {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
onTrigger();
|
|
431
|
+
},
|
|
432
|
+
[propsRef, onTrigger, context.triggerMode],
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const onDoubleClick = React.useCallback(
|
|
436
|
+
(event: React.MouseEvent<PreviewElement>) => {
|
|
437
|
+
propsRef.current.onDoubleClick?.(event);
|
|
438
|
+
if (event.defaultPrevented || context.triggerMode !== "dblclick") {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
onTrigger();
|
|
443
|
+
},
|
|
444
|
+
[propsRef, onTrigger, context.triggerMode],
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const onFocus = React.useCallback(
|
|
448
|
+
(event: React.FocusEvent<PreviewElement>) => {
|
|
449
|
+
propsRef.current.onFocus?.(event);
|
|
450
|
+
if (event.defaultPrevented || context.triggerMode !== "focus") {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
onTrigger();
|
|
455
|
+
},
|
|
456
|
+
[propsRef, onTrigger, context.triggerMode],
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
const onKeyDown = React.useCallback(
|
|
460
|
+
(event: React.KeyboardEvent<PreviewElement>) => {
|
|
461
|
+
propsRef.current.onKeyDown?.(event);
|
|
462
|
+
if (event.defaultPrevented) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (event.key === "Enter") {
|
|
467
|
+
const nativeEvent = event.nativeEvent;
|
|
468
|
+
if (context.onEnterKeyDown) {
|
|
469
|
+
context.onEnterKeyDown(nativeEvent);
|
|
470
|
+
if (nativeEvent.defaultPrevented) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
onTrigger();
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
[propsRef, onTrigger, context.onEnterKeyDown],
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const PreviewPrimitive = asChild ? Slot : "div";
|
|
481
|
+
|
|
482
|
+
if (editing || context.readOnly) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
<PreviewPrimitive
|
|
488
|
+
role="button"
|
|
489
|
+
aria-disabled={context.disabled || context.readOnly}
|
|
490
|
+
data-empty={!value ? "" : undefined}
|
|
491
|
+
data-disabled={context.disabled ? "" : undefined}
|
|
492
|
+
data-readonly={context.readOnly ? "" : undefined}
|
|
493
|
+
data-slot="editable-preview"
|
|
494
|
+
tabIndex={context.disabled || context.readOnly ? undefined : 0}
|
|
495
|
+
{...previewProps}
|
|
496
|
+
ref={ref}
|
|
497
|
+
onClick={onClick}
|
|
498
|
+
onDoubleClick={onDoubleClick}
|
|
499
|
+
onFocus={onFocus}
|
|
500
|
+
onKeyDown={onKeyDown}
|
|
501
|
+
className={cn(
|
|
502
|
+
"cursor-text whitespace-pre-wrap rounded-sm border border-transparent px-1.5 py-1 transition-colors hover:bg-muted focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden data-disabled:cursor-not-allowed data-disabled:opacity-50 data-empty:text-muted-foreground/60 data-empty:italic data-readonly:cursor-default data-readonly:hover:bg-transparent",
|
|
503
|
+
className,
|
|
504
|
+
)}
|
|
505
|
+
>
|
|
506
|
+
{value || context.placeholder}
|
|
507
|
+
</PreviewPrimitive>
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
interface EditableInputProps extends React.ComponentProps<"input"> {
|
|
512
|
+
asChild?: boolean;
|
|
513
|
+
maxLength?: number;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function EditableInput(props: EditableInputProps) {
|
|
517
|
+
const {
|
|
518
|
+
onBlur: onBlurProp,
|
|
519
|
+
onChange: onChangeProp,
|
|
520
|
+
onKeyDown: onKeyDownProp,
|
|
521
|
+
asChild,
|
|
522
|
+
className,
|
|
523
|
+
disabled,
|
|
524
|
+
readOnly,
|
|
525
|
+
required,
|
|
526
|
+
maxLength,
|
|
527
|
+
ref,
|
|
528
|
+
...inputProps
|
|
529
|
+
} = props;
|
|
530
|
+
|
|
531
|
+
const context = useEditableContext(INPUT_NAME);
|
|
532
|
+
const store = useStoreContext(INPUT_NAME);
|
|
533
|
+
const value = useStore((state) => state.value);
|
|
534
|
+
const editing = useStore((state) => state.editing);
|
|
535
|
+
const inputRef = React.useRef<InputElement>(null);
|
|
536
|
+
const composedRef = useComposedRefs(ref, inputRef);
|
|
537
|
+
|
|
538
|
+
const propsRef = useAsRef({
|
|
539
|
+
onBlur: onBlurProp,
|
|
540
|
+
onChange: onChangeProp,
|
|
541
|
+
onKeyDown: onKeyDownProp,
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const isDisabled = disabled || context.disabled;
|
|
545
|
+
const isReadOnly = readOnly || context.readOnly;
|
|
546
|
+
const isRequired = required || context.required;
|
|
547
|
+
|
|
548
|
+
const onAutosize = React.useCallback(
|
|
549
|
+
(target: InputElement) => {
|
|
550
|
+
if (!context.autosize) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// `scrollHeight`/`scrollWidth` exclude the border, which `box-sizing: border-box` counts in, so the last line would clip without it.
|
|
555
|
+
const { borderTopWidth, borderBottomWidth, borderLeftWidth, borderRightWidth } = getComputedStyle(target);
|
|
556
|
+
if (target instanceof HTMLTextAreaElement) {
|
|
557
|
+
target.style.height = "0";
|
|
558
|
+
target.style.height = `${target.scrollHeight + Number.parseFloat(borderTopWidth) + Number.parseFloat(borderBottomWidth)}px`;
|
|
559
|
+
} else {
|
|
560
|
+
target.style.width = "0";
|
|
561
|
+
target.style.width = `${target.scrollWidth + Number.parseFloat(borderLeftWidth) + Number.parseFloat(borderRightWidth)}px`;
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
[context.autosize],
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
const onBlur = React.useCallback(
|
|
568
|
+
(event: React.FocusEvent<InputElement>) => {
|
|
569
|
+
if (isDisabled || isReadOnly) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
propsRef.current.onBlur?.(event);
|
|
574
|
+
if (event.defaultPrevented) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const relatedTarget = event.relatedTarget;
|
|
579
|
+
|
|
580
|
+
const isAction =
|
|
581
|
+
relatedTarget instanceof HTMLElement &&
|
|
582
|
+
(relatedTarget.closest(`[data-slot="editable-trigger"]`) ||
|
|
583
|
+
relatedTarget.closest(`[data-slot="editable-cancel"]`));
|
|
584
|
+
|
|
585
|
+
if (!isAction) {
|
|
586
|
+
context.onSubmit(value);
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
[value, context.onSubmit, propsRef, isDisabled, isReadOnly],
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
const onChange = React.useCallback(
|
|
593
|
+
(event: React.ChangeEvent<InputElement>) => {
|
|
594
|
+
if (isDisabled || isReadOnly) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
propsRef.current.onChange?.(event);
|
|
599
|
+
if (event.defaultPrevented) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
store.setState("value", event.target.value);
|
|
604
|
+
onAutosize(event.target);
|
|
605
|
+
},
|
|
606
|
+
[store, propsRef, onAutosize, isDisabled, isReadOnly],
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const onKeyDown = React.useCallback(
|
|
610
|
+
(event: React.KeyboardEvent<InputElement>) => {
|
|
611
|
+
if (isDisabled || isReadOnly) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
propsRef.current.onKeyDown?.(event);
|
|
616
|
+
if (event.defaultPrevented) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (event.key === "Escape") {
|
|
621
|
+
const nativeEvent = event.nativeEvent;
|
|
622
|
+
if (context.onEscapeKeyDown) {
|
|
623
|
+
context.onEscapeKeyDown(nativeEvent);
|
|
624
|
+
if (nativeEvent.defaultPrevented) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
context.onCancel();
|
|
629
|
+
} else if (event.key === "Enter" && !context.multiline) {
|
|
630
|
+
event.preventDefault();
|
|
631
|
+
context.onSubmit(value);
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
[
|
|
635
|
+
value,
|
|
636
|
+
context.onSubmit,
|
|
637
|
+
context.onCancel,
|
|
638
|
+
context.onEscapeKeyDown,
|
|
639
|
+
context.multiline,
|
|
640
|
+
propsRef,
|
|
641
|
+
isDisabled,
|
|
642
|
+
isReadOnly,
|
|
643
|
+
],
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
useIsomorphicLayoutEffect(() => {
|
|
647
|
+
if (!editing || isDisabled || isReadOnly || !inputRef.current) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const frameId = window.requestAnimationFrame(() => {
|
|
652
|
+
if (!inputRef.current) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
inputRef.current.focus();
|
|
657
|
+
inputRef.current.select();
|
|
658
|
+
onAutosize(inputRef.current);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
return () => {
|
|
662
|
+
window.cancelAnimationFrame(frameId);
|
|
663
|
+
};
|
|
664
|
+
}, [editing, onAutosize, isDisabled, isReadOnly]);
|
|
665
|
+
|
|
666
|
+
const InputPrimitive = asChild ? Slot : "input";
|
|
667
|
+
|
|
668
|
+
if (!editing && !isReadOnly) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return (
|
|
673
|
+
<InputPrimitive
|
|
674
|
+
aria-required={isRequired}
|
|
675
|
+
aria-invalid={context.invalid}
|
|
676
|
+
data-slot="editable-input"
|
|
677
|
+
dir={context.dir}
|
|
678
|
+
disabled={isDisabled}
|
|
679
|
+
readOnly={isReadOnly}
|
|
680
|
+
required={isRequired}
|
|
681
|
+
{...inputProps}
|
|
682
|
+
id={context.inputId}
|
|
683
|
+
aria-labelledby={context.labelId}
|
|
684
|
+
ref={composedRef}
|
|
685
|
+
maxLength={maxLength ?? context.maxLength}
|
|
686
|
+
placeholder={context.placeholder}
|
|
687
|
+
value={value}
|
|
688
|
+
onBlur={onBlur}
|
|
689
|
+
onChange={onChange}
|
|
690
|
+
onKeyDown={onKeyDown}
|
|
691
|
+
className={cn(
|
|
692
|
+
"flex rounded-sm border border-input bg-transparent px-1.5 py-1 shadow-xs transition-colors placeholder:text-muted-foreground/60 placeholder:italic focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
|
693
|
+
context.autosize && !context.multiline ? "w-auto" : "w-full",
|
|
694
|
+
className,
|
|
695
|
+
)}
|
|
696
|
+
/>
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
interface EditableTriggerProps extends React.ComponentProps<"button"> {
|
|
701
|
+
asChild?: boolean;
|
|
702
|
+
forceMount?: boolean;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function EditableTrigger(props: EditableTriggerProps) {
|
|
706
|
+
const { asChild, forceMount = false, ref, ...triggerProps } = props;
|
|
707
|
+
const context = useEditableContext(TRIGGER_NAME);
|
|
708
|
+
const editing = useStore((state) => state.editing);
|
|
709
|
+
|
|
710
|
+
const onTrigger = React.useCallback(() => {
|
|
711
|
+
if (context.disabled || context.readOnly) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
context.onEdit();
|
|
715
|
+
}, [context.disabled, context.readOnly, context.onEdit]);
|
|
716
|
+
|
|
717
|
+
const TriggerPrimitive = asChild ? Slot : "button";
|
|
718
|
+
|
|
719
|
+
if (!forceMount && (editing || context.readOnly)) {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return (
|
|
724
|
+
<TriggerPrimitive
|
|
725
|
+
type="button"
|
|
726
|
+
aria-controls={context.rootId}
|
|
727
|
+
aria-disabled={context.disabled || context.readOnly}
|
|
728
|
+
data-disabled={context.disabled ? "" : undefined}
|
|
729
|
+
data-readonly={context.readOnly ? "" : undefined}
|
|
730
|
+
data-slot="editable-trigger"
|
|
731
|
+
{...triggerProps}
|
|
732
|
+
ref={ref}
|
|
733
|
+
onClick={context.triggerMode === "click" ? onTrigger : undefined}
|
|
734
|
+
onDoubleClick={context.triggerMode === "dblclick" ? onTrigger : undefined}
|
|
735
|
+
/>
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
interface EditableToolbarProps extends React.ComponentProps<"div"> {
|
|
740
|
+
asChild?: boolean;
|
|
741
|
+
orientation?: "horizontal" | "vertical";
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function EditableToolbar(props: EditableToolbarProps) {
|
|
745
|
+
const { asChild, className, orientation = "horizontal", ref, ...toolbarProps } = props;
|
|
746
|
+
const context = useEditableContext(TOOLBAR_NAME);
|
|
747
|
+
|
|
748
|
+
const ToolbarPrimitive = asChild ? Slot : "div";
|
|
749
|
+
|
|
750
|
+
return (
|
|
751
|
+
<ToolbarPrimitive
|
|
752
|
+
role="toolbar"
|
|
753
|
+
aria-controls={context.rootId}
|
|
754
|
+
aria-orientation={orientation}
|
|
755
|
+
data-slot="editable-toolbar"
|
|
756
|
+
dir={context.dir}
|
|
757
|
+
{...toolbarProps}
|
|
758
|
+
ref={ref}
|
|
759
|
+
className={cn("flex items-center gap-2", orientation === "vertical" && "flex-col", className)}
|
|
760
|
+
/>
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
interface EditableCancelProps extends React.ComponentProps<"button"> {
|
|
765
|
+
asChild?: boolean;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function EditableCancel(props: EditableCancelProps) {
|
|
769
|
+
const { onClick: onClickProp, asChild, ref, ...cancelProps } = props;
|
|
770
|
+
const context = useEditableContext(CANCEL_NAME);
|
|
771
|
+
const editing = useStore((state) => state.editing);
|
|
772
|
+
|
|
773
|
+
const propsRef = useAsRef({
|
|
774
|
+
onClick: onClickProp,
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
const onClick = React.useCallback(
|
|
778
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
779
|
+
if (context.disabled || context.readOnly) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
propsRef.current.onClick?.(event);
|
|
784
|
+
if (event.defaultPrevented) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
context.onCancel();
|
|
789
|
+
},
|
|
790
|
+
[propsRef, context.onCancel, context.disabled, context.readOnly],
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
const CancelPrimitive = asChild ? Slot : "button";
|
|
794
|
+
|
|
795
|
+
if (!editing && !context.readOnly) {
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return (
|
|
800
|
+
<CancelPrimitive
|
|
801
|
+
type="button"
|
|
802
|
+
aria-controls={context.rootId}
|
|
803
|
+
data-slot="editable-cancel"
|
|
804
|
+
{...cancelProps}
|
|
805
|
+
onClick={onClick}
|
|
806
|
+
ref={ref}
|
|
807
|
+
/>
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
interface EditableSubmitProps extends React.ComponentProps<"button"> {
|
|
812
|
+
asChild?: boolean;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function EditableSubmit(props: EditableSubmitProps) {
|
|
816
|
+
const { onClick: onClickProp, asChild, ref, ...submitProps } = props;
|
|
817
|
+
const context = useEditableContext(SUBMIT_NAME);
|
|
818
|
+
const value = useStore((state) => state.value);
|
|
819
|
+
const editing = useStore((state) => state.editing);
|
|
820
|
+
|
|
821
|
+
const propsRef = useAsRef({
|
|
822
|
+
onClick: onClickProp,
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
const onClick = React.useCallback(
|
|
826
|
+
(event: React.MouseEvent<SubmitElement>) => {
|
|
827
|
+
if (context.disabled || context.readOnly) {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
propsRef.current.onClick?.(event);
|
|
832
|
+
if (event.defaultPrevented) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
context.onSubmit(value);
|
|
837
|
+
},
|
|
838
|
+
[propsRef, context.onSubmit, value, context.disabled, context.readOnly],
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
const SubmitPrimitive = asChild ? Slot : "button";
|
|
842
|
+
|
|
843
|
+
if (!editing && !context.readOnly) {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return (
|
|
848
|
+
<SubmitPrimitive
|
|
849
|
+
type="button"
|
|
850
|
+
aria-controls={context.rootId}
|
|
851
|
+
data-slot="editable-submit"
|
|
852
|
+
{...submitProps}
|
|
853
|
+
ref={ref}
|
|
854
|
+
onClick={onClick}
|
|
855
|
+
/>
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export {
|
|
860
|
+
Editable,
|
|
861
|
+
EditableArea,
|
|
862
|
+
EditableCancel,
|
|
863
|
+
EditableInput,
|
|
864
|
+
EditableLabel,
|
|
865
|
+
EditablePreview,
|
|
866
|
+
type EditableProps,
|
|
867
|
+
EditableSubmit,
|
|
868
|
+
EditableToolbar,
|
|
869
|
+
EditableTrigger,
|
|
870
|
+
useStore as useEditable,
|
|
871
|
+
};
|