@telegraph/helpers 0.0.13 → 0.0.15
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/CHANGELOG.md +12 -0
- package/README.md +576 -2
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.mjs +49 -37
- package/dist/esm/index.mjs.map +1 -1
- package/dist/types/components/RefToTgphRef/RefToTgphRef.d.ts +9 -0
- package/dist/types/components/RefToTgphRef/RefToTgphRef.d.ts.map +1 -1
- package/dist/types/hooks/useDeterminateState.d.ts.map +1 -1
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @telegraph/helpers
|
|
2
2
|
|
|
3
|
+
## 0.0.15
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#653](https://github.com/knocklabs/telegraph/pull/653) [`d6c6aa9`](https://github.com/knocklabs/telegraph/commit/d6c6aa9cb0e11ba96df7d7efd479c8e4652fc029) Thanks [@dependabot](https://github.com/apps/dependabot)! - chore(deps): bump react and @types/react
|
|
8
|
+
|
|
9
|
+
## 0.0.14
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#650](https://github.com/knocklabs/telegraph/pull/650) [`c7ffe1d`](https://github.com/knocklabs/telegraph/commit/c7ffe1d85a0320dec6a05b1fd386ba0092c48e37) Thanks [@kylemcd](https://github.com/kylemcd)! - fix: infinite render issue with RefToTgphRef's interaction with radix's ref
|
|
14
|
+
|
|
3
15
|
## 0.0.13
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,8 +1,582 @@
|
|
|
1
|
+
# 🛠️ Helpers
|
|
2
|
+
|
|
3
|
+
> TypeScript utilities, React components, and hooks for building robust Telegraph components.
|
|
4
|
+
|
|
1
5
|

|
|
2
6
|
|
|
3
7
|
[](https://www.npmjs.com/package/@telegraph/helpers)
|
|
8
|
+
[](https://bundlephobia.com/result?p=@telegraph/helpers)
|
|
9
|
+
[](https://github.com/knocklabs/telegraph/blob/main/LICENSE)
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @telegraph/helpers
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> **Note**: This package contains TypeScript utilities and React helpers. No stylesheets required.
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import {
|
|
23
|
+
PolymorphicProps,
|
|
24
|
+
RefToTgphRef,
|
|
25
|
+
useDeterminateState,
|
|
26
|
+
} from "@telegraph/helpers";
|
|
27
|
+
|
|
28
|
+
// Type-safe polymorphic component
|
|
29
|
+
type ButtonProps<T extends TgphElement> = PolymorphicProps<T> & {
|
|
30
|
+
variant?: "solid" | "outline";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Hook for loading states with minimum duration
|
|
34
|
+
const { state } = useDeterminateState({
|
|
35
|
+
value: isLoading ? "loading" : "idle",
|
|
36
|
+
determinateValue: "loading",
|
|
37
|
+
minDurationMs: 1000,
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API Reference
|
|
42
|
+
|
|
43
|
+
### Type Utilities
|
|
44
|
+
|
|
45
|
+
#### `Required<T, K>`
|
|
46
|
+
|
|
47
|
+
Make specific properties of a type required.
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { Required } from "@telegraph/helpers";
|
|
51
|
+
|
|
52
|
+
type User = {
|
|
53
|
+
name?: string;
|
|
54
|
+
email?: string;
|
|
55
|
+
age?: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Make name and email required
|
|
59
|
+
type UserWithRequiredFields = Required<User, "name" | "email">;
|
|
60
|
+
// Result: { name: string; email: string; age?: number; }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### `Optional<T, K>`
|
|
64
|
+
|
|
65
|
+
Make specific properties of a type optional.
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { Optional } from "@telegraph/helpers";
|
|
69
|
+
|
|
70
|
+
type User = {
|
|
71
|
+
name: string;
|
|
72
|
+
email: string;
|
|
73
|
+
age: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Make age optional
|
|
77
|
+
type UserWithOptionalAge = Optional<User, "age">;
|
|
78
|
+
// Result: { name: string; email: string; age?: number; }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### `RemappedOmit<T, K>`
|
|
82
|
+
|
|
83
|
+
Enhanced version of TypeScript's `Omit` that ensures complete field removal.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { RemappedOmit } from "@telegraph/helpers";
|
|
87
|
+
|
|
88
|
+
type User = {
|
|
89
|
+
id: string;
|
|
90
|
+
name: string;
|
|
91
|
+
password: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Remove sensitive fields
|
|
95
|
+
type PublicUser = RemappedOmit<User, "password">;
|
|
96
|
+
// Result: { id: string; name: string; }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Polymorphic Component Types
|
|
100
|
+
|
|
101
|
+
#### `TgphElement`
|
|
102
|
+
|
|
103
|
+
Type alias for `React.ElementType` used throughout Telegraph.
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { TgphElement } from "@telegraph/helpers";
|
|
107
|
+
|
|
108
|
+
type ComponentProps<T extends TgphElement> = {
|
|
109
|
+
as?: T;
|
|
110
|
+
children: React.ReactNode;
|
|
111
|
+
};
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### `TgphComponentProps<T>`
|
|
115
|
+
|
|
116
|
+
Type alias for `React.ComponentProps<T>`.
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
import { TgphComponentProps } from "@telegraph/helpers";
|
|
120
|
+
|
|
121
|
+
type ButtonProps = TgphComponentProps<"button"> & {
|
|
122
|
+
variant?: string;
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### `AsProp<C>`
|
|
127
|
+
|
|
128
|
+
Type for the `as` prop used in polymorphic components.
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { AsProp } from "@telegraph/helpers";
|
|
132
|
+
|
|
133
|
+
type BaseProps = AsProp<React.ElementType> & {
|
|
134
|
+
children: React.ReactNode;
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### `PolymorphicProps<E>`
|
|
139
|
+
|
|
140
|
+
Complete props type for polymorphic components.
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { PolymorphicProps, TgphElement } from "@telegraph/helpers";
|
|
144
|
+
|
|
145
|
+
type BoxProps<T extends TgphElement> = PolymorphicProps<T> & {
|
|
146
|
+
padding?: string;
|
|
147
|
+
margin?: string;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const Box = <T extends TgphElement = "div">({
|
|
151
|
+
as,
|
|
152
|
+
padding,
|
|
153
|
+
margin,
|
|
154
|
+
...props
|
|
155
|
+
}: BoxProps<T>) => {
|
|
156
|
+
const Component = as || "div";
|
|
157
|
+
return <Component style={{ padding, margin }} {...props} />;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Usage examples:
|
|
161
|
+
<Box>Default div</Box>
|
|
162
|
+
<Box as="section">Semantic section</Box>
|
|
163
|
+
<Box as="button" onClick={() => {}}>Button element</Box>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### `PolymorphicPropsWithTgphRef<E, R>`
|
|
167
|
+
|
|
168
|
+
Polymorphic props with Telegraph-specific ref handling.
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import { PolymorphicPropsWithTgphRef, TgphElement } from "@telegraph/helpers";
|
|
172
|
+
|
|
173
|
+
type InputProps<T extends TgphElement> = PolymorphicPropsWithTgphRef<
|
|
174
|
+
T,
|
|
175
|
+
HTMLInputElement
|
|
176
|
+
> & {
|
|
177
|
+
placeholder?: string;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const Input = <T extends TgphElement = "input">({
|
|
181
|
+
as,
|
|
182
|
+
tgphRef,
|
|
183
|
+
...props
|
|
184
|
+
}: InputProps<T>) => {
|
|
185
|
+
const Component = as || "input";
|
|
186
|
+
return <Component ref={tgphRef} {...props} />;
|
|
187
|
+
};
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### `PropsWithAs<C, P>`
|
|
191
|
+
|
|
192
|
+
Utility for combining element props with custom props.
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
import { PropsWithAs } from "@telegraph/helpers";
|
|
196
|
+
|
|
197
|
+
type CustomButtonProps = PropsWithAs<
|
|
198
|
+
"button",
|
|
199
|
+
{
|
|
200
|
+
variant: "primary" | "secondary";
|
|
201
|
+
loading?: boolean;
|
|
202
|
+
}
|
|
203
|
+
>;
|
|
204
|
+
|
|
205
|
+
const CustomButton = ({
|
|
206
|
+
as: Component = "button",
|
|
207
|
+
variant,
|
|
208
|
+
loading,
|
|
209
|
+
...props
|
|
210
|
+
}: CustomButtonProps) => {
|
|
211
|
+
return <Component disabled={loading} data-variant={variant} {...props} />;
|
|
212
|
+
};
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## React Components
|
|
216
|
+
|
|
217
|
+
### `RefToTgphRef`
|
|
218
|
+
|
|
219
|
+
Component for handling ref forwarding between external libraries (like Radix) and Telegraph components.
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
import * as Popover from "@radix-ui/react-popover";
|
|
223
|
+
import { Button } from "@telegraph/button";
|
|
224
|
+
import { RefToTgphRef } from "@telegraph/helpers";
|
|
225
|
+
|
|
226
|
+
// Radix expects a `ref` prop, but Telegraph uses `tgphRef`
|
|
227
|
+
<Popover.Trigger asChild>
|
|
228
|
+
<RefToTgphRef>
|
|
229
|
+
<Button>Open Popover</Button>
|
|
230
|
+
</RefToTgphRef>
|
|
231
|
+
</Popover.Trigger>;
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### Use Cases
|
|
235
|
+
|
|
236
|
+
**With Radix UI Primitives:**
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
import * as Dialog from "@radix-ui/react-dialog";
|
|
240
|
+
import { Button } from "@telegraph/button";
|
|
241
|
+
import { RefToTgphRef } from "@telegraph/helpers";
|
|
242
|
+
|
|
243
|
+
const DialogExample = () => (
|
|
244
|
+
<Dialog.Root>
|
|
245
|
+
<Dialog.Trigger asChild>
|
|
246
|
+
<RefToTgphRef>
|
|
247
|
+
<Button>Open Dialog</Button>
|
|
248
|
+
</RefToTgphRef>
|
|
249
|
+
</Dialog.Trigger>
|
|
250
|
+
<Dialog.Content>{/* Dialog content */}</Dialog.Content>
|
|
251
|
+
</Dialog.Root>
|
|
252
|
+
);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**With Form Libraries:**
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
import { RefToTgphRef } from "@telegraph/helpers";
|
|
259
|
+
import { Input } from "@telegraph/input";
|
|
260
|
+
import { Controller, useForm } from "react-hook-form";
|
|
261
|
+
|
|
262
|
+
const FormExample = () => {
|
|
263
|
+
const { control } = useForm();
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<Controller
|
|
267
|
+
name="email"
|
|
268
|
+
control={control}
|
|
269
|
+
render={({ field }) => (
|
|
270
|
+
<RefToTgphRef>
|
|
271
|
+
<Input {...field} placeholder="Email" />
|
|
272
|
+
</RefToTgphRef>
|
|
273
|
+
)}
|
|
274
|
+
/>
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## React Hooks
|
|
280
|
+
|
|
281
|
+
### `useDeterminateState`
|
|
282
|
+
|
|
283
|
+
Hook for managing state transitions with minimum duration guarantees.
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
import { useDeterminateState } from "@telegraph/helpers";
|
|
287
|
+
|
|
288
|
+
const useDeterminateState = <T>({
|
|
289
|
+
value: T; // Current value
|
|
290
|
+
determinateValue: T; // Value that should persist for minimum duration
|
|
291
|
+
minDurationMs?: number; // Minimum duration (default: 1000ms)
|
|
292
|
+
}): T
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### Basic Usage
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
import { useDeterminateState } from "@telegraph/helpers";
|
|
299
|
+
import { useState } from "react";
|
|
300
|
+
|
|
301
|
+
const LoadingButton = () => {
|
|
302
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
303
|
+
|
|
304
|
+
// Ensure loading state persists for at least 1 second
|
|
305
|
+
const buttonState = useDeterminateState({
|
|
306
|
+
value: isLoading ? "loading" : "idle",
|
|
307
|
+
determinateValue: "loading",
|
|
308
|
+
minDurationMs: 1000,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const handleClick = async () => {
|
|
312
|
+
setIsLoading(true);
|
|
313
|
+
try {
|
|
314
|
+
await someAsyncOperation();
|
|
315
|
+
} finally {
|
|
316
|
+
setIsLoading(false); // Will transition back after minimum duration
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<button onClick={handleClick} disabled={buttonState === "loading"}>
|
|
322
|
+
{buttonState === "loading" ? "Loading..." : "Click me"}
|
|
323
|
+
</button>
|
|
324
|
+
);
|
|
325
|
+
};
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
#### Advanced Usage
|
|
329
|
+
|
|
330
|
+
```tsx
|
|
331
|
+
import { useDeterminateState } from "@telegraph/helpers";
|
|
332
|
+
|
|
333
|
+
type FormState = "idle" | "submitting" | "success" | "error";
|
|
334
|
+
|
|
335
|
+
const FormWithFeedback = () => {
|
|
336
|
+
const [formState, setFormState] = useState<FormState>("idle");
|
|
337
|
+
|
|
338
|
+
// Ensure success/error states are visible for at least 2 seconds
|
|
339
|
+
const displayState = useDeterminateState({
|
|
340
|
+
value: formState,
|
|
341
|
+
determinateValue:
|
|
342
|
+
formState === "success" || formState === "error" ? formState : "idle",
|
|
343
|
+
minDurationMs: 2000,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const handleSubmit = async () => {
|
|
347
|
+
setFormState("submitting");
|
|
348
|
+
try {
|
|
349
|
+
await submitForm();
|
|
350
|
+
setFormState("success");
|
|
351
|
+
// Auto-reset after success is shown
|
|
352
|
+
setTimeout(() => setFormState("idle"), 2500);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
setFormState("error");
|
|
355
|
+
setTimeout(() => setFormState("idle"), 2500);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<form onSubmit={handleSubmit}>
|
|
361
|
+
<button disabled={displayState === "submitting"}>
|
|
362
|
+
{displayState === "submitting" && "Submitting..."}
|
|
363
|
+
{displayState === "success" && "✓ Saved!"}
|
|
364
|
+
{displayState === "error" && "✗ Error"}
|
|
365
|
+
{displayState === "idle" && "Save"}
|
|
366
|
+
</button>
|
|
367
|
+
</form>
|
|
368
|
+
);
|
|
369
|
+
};
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Advanced Usage
|
|
373
|
+
|
|
374
|
+
### Creating Polymorphic Components
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
import { forwardRef } from "react";
|
|
378
|
+
import { PolymorphicPropsWithTgphRef, TgphElement } from "@telegraph/helpers";
|
|
379
|
+
|
|
380
|
+
type TextProps<T extends TgphElement> = PolymorphicPropsWithTgphRef<T, HTMLElement> & {
|
|
381
|
+
size?: "small" | "medium" | "large";
|
|
382
|
+
weight?: "normal" | "bold";
|
|
383
|
+
color?: string;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const Text = forwardRef<HTMLElement, TextProps<TgphElement>>(
|
|
387
|
+
<T extends TgphElement = "span">({
|
|
388
|
+
as,
|
|
389
|
+
size = "medium",
|
|
390
|
+
weight = "normal",
|
|
391
|
+
color,
|
|
392
|
+
tgphRef,
|
|
393
|
+
style,
|
|
394
|
+
...props
|
|
395
|
+
}: TextProps<T>, ref) => {
|
|
396
|
+
const Component = as || "span";
|
|
397
|
+
|
|
398
|
+
return (
|
|
399
|
+
<Component
|
|
400
|
+
ref={tgphRef || ref}
|
|
401
|
+
style={{
|
|
402
|
+
fontSize: size === "small" ? "12px" : size === "large" ? "18px" : "14px",
|
|
403
|
+
fontWeight: weight === "bold" ? "bold" : "normal",
|
|
404
|
+
color,
|
|
405
|
+
...style,
|
|
406
|
+
}}
|
|
407
|
+
{...props}
|
|
408
|
+
/>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// Usage examples:
|
|
414
|
+
<Text>Default span text</Text>
|
|
415
|
+
<Text as="p" size="large" weight="bold">Paragraph text</Text>
|
|
416
|
+
<Text as="h1" color="blue">Heading text</Text>
|
|
417
|
+
<Text as={Link} href="/about">Link text</Text>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Type-Safe Component APIs
|
|
421
|
+
|
|
422
|
+
```tsx
|
|
423
|
+
import { Required, Optional, TgphComponentProps } from "@telegraph/helpers";
|
|
424
|
+
|
|
425
|
+
// Base props with all optional styling
|
|
426
|
+
type BaseCardProps = {
|
|
427
|
+
padding?: string;
|
|
428
|
+
shadow?: boolean;
|
|
429
|
+
rounded?: boolean;
|
|
430
|
+
background?: string;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// Card variant that requires certain props
|
|
434
|
+
type PrimaryCardProps = Required<BaseCardProps, "background"> & {
|
|
435
|
+
variant: "primary";
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// Card variant with some optional props
|
|
439
|
+
type SecondaryCardProps = Optional<BaseCardProps, "padding"> & {
|
|
440
|
+
variant: "secondary";
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
type CardProps = (PrimaryCardProps | SecondaryCardProps) & {
|
|
444
|
+
children: React.ReactNode;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const Card = ({ variant, children, ...props }: CardProps) => {
|
|
448
|
+
if (variant === "primary") {
|
|
449
|
+
// TypeScript knows `background` is required here
|
|
450
|
+
return <div style={{ background: props.background }} />;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Handle secondary variant
|
|
454
|
+
return <div>{children}</div>;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// Usage:
|
|
458
|
+
<Card variant="primary" background="white">Content</Card> // ✓ Valid
|
|
459
|
+
<Card variant="primary">Content</Card> // ✗ TypeScript error - missing background
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Integration with External Libraries
|
|
463
|
+
|
|
464
|
+
```tsx
|
|
465
|
+
import * as RadixPopover from "@radix-ui/react-popover";
|
|
466
|
+
import { Button } from "@telegraph/button";
|
|
467
|
+
import { PolymorphicProps, RefToTgphRef } from "@telegraph/helpers";
|
|
468
|
+
|
|
469
|
+
type PopoverProps = PolymorphicProps<"div"> & {
|
|
470
|
+
trigger: React.ReactNode;
|
|
471
|
+
content: React.ReactNode;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const Popover = ({ trigger, content, ...props }: PopoverProps) => (
|
|
475
|
+
<RadixPopover.Root>
|
|
476
|
+
<RadixPopover.Trigger asChild>
|
|
477
|
+
<RefToTgphRef>{trigger}</RefToTgphRef>
|
|
478
|
+
</RadixPopover.Trigger>
|
|
479
|
+
<RadixPopover.Content {...props}>{content}</RadixPopover.Content>
|
|
480
|
+
</RadixPopover.Root>
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
// Usage:
|
|
484
|
+
<Popover
|
|
485
|
+
trigger={<Button>Open Menu</Button>}
|
|
486
|
+
content={<div>Popover content</div>}
|
|
487
|
+
/>;
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## TypeScript
|
|
491
|
+
|
|
492
|
+
### Utility Type Examples
|
|
493
|
+
|
|
494
|
+
```tsx
|
|
495
|
+
import { Optional, RemappedOmit, Required } from "@telegraph/helpers";
|
|
496
|
+
|
|
497
|
+
// API response type
|
|
498
|
+
type User = {
|
|
499
|
+
id: string;
|
|
500
|
+
name: string;
|
|
501
|
+
email: string;
|
|
502
|
+
avatar?: string;
|
|
503
|
+
createdAt: Date;
|
|
504
|
+
updatedAt: Date;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// Create user payload (remove server-generated fields)
|
|
508
|
+
type CreateUserPayload = RemappedOmit<User, "id" | "createdAt" | "updatedAt">;
|
|
509
|
+
|
|
510
|
+
// Update user payload (make all fields optional except id)
|
|
511
|
+
type UpdateUserPayload = Required<
|
|
512
|
+
Optional<User, "name" | "email" | "avatar">,
|
|
513
|
+
"id"
|
|
514
|
+
>;
|
|
515
|
+
|
|
516
|
+
// Public user (safe for client-side)
|
|
517
|
+
type PublicUser = RemappedOmit<User, "email">;
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Component Prop Patterns
|
|
521
|
+
|
|
522
|
+
```tsx
|
|
523
|
+
import {
|
|
524
|
+
PolymorphicProps,
|
|
525
|
+
TgphComponentProps,
|
|
526
|
+
TgphElement,
|
|
527
|
+
} from "@telegraph/helpers";
|
|
528
|
+
|
|
529
|
+
// Pattern 1: Simple polymorphic component
|
|
530
|
+
type SimpleProps<T extends TgphElement> = PolymorphicProps<T> & {
|
|
531
|
+
variant?: string;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// Pattern 2: Component with ref forwarding
|
|
535
|
+
type WithRefProps<T extends TgphElement> = PolymorphicPropsWithTgphRef<
|
|
536
|
+
T,
|
|
537
|
+
HTMLElement
|
|
538
|
+
> & {
|
|
539
|
+
variant?: string;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// Pattern 3: Extending native element props
|
|
543
|
+
type NativeProps = TgphComponentProps<"button"> & {
|
|
544
|
+
loading?: boolean;
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// Pattern 4: Conditional props based on variant
|
|
548
|
+
type ConditionalProps<T extends TgphElement> = PolymorphicProps<T> &
|
|
549
|
+
(
|
|
550
|
+
| { variant: "icon"; icon: React.ComponentType; label?: never }
|
|
551
|
+
| { variant: "text"; label: string; icon?: never }
|
|
552
|
+
);
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
## Best Practices
|
|
556
|
+
|
|
557
|
+
### Type Utility Guidelines
|
|
558
|
+
|
|
559
|
+
1. **Use `RemappedOmit` over `Omit`**: Ensures complete field removal
|
|
560
|
+
2. **Prefer `Required`/`Optional` for partial updates**: More explicit than `Partial`
|
|
561
|
+
3. **Use `PolymorphicProps` for flexible components**: Enables `as` prop pattern
|
|
562
|
+
4. **Always provide default `TgphElement`**: Improves TypeScript inference
|
|
563
|
+
|
|
564
|
+
### Component Development
|
|
565
|
+
|
|
566
|
+
1. **Use `RefToTgphRef` with external libraries**: Ensures ref compatibility
|
|
567
|
+
2. **Implement `useDeterminateState` for loading states**: Improves UX with minimum durations
|
|
568
|
+
3. **Type polymorphic components properly**: Use appropriate helper types
|
|
569
|
+
4. **Test type constraints**: Verify TypeScript catches errors correctly
|
|
570
|
+
|
|
571
|
+
## References
|
|
572
|
+
|
|
573
|
+
- [TypeScript Handbook - Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html)
|
|
574
|
+
- [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/)
|
|
575
|
+
|
|
576
|
+
## Contributing
|
|
4
577
|
|
|
5
|
-
|
|
6
|
-
> Internal helpers for use in telegraph
|
|
578
|
+
See our [Contributing Guide](../../CONTRIBUTING.md) for more details.
|
|
7
579
|
|
|
580
|
+
## License
|
|
8
581
|
|
|
582
|
+
MIT License - see [LICENSE](../../LICENSE) for details.
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const f=require("react"),p=Symbol.for("react.forward_ref"),R=(s,l)=>{const e={...l};for(const c in l){const t=s[c],r=l[c];/^on[A-Z]/.test(c)?t&&r?e[c]=(...o)=>{r(...o),t(...o)}:t&&(e[c]=t):c==="style"?e[c]={...t,...r}:c==="className"&&(e[c]=[t,r].filter(Boolean).join(" "))}return{...s,...e}},y=({children:s,...l},e)=>s?f.Children.toArray(s).map(t=>{if(f.isValidElement(t)){const r=t,n=r.$$typeof,o=r.type.$$typeof,u=r.props,i=u.tgphRef;return n===p||o===p?f.cloneElement(r,{...R(l,u),tgphRef:i||e,ref:i||e}):f.cloneElement(r,{...R(l,u),tgphRef:i||e})}return t}):null,m=f.forwardRef(({children:s,...l},e)=>{const c=f.useRef(e),t=f.useRef(null);f.useEffect(()=>{const n=c.current,o=t.current;n!==e&&o&&(typeof n=="function"?n(null):n&&(n.current=null),typeof e=="function"?e(o):e&&(e.current=o)),c.current=e});const r=f.useCallback(n=>{t.current=n;const o=c.current;typeof o=="function"?o(n):o&&(o.current=n)},[]);return y({children:s,...l},r)}),T=({value:s,determinateValue:l,minDurationMs:e=1e3})=>{const[c,t]=f.useState(s),r=f.useRef(null),n=f.useRef(null),o=()=>{r.current&&(clearTimeout(r.current),r.current=null)},u=f.useCallback(()=>{if(s===l)o(),t(l),n.current=Date.now();else if(n.current!==null){const i=Date.now()-n.current,a=e-i;a>0?(o(),r.current=setTimeout(()=>{t(s),n.current=null},a)):(t(s),n.current=null)}else t(s)},[s,l,e]);return f.useEffect(()=>(u(),o),[s,u]),c};exports.RefToTgphRef=m;exports.useDeterminateState=T;
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/components/RefToTgphRef/RefToTgphRef.tsx","../../src/hooks/useDeterminateState.ts"],"sourcesContent":["//\n// When interacting with components like a Radix.Trigger, they assume that\n// our components accept a `ref` prop. This is not the case with our components\n// because we use the `tgphRef` prop instead. To work around this, we can create\n// a new component that accepts a `ref` prop and forwards it to the `tgphRef`\n// prop.\n//\nimport React from \"react\";\n\nconst FORWARD_REF_SYMBOL = Symbol.for(\"react.forward_ref\");\n\ntype ApplyRefPropsProps = {\n children: React.ReactNode;\n};\n\ntype Child = React.ReactElement & {\n $$typeof: symbol;\n type: { $$typeof: symbol };\n};\n\n// Merge props the same way that radix slot does\n// https://github.com/radix-ui/primitives/blob/main/packages/react/slot/src/Slot.tsx\nconst mergeProps = (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n slotProps: Record<string, any>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n childProps: Record<string, any>,\n) => {\n // all child props should override\n const overrideProps = { ...childProps };\n\n for (const propName in childProps) {\n const slotPropValue = slotProps[propName];\n const childPropValue = childProps[propName];\n\n const isHandler = /^on[A-Z]/.test(propName);\n if (isHandler) {\n // if the handler exists on both, we compose them\n if (slotPropValue && childPropValue) {\n overrideProps[propName] = (...args: unknown[]) => {\n childPropValue(...args);\n slotPropValue(...args);\n };\n }\n // but if it exists only on the slot, we use only this one\n else if (slotPropValue) {\n overrideProps[propName] = slotPropValue;\n }\n }\n // if it's `style`, we merge them\n else if (propName === \"style\") {\n overrideProps[propName] = { ...slotPropValue, ...childPropValue };\n } else if (propName === \"className\") {\n overrideProps[propName] = [slotPropValue, childPropValue]\n .filter(Boolean)\n .join(\" \");\n }\n }\n\n return { ...slotProps, ...overrideProps };\n};\n\nconst applyRefProps = (\n { children, ...props }: ApplyRefPropsProps,\n ref: React.Ref<unknown>,\n) => {\n if (!children) return null;\n const childrenArray = React.Children.toArray(children);\n return childrenArray.map((child) => {\n if (React.isValidElement(child)) {\n const validChild = child as Child;\n const $$typeof = validChild.$$typeof;\n const $$typeofType = validChild.type.$$typeof;\n const childProps = validChild.props;\n const tgphRef = childProps.tgphRef;\n\n // If we detect that the child is a forwardRef, we to pass the `ref` prop\n // to it so that components that exist outside of our library can still\n // receive the ref. We do it this way in order to avoid this warning:\n // \"Function components cannot be given refs. Attempts to access this ref will fail.\n // Did you mean to use React.forwardRef()\"\n if (\n $$typeof === FORWARD_REF_SYMBOL ||\n $$typeofType === FORWARD_REF_SYMBOL\n ) {\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps),\n tgphRef: tgphRef || ref,\n ref: tgphRef || ref,\n });\n }\n\n // Otherwise, we can just pass the `tgphRef` prop to the child.\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps),\n tgphRef: tgphRef || ref,\n });\n }\n\n // If the child is not a valid element, we can just return it.\n return child;\n });\n};\n\n// We can't generate the type of the ref because it's a forwardRef so any\n// works for this use case\n//\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst RefToTgphRef = React.forwardRef<any, any>(\n ({ children: childrenProp, ...props }, ref) => {\n return applyRefProps({ children: childrenProp, ...props }, ref);\n },\n);\n\nexport { RefToTgphRef };\n","/*\n * useDeterminateState\n *\n * A hook that returns a state transitioning to a determinate value after a minimum duration.\n * For example, you could use this hook with a button that transitions into a \"loading\" state,\n * ensuring it remains in the \"loading\" state for at least 1000ms. This provides clear feedback\n * to the user that the action is being processed.\n *\n */\nimport React from \"react\";\n\ntype UseDeterminateStateParams<T> = {\n value: T;\n determinateValue: T;\n minDurationMs?: number;\n};\n\nconst useDeterminateState = <T>({\n value,\n determinateValue,\n minDurationMs = 1000,\n}: UseDeterminateStateParams<T>): T => {\n const [state, setState] = React.useState<T>(value);\n const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);\n const startTimeRef = React.useRef<number | null>(null);\n\n const clearExistingTimeout = () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n };\n\n const handleTransition = React.useCallback(() => {\n if (value === determinateValue) {\n clearExistingTimeout();\n setState(determinateValue);\n startTimeRef.current = Date.now();\n } else if (startTimeRef.current !== null) {\n const elapsedTime = Date.now() - startTimeRef.current;\n const remainingTime = minDurationMs - elapsedTime;\n\n if (remainingTime > 0) {\n clearExistingTimeout();\n timeoutRef.current = setTimeout(() => {\n setState(value);\n startTimeRef.current = null;\n }, remainingTime);\n } else {\n setState(value);\n startTimeRef.current = null;\n }\n } else {\n setState(value);\n }\n }, [value, determinateValue, minDurationMs]);\n\n React.useEffect(() => {\n handleTransition();\n return clearExistingTimeout;\n }, [value, handleTransition]);\n\n return state;\n};\n\nexport { useDeterminateState };\n"],"names":["FORWARD_REF_SYMBOL","mergeProps","slotProps","childProps","overrideProps","propName","slotPropValue","childPropValue","args","applyRefProps","children","props","ref","React","child","validChild","$$typeof","$$typeofType","tgphRef","RefToTgphRef","childrenProp","useDeterminateState","value","determinateValue","minDurationMs","state","setState","timeoutRef","startTimeRef","clearExistingTimeout","handleTransition","elapsedTime","remainingTime"],"mappings":"yGASMA,EAAqB,OAAO,IAAI,mBAAmB,EAanDC,EAAa,CAEjBC,EAEAC,IACG,CAEG,MAAAC,EAAgB,CAAE,GAAGD,CAAW,EAEtC,UAAWE,KAAYF,EAAY,CAC3B,MAAAG,EAAgBJ,EAAUG,CAAQ,EAClCE,EAAiBJ,EAAWE,CAAQ,EAExB,WAAW,KAAKA,CAAQ,EAGpCC,GAAiBC,EACLH,EAAAC,CAAQ,EAAI,IAAIG,IAAoB,CAChDD,EAAe,GAAGC,CAAI,EACtBF,EAAc,GAAGE,CAAI,CACvB,EAGOF,IACPF,EAAcC,CAAQ,EAAIC,GAIrBD,IAAa,QACpBD,EAAcC,CAAQ,EAAI,CAAE,GAAGC,EAAe,GAAGC,CAAe,EACvDF,IAAa,cACRD,EAAAC,CAAQ,EAAI,CAACC,EAAeC,CAAc,EACrD,OAAO,OAAO,EACd,KAAK,GAAG,EACb,CAGF,MAAO,CAAE,GAAGL,EAAW,GAAGE,CAAc,CAC1C,EAEMK,EAAgB,CACpB,CAAE,SAAAC,EAAU,GAAGC,CAAA,EACfC,IAEKF,EACiBG,EAAM,SAAS,QAAQH,CAAQ,EAChC,IAAKI,GAAU,CAC9B,GAAAD,EAAM,eAAeC,CAAK,EAAG,CAC/B,MAAMC,EAAaD,EACbE,EAAWD,EAAW,SACtBE,EAAeF,EAAW,KAAK,SAC/BZ,EAAaY,EAAW,MACxBG,EAAUf,EAAW,QAQzB,OAAAa,IAAahB,GACbiB,IAAiBjB,EAEVa,EAAM,aAAaE,EAAY,CACpC,GAAGd,EAAWU,EAAOR,CAAU,EAC/B,QAASe,GAAWN,EACpB,IAAKM,GAAWN,CAAA,CACjB,EAIIC,EAAM,aAAaE,EAAY,CACpC,GAAGd,EAAWU,EAAOR,CAAU,EAC/B,QAASe,GAAWN,CAAA,CACrB,CAAA,CAII,OAAAE,CAAA,CACR,EAnCqB,KA0ClBK,EAAeN,EAAM,WACzB,CAAC,CAAE,SAAUO,EAAc,GAAGT,CAAA,EAASC,IAC9BH,EAAc,CAAE,SAAUW,EAAc,GAAGT,GAASC,CAAG,CAElE,EC/FMS,EAAsB,CAAI,CAC9B,MAAAC,EACA,iBAAAC,EACA,cAAAC,EAAgB,GAClB,IAAuC,CACrC,KAAM,CAACC,EAAOC,CAAQ,EAAIb,EAAM,SAAYS,CAAK,EAC3CK,EAAad,EAAM,OAA8B,IAAI,EACrDe,EAAef,EAAM,OAAsB,IAAI,EAE/CgB,EAAuB,IAAM,CAC7BF,EAAW,UACb,aAAaA,EAAW,OAAO,EAC/BA,EAAW,QAAU,KAEzB,EAEMG,EAAmBjB,EAAM,YAAY,IAAM,CAC/C,GAAIS,IAAUC,EACSM,EAAA,EACrBH,EAASH,CAAgB,EACZK,EAAA,QAAU,KAAK,IAAI,UACvBA,EAAa,UAAY,KAAM,CACxC,MAAMG,EAAc,KAAK,IAAI,EAAIH,EAAa,QACxCI,EAAgBR,EAAgBO,EAElCC,EAAgB,GACGH,EAAA,EACVF,EAAA,QAAU,WAAW,IAAM,CACpCD,EAASJ,CAAK,EACdM,EAAa,QAAU,MACtBI,CAAa,IAEhBN,EAASJ,CAAK,EACdM,EAAa,QAAU,KACzB,MAEAF,EAASJ,CAAK,CAEf,EAAA,CAACA,EAAOC,EAAkBC,CAAa,CAAC,EAE3C,OAAAX,EAAM,UAAU,KACGiB,EAAA,EACVD,GACN,CAACP,EAAOQ,CAAgB,CAAC,EAErBL,CACT"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/components/RefToTgphRef/RefToTgphRef.tsx","../../src/hooks/useDeterminateState.ts"],"sourcesContent":["/**\n * RefToTgphRef Component\n *\n * PURPOSE:\n * ========\n * This component bridges the gap between third-party libraries (like Radix UI) and\n * Telegraph components. Third-party libraries expect components to accept a standard\n * React `ref` prop, but Telegraph components use a custom `tgphRef` prop instead.\n *\n * Without this adapter, using Telegraph components with libraries like Radix would fail\n * because Radix would try to pass a `ref` that Telegraph components wouldn't receive.\n *\n * EXAMPLE USAGE:\n * ==============\n * ```tsx\n * <RadixTooltip.Trigger asChild>\n * <RefToTgphRef>\n * <Button>Hover me</Button> // Button uses tgphRef internally\n * </RefToTgphRef>\n * </RadixTooltip.Trigger>\n * ```\n *\n * WHAT IT DOES:\n * =============\n * 1. Receives a `ref` from the parent (e.g., Radix)\n * 2. Forwards it as both `ref` AND `tgphRef` to Telegraph children\n * 3. Merges any additional props from the parent with child props\n * 4. Handles both forwardRef components and regular components appropriately\n *\n * THE INFINITE LOOP PROBLEM:\n * ==========================\n * Radix and other libraries often pass ref callbacks that are recreated on every render\n * (new function references). When we pass these unstable refs to children via\n * React.cloneElement, it causes the child to re-render with \"new\" props even though\n * the ref functionality hasn't actually changed. This can trigger infinite render loops.\n *\n * THE SOLUTION:\n * =============\n * We create a STABLE ref callback using useCallback with an empty dependency array,\n * so the function reference never changes. We store the actual (unstable) ref in a\n * mutable ref (refStorage) and update it on every render. When our stable callback\n * is invoked, it reads from refStorage to get the latest ref and calls it.\n *\n * We also track the DOM node so that if the ref callback itself changes (rare but\n * possible), we can properly cleanup the old ref by calling it with null, and then\n * call the new ref with the current node. This matches React's standard ref behavior.\n */\nimport React from \"react\";\n\nconst FORWARD_REF_SYMBOL = Symbol.for(\"react.forward_ref\");\n\ntype ApplyRefPropsProps = {\n children: React.ReactNode;\n};\n\ntype Child = React.ReactElement & {\n $$typeof: symbol;\n type: { $$typeof: symbol };\n};\n\n/**\n * mergeProps\n *\n * Merges props from the slot (parent/wrapper) with props from the child component.\n * This follows the same approach as Radix's Slot component to ensure compatibility.\n *\n * MERGE STRATEGY:\n * - Event handlers (onX): Compose them so both parent and child handlers run\n * - style: Merge objects (child styles override parent styles with same keys)\n * - className: Concatenate both class strings\n * - Other props: Child props override parent props\n *\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/slot/src/Slot.tsx\n */\nconst mergeProps = (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n slotProps: Record<string, any>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n childProps: Record<string, any>,\n) => {\n // all child props should override\n const overrideProps = { ...childProps };\n\n for (const propName in childProps) {\n const slotPropValue = slotProps[propName];\n const childPropValue = childProps[propName];\n\n const isHandler = /^on[A-Z]/.test(propName);\n if (isHandler) {\n // if the handler exists on both, we compose them\n if (slotPropValue && childPropValue) {\n overrideProps[propName] = (...args: unknown[]) => {\n childPropValue(...args);\n slotPropValue(...args);\n };\n }\n // but if it exists only on the slot, we use only this one\n else if (slotPropValue) {\n overrideProps[propName] = slotPropValue;\n }\n }\n // if it's `style`, we merge them\n else if (propName === \"style\") {\n overrideProps[propName] = { ...slotPropValue, ...childPropValue };\n } else if (propName === \"className\") {\n overrideProps[propName] = [slotPropValue, childPropValue]\n .filter(Boolean)\n .join(\" \");\n }\n }\n\n return { ...slotProps, ...overrideProps };\n};\n\n/**\n * applyRefProps\n *\n * Clones child elements and applies the forwarded ref and any merged props to them.\n *\n * KEY DECISIONS:\n *\n * 1. ForwardRef Detection:\n * We check if a child is a forwardRef component by inspecting its $$typeof symbol.\n * This is necessary because forwardRef components EXPECT a `ref` prop, while\n * regular function components would throw a warning if given one.\n *\n * 2. Dual Ref Forwarding (forwardRef components):\n * For forwardRef components, we pass BOTH `ref` and `tgphRef` because:\n * - They might be third-party components that only understand `ref`\n * - They might be Telegraph components that need `tgphRef`\n * - Passing both ensures compatibility with all cases\n *\n * 3. Single Ref Forwarding (regular components):\n * For non-forwardRef components, we only pass `tgphRef` to avoid React warnings\n * about function components receiving refs.\n *\n * 4. Ref Priority:\n * If a child already has a `tgphRef`, we use that instead of the forwarded ref.\n * This allows child components to override ref behavior if needed.\n */\nconst applyRefProps = (\n { children, ...props }: ApplyRefPropsProps,\n ref: React.Ref<unknown>,\n) => {\n if (!children) return null;\n const childrenArray = React.Children.toArray(children);\n return childrenArray.map((child) => {\n if (React.isValidElement(child)) {\n const validChild = child as Child;\n const $$typeof = validChild.$$typeof;\n const $$typeofType = validChild.type.$$typeof;\n const childProps = validChild.props as Record<string, unknown>;\n const tgphRef = childProps.tgphRef;\n\n // CASE 1: ForwardRef Component\n // Pass both `ref` and `tgphRef` to ensure compatibility with both\n // Telegraph components and third-party forwardRef components.\n if (\n $$typeof === FORWARD_REF_SYMBOL ||\n $$typeofType === FORWARD_REF_SYMBOL\n ) {\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps as Record<string, unknown>),\n tgphRef: tgphRef || ref,\n ref: tgphRef || ref,\n } as Record<string, unknown>);\n }\n\n // CASE 2: Regular Component\n // Only pass `tgphRef` to avoid React warnings about function components\n // receiving refs (which would happen if we passed `ref`).\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps as Record<string, unknown>),\n tgphRef: tgphRef || ref,\n } as Record<string, unknown>);\n }\n\n // CASE 3: Non-element children (strings, numbers, etc.)\n // Return as-is since they can't receive refs or props.\n return child;\n });\n};\n\n/**\n * RefToTgphRef Component Implementation\n *\n * TYPE CONSTRAINTS:\n * We use `any` for the ref type because this component must accept refs of any type\n * (HTMLButtonElement, HTMLDivElement, custom component refs, etc.). Since we're\n * forwarding refs generically, there's no way to statically type this without\n * making the API cumbersome.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst RefToTgphRef = React.forwardRef<any, any>(\n ({ children: childrenProp, ...props }, ref) => {\n /**\n * REF STABILIZATION ARCHITECTURE\n *\n * PROBLEM:\n * Libraries like Radix create new ref callback functions on every render.\n * If we pass these unstable refs directly to children via React.cloneElement,\n * React sees the props object as changed (new function reference), causing\n * unnecessary re-renders and potential infinite loops.\n *\n * SOLUTION OVERVIEW:\n * Create a stable callback (stableRef) that never changes (empty deps array),\n * but internally reads from a mutable storage to get the latest ref. This way:\n * - Children receive the same function reference every render (no infinite loops)\n * - The function still forwards to the latest ref (functionality preserved)\n *\n */\n\n // Storage for the latest ref callback/object from parent (e.g., Radix)\n // This gets updated on every render but doesn't cause re-renders since it's\n // a mutable ref, not state.\n const refStorage = React.useRef(ref);\n\n // Storage for the current DOM node/component instance\n // We need this to handle ref changes properly (cleanup old, set new)\n const nodeStorage = React.useRef<unknown>(null);\n\n /**\n * REF CHANGE HANDLING\n *\n * When the parent ref changes (rare, but possible), we need to:\n * 1. Call the OLD ref with null (cleanup - standard React behavior)\n * 2. Call the NEW ref with the current node (re-attach)\n *\n * This matches React's native behavior when a ref prop changes.\n *\n * WHY IN useEffect:\n * We use useEffect (not direct assignment) because we need to detect when\n * the ref has actually changed between renders and perform cleanup/setup.\n */\n React.useEffect(() => {\n const prevRef = refStorage.current;\n const currentNode = nodeStorage.current;\n\n // Detect ref change\n if (prevRef !== ref && currentNode) {\n // Step 1: Cleanup old ref (call with null)\n if (typeof prevRef === \"function\") {\n prevRef(null);\n } else if (prevRef) {\n (prevRef as React.MutableRefObject<unknown>).current = null;\n }\n\n // Step 2: Set new ref with current node\n if (typeof ref === \"function\") {\n ref(currentNode);\n } else if (ref) {\n (ref as React.MutableRefObject<unknown>).current = currentNode;\n }\n }\n\n // Update storage with latest ref for next render\n refStorage.current = ref;\n });\n\n /**\n * STABLE REF CALLBACK\n *\n * This is the key to preventing infinite loops. The function reference\n * returned by useCallback with an empty dependency array NEVER changes.\n *\n * When called (by React when attaching/detaching from DOM):\n * 1. Store the node so we can handle ref changes\n * 2. Read the LATEST ref from refStorage\n * 3. Forward the call to that ref\n *\n * This indirection gives us stability (no infinite loops) while maintaining\n * correctness (always calls the latest ref).\n */\n const stableRef = React.useCallback((node: unknown) => {\n // Store node for ref change handling\n nodeStorage.current = node;\n\n // Get the current ref (might have been updated since last call)\n const currentRef = refStorage.current;\n\n // Forward to the actual ref (handle both callback refs and ref objects)\n if (typeof currentRef === \"function\") {\n currentRef(node);\n } else if (currentRef) {\n (currentRef as React.MutableRefObject<unknown>).current = node;\n }\n }, []); // Empty deps = stable function reference forever\n\n // Apply the stable ref and merged props to children\n return applyRefProps({ children: childrenProp, ...props }, stableRef);\n },\n);\n\nexport { RefToTgphRef };\n","/*\n * useDeterminateState\n *\n * A hook that returns a state transitioning to a determinate value after a minimum duration.\n * For example, you could use this hook with a button that transitions into a \"loading\" state,\n * ensuring it remains in the \"loading\" state for at least 1000ms. This provides clear feedback\n * to the user that the action is being processed.\n *\n */\nimport React from \"react\";\n\ntype UseDeterminateStateParams<T> = {\n value: T;\n determinateValue: T;\n minDurationMs?: number;\n};\n\nconst useDeterminateState = <T>({\n value,\n determinateValue,\n minDurationMs = 1000,\n}: UseDeterminateStateParams<T>): T => {\n const [state, setState] = React.useState<T>(value);\n const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);\n const startTimeRef = React.useRef<number | null>(null);\n\n const clearExistingTimeout = () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n };\n\n const handleTransition = React.useCallback(() => {\n if (value === determinateValue) {\n clearExistingTimeout();\n setState(determinateValue);\n startTimeRef.current = Date.now();\n } else if (startTimeRef.current !== null) {\n const elapsedTime = Date.now() - startTimeRef.current;\n const remainingTime = minDurationMs - elapsedTime;\n\n if (remainingTime > 0) {\n clearExistingTimeout();\n timeoutRef.current = setTimeout(() => {\n setState(value);\n startTimeRef.current = null;\n }, remainingTime);\n } else {\n setState(value);\n startTimeRef.current = null;\n }\n } else {\n setState(value);\n }\n }, [value, determinateValue, minDurationMs]);\n\n React.useEffect(() => {\n handleTransition();\n return clearExistingTimeout;\n }, [value, handleTransition]);\n\n return state;\n};\n\nexport { useDeterminateState };\n"],"names":["FORWARD_REF_SYMBOL","mergeProps","slotProps","childProps","overrideProps","propName","slotPropValue","childPropValue","args","applyRefProps","children","props","ref","React","child","validChild","$$typeof","$$typeofType","tgphRef","RefToTgphRef","childrenProp","refStorage","nodeStorage","prevRef","currentNode","stableRef","node","currentRef","useDeterminateState","value","determinateValue","minDurationMs","state","setState","timeoutRef","startTimeRef","clearExistingTimeout","handleTransition","elapsedTime","remainingTime"],"mappings":"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"}
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,47 +1,59 @@
|
|
|
1
|
-
import
|
|
2
|
-
const p = Symbol.for("react.forward_ref"),
|
|
3
|
-
const
|
|
4
|
-
for (const
|
|
5
|
-
const t =
|
|
6
|
-
/^on[A-Z]/.test(
|
|
7
|
-
|
|
8
|
-
} : t && (
|
|
1
|
+
import f from "react";
|
|
2
|
+
const p = Symbol.for("react.forward_ref"), R = (s, l) => {
|
|
3
|
+
const e = { ...l };
|
|
4
|
+
for (const c in l) {
|
|
5
|
+
const t = s[c], r = l[c];
|
|
6
|
+
/^on[A-Z]/.test(c) ? t && r ? e[c] = (...o) => {
|
|
7
|
+
r(...o), t(...o);
|
|
8
|
+
} : t && (e[c] = t) : c === "style" ? e[c] = { ...t, ...r } : c === "className" && (e[c] = [t, r].filter(Boolean).join(" "));
|
|
9
9
|
}
|
|
10
|
-
return { ...
|
|
11
|
-
},
|
|
12
|
-
if (
|
|
13
|
-
const
|
|
14
|
-
return
|
|
15
|
-
...
|
|
16
|
-
tgphRef:
|
|
17
|
-
ref:
|
|
18
|
-
}) :
|
|
19
|
-
...
|
|
20
|
-
tgphRef:
|
|
10
|
+
return { ...s, ...e };
|
|
11
|
+
}, m = ({ children: s, ...l }, e) => s ? f.Children.toArray(s).map((t) => {
|
|
12
|
+
if (f.isValidElement(t)) {
|
|
13
|
+
const r = t, n = r.$$typeof, o = r.type.$$typeof, u = r.props, i = u.tgphRef;
|
|
14
|
+
return n === p || o === p ? f.cloneElement(r, {
|
|
15
|
+
...R(l, u),
|
|
16
|
+
tgphRef: i || e,
|
|
17
|
+
ref: i || e
|
|
18
|
+
}) : f.cloneElement(r, {
|
|
19
|
+
...R(l, u),
|
|
20
|
+
tgphRef: i || e
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
return t;
|
|
24
|
-
}) : null, d =
|
|
25
|
-
({ children:
|
|
24
|
+
}) : null, d = f.forwardRef(
|
|
25
|
+
({ children: s, ...l }, e) => {
|
|
26
|
+
const c = f.useRef(e), t = f.useRef(null);
|
|
27
|
+
f.useEffect(() => {
|
|
28
|
+
const n = c.current, o = t.current;
|
|
29
|
+
n !== e && o && (typeof n == "function" ? n(null) : n && (n.current = null), typeof e == "function" ? e(o) : e && (e.current = o)), c.current = e;
|
|
30
|
+
});
|
|
31
|
+
const r = f.useCallback((n) => {
|
|
32
|
+
t.current = n;
|
|
33
|
+
const o = c.current;
|
|
34
|
+
typeof o == "function" ? o(n) : o && (o.current = n);
|
|
35
|
+
}, []);
|
|
36
|
+
return m({ children: s, ...l }, r);
|
|
37
|
+
}
|
|
26
38
|
), T = ({
|
|
27
|
-
value:
|
|
28
|
-
determinateValue:
|
|
29
|
-
minDurationMs:
|
|
39
|
+
value: s,
|
|
40
|
+
determinateValue: l,
|
|
41
|
+
minDurationMs: e = 1e3
|
|
30
42
|
}) => {
|
|
31
|
-
const [
|
|
32
|
-
|
|
33
|
-
},
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
else if (
|
|
37
|
-
const
|
|
38
|
-
a > 0 ? (
|
|
39
|
-
t(
|
|
40
|
-
}, a)) : (t(
|
|
43
|
+
const [c, t] = f.useState(s), r = f.useRef(null), n = f.useRef(null), o = () => {
|
|
44
|
+
r.current && (clearTimeout(r.current), r.current = null);
|
|
45
|
+
}, u = f.useCallback(() => {
|
|
46
|
+
if (s === l)
|
|
47
|
+
o(), t(l), n.current = Date.now();
|
|
48
|
+
else if (n.current !== null) {
|
|
49
|
+
const i = Date.now() - n.current, a = e - i;
|
|
50
|
+
a > 0 ? (o(), r.current = setTimeout(() => {
|
|
51
|
+
t(s), n.current = null;
|
|
52
|
+
}, a)) : (t(s), n.current = null);
|
|
41
53
|
} else
|
|
42
|
-
t(
|
|
43
|
-
}, [
|
|
44
|
-
return
|
|
54
|
+
t(s);
|
|
55
|
+
}, [s, l, e]);
|
|
56
|
+
return f.useEffect(() => (u(), o), [s, u]), c;
|
|
45
57
|
};
|
|
46
58
|
export {
|
|
47
59
|
d as RefToTgphRef,
|
package/dist/esm/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":["../../src/components/RefToTgphRef/RefToTgphRef.tsx","../../src/hooks/useDeterminateState.ts"],"sourcesContent":["//\n// When interacting with components like a Radix.Trigger, they assume that\n// our components accept a `ref` prop. This is not the case with our components\n// because we use the `tgphRef` prop instead. To work around this, we can create\n// a new component that accepts a `ref` prop and forwards it to the `tgphRef`\n// prop.\n//\nimport React from \"react\";\n\nconst FORWARD_REF_SYMBOL = Symbol.for(\"react.forward_ref\");\n\ntype ApplyRefPropsProps = {\n children: React.ReactNode;\n};\n\ntype Child = React.ReactElement & {\n $$typeof: symbol;\n type: { $$typeof: symbol };\n};\n\n// Merge props the same way that radix slot does\n// https://github.com/radix-ui/primitives/blob/main/packages/react/slot/src/Slot.tsx\nconst mergeProps = (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n slotProps: Record<string, any>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n childProps: Record<string, any>,\n) => {\n // all child props should override\n const overrideProps = { ...childProps };\n\n for (const propName in childProps) {\n const slotPropValue = slotProps[propName];\n const childPropValue = childProps[propName];\n\n const isHandler = /^on[A-Z]/.test(propName);\n if (isHandler) {\n // if the handler exists on both, we compose them\n if (slotPropValue && childPropValue) {\n overrideProps[propName] = (...args: unknown[]) => {\n childPropValue(...args);\n slotPropValue(...args);\n };\n }\n // but if it exists only on the slot, we use only this one\n else if (slotPropValue) {\n overrideProps[propName] = slotPropValue;\n }\n }\n // if it's `style`, we merge them\n else if (propName === \"style\") {\n overrideProps[propName] = { ...slotPropValue, ...childPropValue };\n } else if (propName === \"className\") {\n overrideProps[propName] = [slotPropValue, childPropValue]\n .filter(Boolean)\n .join(\" \");\n }\n }\n\n return { ...slotProps, ...overrideProps };\n};\n\nconst applyRefProps = (\n { children, ...props }: ApplyRefPropsProps,\n ref: React.Ref<unknown>,\n) => {\n if (!children) return null;\n const childrenArray = React.Children.toArray(children);\n return childrenArray.map((child) => {\n if (React.isValidElement(child)) {\n const validChild = child as Child;\n const $$typeof = validChild.$$typeof;\n const $$typeofType = validChild.type.$$typeof;\n const childProps = validChild.props;\n const tgphRef = childProps.tgphRef;\n\n // If we detect that the child is a forwardRef, we to pass the `ref` prop\n // to it so that components that exist outside of our library can still\n // receive the ref. We do it this way in order to avoid this warning:\n // \"Function components cannot be given refs. Attempts to access this ref will fail.\n // Did you mean to use React.forwardRef()\"\n if (\n $$typeof === FORWARD_REF_SYMBOL ||\n $$typeofType === FORWARD_REF_SYMBOL\n ) {\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps),\n tgphRef: tgphRef || ref,\n ref: tgphRef || ref,\n });\n }\n\n // Otherwise, we can just pass the `tgphRef` prop to the child.\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps),\n tgphRef: tgphRef || ref,\n });\n }\n\n // If the child is not a valid element, we can just return it.\n return child;\n });\n};\n\n// We can't generate the type of the ref because it's a forwardRef so any\n// works for this use case\n//\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst RefToTgphRef = React.forwardRef<any, any>(\n ({ children: childrenProp, ...props }, ref) => {\n return applyRefProps({ children: childrenProp, ...props }, ref);\n },\n);\n\nexport { RefToTgphRef };\n","/*\n * useDeterminateState\n *\n * A hook that returns a state transitioning to a determinate value after a minimum duration.\n * For example, you could use this hook with a button that transitions into a \"loading\" state,\n * ensuring it remains in the \"loading\" state for at least 1000ms. This provides clear feedback\n * to the user that the action is being processed.\n *\n */\nimport React from \"react\";\n\ntype UseDeterminateStateParams<T> = {\n value: T;\n determinateValue: T;\n minDurationMs?: number;\n};\n\nconst useDeterminateState = <T>({\n value,\n determinateValue,\n minDurationMs = 1000,\n}: UseDeterminateStateParams<T>): T => {\n const [state, setState] = React.useState<T>(value);\n const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);\n const startTimeRef = React.useRef<number | null>(null);\n\n const clearExistingTimeout = () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n };\n\n const handleTransition = React.useCallback(() => {\n if (value === determinateValue) {\n clearExistingTimeout();\n setState(determinateValue);\n startTimeRef.current = Date.now();\n } else if (startTimeRef.current !== null) {\n const elapsedTime = Date.now() - startTimeRef.current;\n const remainingTime = minDurationMs - elapsedTime;\n\n if (remainingTime > 0) {\n clearExistingTimeout();\n timeoutRef.current = setTimeout(() => {\n setState(value);\n startTimeRef.current = null;\n }, remainingTime);\n } else {\n setState(value);\n startTimeRef.current = null;\n }\n } else {\n setState(value);\n }\n }, [value, determinateValue, minDurationMs]);\n\n React.useEffect(() => {\n handleTransition();\n return clearExistingTimeout;\n }, [value, handleTransition]);\n\n return state;\n};\n\nexport { useDeterminateState };\n"],"names":["FORWARD_REF_SYMBOL","mergeProps","slotProps","childProps","overrideProps","propName","slotPropValue","childPropValue","args","applyRefProps","children","props","ref","React","child","validChild","$$typeof","$$typeofType","tgphRef","RefToTgphRef","childrenProp","useDeterminateState","value","determinateValue","minDurationMs","state","setState","timeoutRef","startTimeRef","clearExistingTimeout","handleTransition","elapsedTime","remainingTime"],"mappings":";AASA,MAAMA,IAAqB,OAAO,IAAI,mBAAmB,GAanDC,IAAa,CAEjBC,GAEAC,MACG;AAEG,QAAAC,IAAgB,EAAE,GAAGD,EAAW;AAEtC,aAAWE,KAAYF,GAAY;AAC3B,UAAAG,IAAgBJ,EAAUG,CAAQ,GAClCE,IAAiBJ,EAAWE,CAAQ;AAG1C,IADkB,WAAW,KAAKA,CAAQ,IAGpCC,KAAiBC,IACLH,EAAAC,CAAQ,IAAI,IAAIG,MAAoB;AAChD,MAAAD,EAAe,GAAGC,CAAI,GACtBF,EAAc,GAAGE,CAAI;AAAA,IACvB,IAGOF,MACPF,EAAcC,CAAQ,IAAIC,KAIrBD,MAAa,UACpBD,EAAcC,CAAQ,IAAI,EAAE,GAAGC,GAAe,GAAGC,EAAe,IACvDF,MAAa,gBACRD,EAAAC,CAAQ,IAAI,CAACC,GAAeC,CAAc,EACrD,OAAO,OAAO,EACd,KAAK,GAAG;AAAA,EACb;AAGF,SAAO,EAAE,GAAGL,GAAW,GAAGE,EAAc;AAC1C,GAEMK,IAAgB,CACpB,EAAE,UAAAC,GAAU,GAAGC,EAAA,GACfC,MAEKF,IACiBG,EAAM,SAAS,QAAQH,CAAQ,EAChC,IAAI,CAACI,MAAU;AAC9B,MAAAD,EAAM,eAAeC,CAAK,GAAG;AAC/B,UAAMC,IAAaD,GACbE,IAAWD,EAAW,UACtBE,IAAeF,EAAW,KAAK,UAC/BZ,IAAaY,EAAW,OACxBG,IAAUf,EAAW;AAQzB,WAAAa,MAAahB,KACbiB,MAAiBjB,IAEVa,EAAM,aAAaE,GAAY;AAAA,MACpC,GAAGd,EAAWU,GAAOR,CAAU;AAAA,MAC/B,SAASe,KAAWN;AAAA,MACpB,KAAKM,KAAWN;AAAA,IAAA,CACjB,IAIIC,EAAM,aAAaE,GAAY;AAAA,MACpC,GAAGd,EAAWU,GAAOR,CAAU;AAAA,MAC/B,SAASe,KAAWN;AAAA,IAAA,CACrB;AAAA,EAAA;AAII,SAAAE;AAAA,CACR,IAnCqB,MA0ClBK,IAAeN,EAAM;AAAA,EACzB,CAAC,EAAE,UAAUO,GAAc,GAAGT,EAAA,GAASC,MAC9BH,EAAc,EAAE,UAAUW,GAAc,GAAGT,KAASC,CAAG;AAElE,GC/FMS,IAAsB,CAAI;AAAA,EAC9B,OAAAC;AAAA,EACA,kBAAAC;AAAA,EACA,eAAAC,IAAgB;AAClB,MAAuC;AACrC,QAAM,CAACC,GAAOC,CAAQ,IAAIb,EAAM,SAAYS,CAAK,GAC3CK,IAAad,EAAM,OAA8B,IAAI,GACrDe,IAAef,EAAM,OAAsB,IAAI,GAE/CgB,IAAuB,MAAM;AACjC,IAAIF,EAAW,YACb,aAAaA,EAAW,OAAO,GAC/BA,EAAW,UAAU;AAAA,EAEzB,GAEMG,IAAmBjB,EAAM,YAAY,MAAM;AAC/C,QAAIS,MAAUC;AACS,MAAAM,EAAA,GACrBH,EAASH,CAAgB,GACZK,EAAA,UAAU,KAAK,IAAI;AAAA,aACvBA,EAAa,YAAY,MAAM;AACxC,YAAMG,IAAc,KAAK,IAAI,IAAIH,EAAa,SACxCI,IAAgBR,IAAgBO;AAEtC,MAAIC,IAAgB,KACGH,EAAA,GACVF,EAAA,UAAU,WAAW,MAAM;AACpC,QAAAD,EAASJ,CAAK,GACdM,EAAa,UAAU;AAAA,SACtBI,CAAa,MAEhBN,EAASJ,CAAK,GACdM,EAAa,UAAU;AAAA,IACzB;AAEA,MAAAF,EAASJ,CAAK;AAAA,EAEf,GAAA,CAACA,GAAOC,GAAkBC,CAAa,CAAC;AAE3C,SAAAX,EAAM,UAAU,OACGiB,EAAA,GACVD,IACN,CAACP,GAAOQ,CAAgB,CAAC,GAErBL;AACT;"}
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../../src/components/RefToTgphRef/RefToTgphRef.tsx","../../src/hooks/useDeterminateState.ts"],"sourcesContent":["/**\n * RefToTgphRef Component\n *\n * PURPOSE:\n * ========\n * This component bridges the gap between third-party libraries (like Radix UI) and\n * Telegraph components. Third-party libraries expect components to accept a standard\n * React `ref` prop, but Telegraph components use a custom `tgphRef` prop instead.\n *\n * Without this adapter, using Telegraph components with libraries like Radix would fail\n * because Radix would try to pass a `ref` that Telegraph components wouldn't receive.\n *\n * EXAMPLE USAGE:\n * ==============\n * ```tsx\n * <RadixTooltip.Trigger asChild>\n * <RefToTgphRef>\n * <Button>Hover me</Button> // Button uses tgphRef internally\n * </RefToTgphRef>\n * </RadixTooltip.Trigger>\n * ```\n *\n * WHAT IT DOES:\n * =============\n * 1. Receives a `ref` from the parent (e.g., Radix)\n * 2. Forwards it as both `ref` AND `tgphRef` to Telegraph children\n * 3. Merges any additional props from the parent with child props\n * 4. Handles both forwardRef components and regular components appropriately\n *\n * THE INFINITE LOOP PROBLEM:\n * ==========================\n * Radix and other libraries often pass ref callbacks that are recreated on every render\n * (new function references). When we pass these unstable refs to children via\n * React.cloneElement, it causes the child to re-render with \"new\" props even though\n * the ref functionality hasn't actually changed. This can trigger infinite render loops.\n *\n * THE SOLUTION:\n * =============\n * We create a STABLE ref callback using useCallback with an empty dependency array,\n * so the function reference never changes. We store the actual (unstable) ref in a\n * mutable ref (refStorage) and update it on every render. When our stable callback\n * is invoked, it reads from refStorage to get the latest ref and calls it.\n *\n * We also track the DOM node so that if the ref callback itself changes (rare but\n * possible), we can properly cleanup the old ref by calling it with null, and then\n * call the new ref with the current node. This matches React's standard ref behavior.\n */\nimport React from \"react\";\n\nconst FORWARD_REF_SYMBOL = Symbol.for(\"react.forward_ref\");\n\ntype ApplyRefPropsProps = {\n children: React.ReactNode;\n};\n\ntype Child = React.ReactElement & {\n $$typeof: symbol;\n type: { $$typeof: symbol };\n};\n\n/**\n * mergeProps\n *\n * Merges props from the slot (parent/wrapper) with props from the child component.\n * This follows the same approach as Radix's Slot component to ensure compatibility.\n *\n * MERGE STRATEGY:\n * - Event handlers (onX): Compose them so both parent and child handlers run\n * - style: Merge objects (child styles override parent styles with same keys)\n * - className: Concatenate both class strings\n * - Other props: Child props override parent props\n *\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/slot/src/Slot.tsx\n */\nconst mergeProps = (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n slotProps: Record<string, any>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n childProps: Record<string, any>,\n) => {\n // all child props should override\n const overrideProps = { ...childProps };\n\n for (const propName in childProps) {\n const slotPropValue = slotProps[propName];\n const childPropValue = childProps[propName];\n\n const isHandler = /^on[A-Z]/.test(propName);\n if (isHandler) {\n // if the handler exists on both, we compose them\n if (slotPropValue && childPropValue) {\n overrideProps[propName] = (...args: unknown[]) => {\n childPropValue(...args);\n slotPropValue(...args);\n };\n }\n // but if it exists only on the slot, we use only this one\n else if (slotPropValue) {\n overrideProps[propName] = slotPropValue;\n }\n }\n // if it's `style`, we merge them\n else if (propName === \"style\") {\n overrideProps[propName] = { ...slotPropValue, ...childPropValue };\n } else if (propName === \"className\") {\n overrideProps[propName] = [slotPropValue, childPropValue]\n .filter(Boolean)\n .join(\" \");\n }\n }\n\n return { ...slotProps, ...overrideProps };\n};\n\n/**\n * applyRefProps\n *\n * Clones child elements and applies the forwarded ref and any merged props to them.\n *\n * KEY DECISIONS:\n *\n * 1. ForwardRef Detection:\n * We check if a child is a forwardRef component by inspecting its $$typeof symbol.\n * This is necessary because forwardRef components EXPECT a `ref` prop, while\n * regular function components would throw a warning if given one.\n *\n * 2. Dual Ref Forwarding (forwardRef components):\n * For forwardRef components, we pass BOTH `ref` and `tgphRef` because:\n * - They might be third-party components that only understand `ref`\n * - They might be Telegraph components that need `tgphRef`\n * - Passing both ensures compatibility with all cases\n *\n * 3. Single Ref Forwarding (regular components):\n * For non-forwardRef components, we only pass `tgphRef` to avoid React warnings\n * about function components receiving refs.\n *\n * 4. Ref Priority:\n * If a child already has a `tgphRef`, we use that instead of the forwarded ref.\n * This allows child components to override ref behavior if needed.\n */\nconst applyRefProps = (\n { children, ...props }: ApplyRefPropsProps,\n ref: React.Ref<unknown>,\n) => {\n if (!children) return null;\n const childrenArray = React.Children.toArray(children);\n return childrenArray.map((child) => {\n if (React.isValidElement(child)) {\n const validChild = child as Child;\n const $$typeof = validChild.$$typeof;\n const $$typeofType = validChild.type.$$typeof;\n const childProps = validChild.props as Record<string, unknown>;\n const tgphRef = childProps.tgphRef;\n\n // CASE 1: ForwardRef Component\n // Pass both `ref` and `tgphRef` to ensure compatibility with both\n // Telegraph components and third-party forwardRef components.\n if (\n $$typeof === FORWARD_REF_SYMBOL ||\n $$typeofType === FORWARD_REF_SYMBOL\n ) {\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps as Record<string, unknown>),\n tgphRef: tgphRef || ref,\n ref: tgphRef || ref,\n } as Record<string, unknown>);\n }\n\n // CASE 2: Regular Component\n // Only pass `tgphRef` to avoid React warnings about function components\n // receiving refs (which would happen if we passed `ref`).\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps as Record<string, unknown>),\n tgphRef: tgphRef || ref,\n } as Record<string, unknown>);\n }\n\n // CASE 3: Non-element children (strings, numbers, etc.)\n // Return as-is since they can't receive refs or props.\n return child;\n });\n};\n\n/**\n * RefToTgphRef Component Implementation\n *\n * TYPE CONSTRAINTS:\n * We use `any` for the ref type because this component must accept refs of any type\n * (HTMLButtonElement, HTMLDivElement, custom component refs, etc.). Since we're\n * forwarding refs generically, there's no way to statically type this without\n * making the API cumbersome.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst RefToTgphRef = React.forwardRef<any, any>(\n ({ children: childrenProp, ...props }, ref) => {\n /**\n * REF STABILIZATION ARCHITECTURE\n *\n * PROBLEM:\n * Libraries like Radix create new ref callback functions on every render.\n * If we pass these unstable refs directly to children via React.cloneElement,\n * React sees the props object as changed (new function reference), causing\n * unnecessary re-renders and potential infinite loops.\n *\n * SOLUTION OVERVIEW:\n * Create a stable callback (stableRef) that never changes (empty deps array),\n * but internally reads from a mutable storage to get the latest ref. This way:\n * - Children receive the same function reference every render (no infinite loops)\n * - The function still forwards to the latest ref (functionality preserved)\n *\n */\n\n // Storage for the latest ref callback/object from parent (e.g., Radix)\n // This gets updated on every render but doesn't cause re-renders since it's\n // a mutable ref, not state.\n const refStorage = React.useRef(ref);\n\n // Storage for the current DOM node/component instance\n // We need this to handle ref changes properly (cleanup old, set new)\n const nodeStorage = React.useRef<unknown>(null);\n\n /**\n * REF CHANGE HANDLING\n *\n * When the parent ref changes (rare, but possible), we need to:\n * 1. Call the OLD ref with null (cleanup - standard React behavior)\n * 2. Call the NEW ref with the current node (re-attach)\n *\n * This matches React's native behavior when a ref prop changes.\n *\n * WHY IN useEffect:\n * We use useEffect (not direct assignment) because we need to detect when\n * the ref has actually changed between renders and perform cleanup/setup.\n */\n React.useEffect(() => {\n const prevRef = refStorage.current;\n const currentNode = nodeStorage.current;\n\n // Detect ref change\n if (prevRef !== ref && currentNode) {\n // Step 1: Cleanup old ref (call with null)\n if (typeof prevRef === \"function\") {\n prevRef(null);\n } else if (prevRef) {\n (prevRef as React.MutableRefObject<unknown>).current = null;\n }\n\n // Step 2: Set new ref with current node\n if (typeof ref === \"function\") {\n ref(currentNode);\n } else if (ref) {\n (ref as React.MutableRefObject<unknown>).current = currentNode;\n }\n }\n\n // Update storage with latest ref for next render\n refStorage.current = ref;\n });\n\n /**\n * STABLE REF CALLBACK\n *\n * This is the key to preventing infinite loops. The function reference\n * returned by useCallback with an empty dependency array NEVER changes.\n *\n * When called (by React when attaching/detaching from DOM):\n * 1. Store the node so we can handle ref changes\n * 2. Read the LATEST ref from refStorage\n * 3. Forward the call to that ref\n *\n * This indirection gives us stability (no infinite loops) while maintaining\n * correctness (always calls the latest ref).\n */\n const stableRef = React.useCallback((node: unknown) => {\n // Store node for ref change handling\n nodeStorage.current = node;\n\n // Get the current ref (might have been updated since last call)\n const currentRef = refStorage.current;\n\n // Forward to the actual ref (handle both callback refs and ref objects)\n if (typeof currentRef === \"function\") {\n currentRef(node);\n } else if (currentRef) {\n (currentRef as React.MutableRefObject<unknown>).current = node;\n }\n }, []); // Empty deps = stable function reference forever\n\n // Apply the stable ref and merged props to children\n return applyRefProps({ children: childrenProp, ...props }, stableRef);\n },\n);\n\nexport { RefToTgphRef };\n","/*\n * useDeterminateState\n *\n * A hook that returns a state transitioning to a determinate value after a minimum duration.\n * For example, you could use this hook with a button that transitions into a \"loading\" state,\n * ensuring it remains in the \"loading\" state for at least 1000ms. This provides clear feedback\n * to the user that the action is being processed.\n *\n */\nimport React from \"react\";\n\ntype UseDeterminateStateParams<T> = {\n value: T;\n determinateValue: T;\n minDurationMs?: number;\n};\n\nconst useDeterminateState = <T>({\n value,\n determinateValue,\n minDurationMs = 1000,\n}: UseDeterminateStateParams<T>): T => {\n const [state, setState] = React.useState<T>(value);\n const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);\n const startTimeRef = React.useRef<number | null>(null);\n\n const clearExistingTimeout = () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n };\n\n const handleTransition = React.useCallback(() => {\n if (value === determinateValue) {\n clearExistingTimeout();\n setState(determinateValue);\n startTimeRef.current = Date.now();\n } else if (startTimeRef.current !== null) {\n const elapsedTime = Date.now() - startTimeRef.current;\n const remainingTime = minDurationMs - elapsedTime;\n\n if (remainingTime > 0) {\n clearExistingTimeout();\n timeoutRef.current = setTimeout(() => {\n setState(value);\n startTimeRef.current = null;\n }, remainingTime);\n } else {\n setState(value);\n startTimeRef.current = null;\n }\n } else {\n setState(value);\n }\n }, [value, determinateValue, minDurationMs]);\n\n React.useEffect(() => {\n handleTransition();\n return clearExistingTimeout;\n }, [value, handleTransition]);\n\n return state;\n};\n\nexport { useDeterminateState };\n"],"names":["FORWARD_REF_SYMBOL","mergeProps","slotProps","childProps","overrideProps","propName","slotPropValue","childPropValue","args","applyRefProps","children","props","ref","React","child","validChild","$$typeof","$$typeofType","tgphRef","RefToTgphRef","childrenProp","refStorage","nodeStorage","prevRef","currentNode","stableRef","node","currentRef","useDeterminateState","value","determinateValue","minDurationMs","state","setState","timeoutRef","startTimeRef","clearExistingTimeout","handleTransition","elapsedTime","remainingTime"],"mappings":"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"}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* RefToTgphRef Component Implementation
|
|
4
|
+
*
|
|
5
|
+
* TYPE CONSTRAINTS:
|
|
6
|
+
* We use `any` for the ref type because this component must accept refs of any type
|
|
7
|
+
* (HTMLButtonElement, HTMLDivElement, custom component refs, etc.). Since we're
|
|
8
|
+
* forwarding refs generically, there's no way to statically type this without
|
|
9
|
+
* making the API cumbersome.
|
|
10
|
+
*/
|
|
2
11
|
declare const RefToTgphRef: React.ForwardRefExoticComponent<Omit<any, "ref"> & React.RefAttributes<any>>;
|
|
3
12
|
export { RefToTgphRef };
|
|
4
13
|
//# sourceMappingURL=RefToTgphRef.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RefToTgphRef.d.ts","sourceRoot":"","sources":["../../../../src/components/RefToTgphRef/RefToTgphRef.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RefToTgphRef.d.ts","sourceRoot":"","sources":["../../../../src/components/RefToTgphRef/RefToTgphRef.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,OAAO,KAAK,MAAM,OAAO,CAAC;AAwI1B;;;;;;;;GAQG;AAEH,QAAA,MAAM,YAAY,8EAkGjB,CAAC;AAEF,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDeterminateState.d.ts","sourceRoot":"","sources":["../../../src/hooks/useDeterminateState.ts"],"names":[],"mappings":"AAWA,KAAK,yBAAyB,CAAC,CAAC,IAAI;IAClC,KAAK,EAAE,CAAC,CAAC;IACT,gBAAgB,EAAE,CAAC,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,QAAA,MAAM,mBAAmB,GAAI,CAAC
|
|
1
|
+
{"version":3,"file":"useDeterminateState.d.ts","sourceRoot":"","sources":["../../../src/hooks/useDeterminateState.ts"],"names":[],"mappings":"AAWA,KAAK,yBAAyB,CAAC,CAAC,IAAI;IAClC,KAAK,EAAE,CAAC,CAAC;IACT,gBAAgB,EAAE,CAAC,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,QAAA,MAAM,mBAAmB,GAAI,CAAC,EAAE,6CAI7B,yBAAyB,CAAC,CAAC,CAAC,KAAG,CA0CjC,CAAC;AAEF,OAAO,EAAE,mBAAmB,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telegraph/helpers",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"repository": "https://github.com/knocklabs/telegraph/tree/main/packages/helpers",
|
|
5
5
|
"author": "@knocklabs",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,16 +29,16 @@
|
|
|
29
29
|
"preview": "vite preview"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@knocklabs/eslint-config": "^0.0.
|
|
32
|
+
"@knocklabs/eslint-config": "^0.0.5",
|
|
33
33
|
"@knocklabs/typescript-config": "^0.0.2",
|
|
34
34
|
"@telegraph/prettier-config": "^0.0.7",
|
|
35
35
|
"@telegraph/vite-config": "^0.0.15",
|
|
36
|
-
"@types/react": "^
|
|
36
|
+
"@types/react": "^19.2.9",
|
|
37
37
|
"eslint": "^8.56.0",
|
|
38
|
-
"react": "^
|
|
39
|
-
"react-dom": "^
|
|
40
|
-
"typescript": "^5.
|
|
41
|
-
"vite": "^6.
|
|
38
|
+
"react": "^19.2.3",
|
|
39
|
+
"react-dom": "^19.2.3",
|
|
40
|
+
"typescript": "^5.9.3",
|
|
41
|
+
"vite": "^6.4.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"react": "^18.0.0 || ^19.0.0",
|