@telegraph/helpers 0.0.15 → 0.0.16

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # @telegraph/helpers
2
2
 
3
+ ## 0.0.16
4
+
5
+ ### Patch Changes
6
+
7
+ - [#830](https://github.com/knocklabs/telegraph/pull/830) [`f9c6e1c`](https://github.com/knocklabs/telegraph/commit/f9c6e1c078a1bd3d6a8e5eb0ce2dd6713ccc781e) Thanks [@kylemcd](https://github.com/kylemcd)! - fix(vite-config): emit declaration files to a consistent `dist/types` root
8
+
9
+ Pin the TypeScript declaration root to `src` in the shared dts plugin config so
10
+ every package emits its types to `dist/types/index.d.ts`. Previously, packages
11
+ whose tsconfig omitted `rootDir` emitted to `dist/types/src/index.d.ts`, which
12
+ did not match the `types` entrypoint declared in their `package.json`, so
13
+ consumers received no type definitions (`@telegraph/compose-refs`, `helpers`,
14
+ `input`, `modal`, `nextjs`, `tokens`).
15
+
16
+ Pinning the declaration root also repairs degraded type emission that depended
17
+ on the inferred root: `@telegraph/tabs` previously emitted a dangling
18
+ `TgphElement` reference (`error TS2304: Cannot find name 'TgphElement'` for
19
+ consumers), and `@telegraph/modal`'s `Content` prop type was widened to `any`.
20
+ Both now emit correct, fully-resolved types.
21
+
22
+ Also corrects the stale top-level `types` field in `@telegraph/tokens`.
23
+
3
24
  ## 0.0.15
4
25
 
5
26
  ### Patch Changes
package/dist/cjs/index.js CHANGED
@@ -1,2 +1,2 @@
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
- //# sourceMappingURL=index.js.map
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require("react");c=s(c);var l=Symbol.for(`react.forward_ref`),u=(e,t)=>{let n={...t};for(let r in t){let i=e[r],a=t[r];/^on[A-Z]/.test(r)?i&&a?n[r]=(...e)=>{a(...e),i(...e)}:i&&(n[r]=i):r===`style`?n[r]={...i,...a}:r===`className`&&(n[r]=[i,a].filter(Boolean).join(` `))}return{...e,...n}},d=({children:e,...t},n)=>e?c.default.Children.toArray(e).map(e=>{if(c.default.isValidElement(e)){let r=e,i=r.$$typeof,a=r.type.$$typeof,o=r.props,s=o.tgphRef;return i===l||a===l?c.default.cloneElement(r,{...u(t,o),tgphRef:s||n,ref:s||n}):c.default.cloneElement(r,{...u(t,o),tgphRef:s||n})}return e}):null,f=c.default.forwardRef(({children:e,...t},n)=>{let r=c.default.useRef(n),i=c.default.useRef(null);c.default.useEffect(()=>{let e=r.current,t=i.current;e!==n&&t&&(typeof e==`function`?e(null):e&&(e.current=null),typeof n==`function`?n(t):n&&(n.current=t)),r.current=n});let a=c.default.useCallback(e=>{i.current=e;let t=r.current;typeof t==`function`?t(e):t&&(t.current=e)},[]);return d({children:e,...t},a)}),p=({value:e,determinateValue:t,minDurationMs:n=1e3})=>{let[r,i]=c.default.useState(e),a=c.default.useRef(null),o=c.default.useRef(null),s=()=>{a.current&&=(clearTimeout(a.current),null)},l=c.default.useCallback(()=>{if(e===t)s(),i(t),o.current=Date.now();else if(o.current!==null){let t=n-(Date.now()-o.current);t>0?(s(),a.current=setTimeout(()=>{i(e),o.current=null},t)):(i(e),o.current=null)}else i(e)},[e,t,n]);return c.default.useEffect(()=>(l(),s),[e,l]),r};exports.RefToTgphRef=f,exports.useDeterminateState=p;
2
+ //# sourceMappingURL=index.js.map
@@ -1 +1 @@
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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"}
1
+ {"version":3,"file":"index.js","names":[],"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"],"mappings":"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"}
@@ -1,62 +1,67 @@
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
- }
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
- });
22
- }
23
- return t;
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
- }
38
- ), T = ({
39
- value: s,
40
- determinateValue: l,
41
- minDurationMs: e = 1e3
42
- }) => {
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);
53
- } else
54
- t(s);
55
- }, [s, l, e]);
56
- return f.useEffect(() => (u(), o), [s, u]), c;
1
+ import e from "react";
2
+ //#region src/components/RefToTgphRef/RefToTgphRef.tsx
3
+ var t = Symbol.for("react.forward_ref"), n = (e, t) => {
4
+ let n = { ...t };
5
+ for (let r in t) {
6
+ let i = e[r], a = t[r];
7
+ /^on[A-Z]/.test(r) ? i && a ? n[r] = (...e) => {
8
+ a(...e), i(...e);
9
+ } : i && (n[r] = i) : r === "style" ? n[r] = {
10
+ ...i,
11
+ ...a
12
+ } : r === "className" && (n[r] = [i, a].filter(Boolean).join(" "));
13
+ }
14
+ return {
15
+ ...e,
16
+ ...n
17
+ };
18
+ }, r = ({ children: r, ...i }, a) => r ? e.Children.toArray(r).map((r) => {
19
+ if (e.isValidElement(r)) {
20
+ let o = r, s = o.$$typeof, c = o.type.$$typeof, l = o.props, u = l.tgphRef;
21
+ return s === t || c === t ? e.cloneElement(o, {
22
+ ...n(i, l),
23
+ tgphRef: u || a,
24
+ ref: u || a
25
+ }) : e.cloneElement(o, {
26
+ ...n(i, l),
27
+ tgphRef: u || a
28
+ });
29
+ }
30
+ return r;
31
+ }) : null, i = e.forwardRef(({ children: t, ...n }, i) => {
32
+ let a = e.useRef(i), o = e.useRef(null);
33
+ e.useEffect(() => {
34
+ let e = a.current, t = o.current;
35
+ e !== i && t && (typeof e == "function" ? e(null) : e && (e.current = null), typeof i == "function" ? i(t) : i && (i.current = t)), a.current = i;
36
+ });
37
+ let s = e.useCallback((e) => {
38
+ o.current = e;
39
+ let t = a.current;
40
+ typeof t == "function" ? t(e) : t && (t.current = e);
41
+ }, []);
42
+ return r({
43
+ children: t,
44
+ ...n
45
+ }, s);
46
+ }), a = ({ value: t, determinateValue: n, minDurationMs: r = 1e3 }) => {
47
+ let [i, a] = e.useState(t), o = e.useRef(null), s = e.useRef(null), c = () => {
48
+ o.current &&= (clearTimeout(o.current), null);
49
+ }, l = e.useCallback(() => {
50
+ if (t === n) c(), a(n), s.current = Date.now();
51
+ else if (s.current !== null) {
52
+ let e = r - (Date.now() - s.current);
53
+ e > 0 ? (c(), o.current = setTimeout(() => {
54
+ a(t), s.current = null;
55
+ }, e)) : (a(t), s.current = null);
56
+ } else a(t);
57
+ }, [
58
+ t,
59
+ n,
60
+ r
61
+ ]);
62
+ return e.useEffect(() => (l(), c), [t, l]), i;
57
63
  };
58
- export {
59
- d as RefToTgphRef,
60
- T as useDeterminateState
61
- };
62
- //# sourceMappingURL=index.mjs.map
64
+ //#endregion
65
+ export { i as RefToTgphRef, a as useDeterminateState };
66
+
67
+ //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
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
+ {"version":3,"file":"index.mjs","names":[],"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"],"mappings":";;AAiDA,IAAM,IAAqB,OAAO,IAAI,mBAAmB,GAyBnD,KAEJ,GAEA,MACG;CAEH,IAAM,IAAgB,EAAE,GAAG,EAAW;CAEtC,KAAK,IAAM,KAAY,GAAY;EACjC,IAAM,IAAgB,EAAU,IAC1B,IAAiB,EAAW;EAGlC,AADkB,WAAW,KAAK,CAC9B,IAEE,KAAiB,IACnB,EAAc,MAAa,GAAG,MAAoB;GAEhD,AADA,EAAe,GAAG,CAAI,GACtB,EAAc,GAAG,CAAI;EACvB,IAGO,MACP,EAAc,KAAY,KAIrB,MAAa,UACpB,EAAc,KAAY;GAAE,GAAG;GAAe,GAAG;EAAe,IACvD,MAAa,gBACtB,EAAc,KAAY,CAAC,GAAe,CAAc,EACrD,OAAO,OAAO,EACd,KAAK,GAAG;CAEf;CAEA,OAAO;EAAE,GAAG;EAAW,GAAG;CAAc;AAC1C,GA4BM,KACJ,EAAE,aAAU,GAAG,KACf,MAEK,IACiB,EAAM,SAAS,QAAQ,CACtC,EAAc,KAAK,MAAU;CAClC,IAAI,EAAM,eAAe,CAAK,GAAG;EAC/B,IAAM,IAAa,GACb,IAAW,EAAW,UACtB,IAAe,EAAW,KAAK,UAC/B,IAAa,EAAW,OACxB,IAAU,EAAW;EAmB3B,OAbE,MAAa,KACb,MAAiB,IAEV,EAAM,aAAa,GAAY;GACpC,GAAG,EAAW,GAAO,CAAqC;GAC1D,SAAS,KAAW;GACpB,KAAK,KAAW;EAClB,CAA4B,IAMvB,EAAM,aAAa,GAAY;GACpC,GAAG,EAAW,GAAO,CAAqC;GAC1D,SAAS,KAAW;EACtB,CAA4B;CAC9B;CAIA,OAAO;AACT,CAAC,IApCqB,MAiDlB,IAAe,EAAM,YACxB,EAAE,UAAU,GAAc,GAAG,KAAS,MAAQ;CAqB7C,IAAM,IAAa,EAAM,OAAO,CAAG,GAI7B,IAAc,EAAM,OAAgB,IAAI;CAe9C,EAAM,gBAAgB;EACpB,IAAM,IAAU,EAAW,SACrB,IAAc,EAAY;EAoBhC,AAjBI,MAAY,KAAO,MAEjB,OAAO,KAAY,aACrB,EAAQ,IAAI,IACH,MACT,EAA6C,UAAU,OAIrD,OAAO,KAAQ,aACjB,EAAI,CAAW,IACN,MACT,EAAyC,UAAU,KAKvD,EAAW,UAAU;CACvB,CAAC;CAgBD,IAAM,IAAY,EAAM,aAAa,MAAkB;EAErD,EAAY,UAAU;EAGtB,IAAM,IAAa,EAAW;EAG9B,AAAI,OAAO,KAAe,aACxB,EAAW,CAAI,IACN,MACT,EAAgD,UAAU;CAE9D,GAAG,CAAC,CAAC;CAGL,OAAO,EAAc;EAAE,UAAU;EAAc,GAAG;CAAM,GAAG,CAAS;AACtE,CACF,GClRM,KAA0B,EAC9B,UACA,qBACA,mBAAgB,UACqB;CACrC,IAAM,CAAC,GAAO,KAAY,EAAM,SAAY,CAAK,GAC3C,IAAa,EAAM,OAA8B,IAAI,GACrD,IAAe,EAAM,OAAsB,IAAI,GAE/C,UAA6B;EACjC,AAEE,EAAW,aADX,aAAa,EAAW,OAAO,GACV;CAEzB,GAEM,IAAmB,EAAM,kBAAkB;EAC/C,IAAI,MAAU,GAGZ,AAFA,EAAqB,GACrB,EAAS,CAAgB,GACzB,EAAa,UAAU,KAAK,IAAI;OAC3B,IAAI,EAAa,YAAY,MAAM;GAExC,IAAM,IAAgB,KADF,KAAK,IAAI,IAAI,EAAa;GAG9C,AAAI,IAAgB,KAClB,EAAqB,GACrB,EAAW,UAAU,iBAAiB;IAEpC,AADA,EAAS,CAAK,GACd,EAAa,UAAU;GACzB,GAAG,CAAa,MAEhB,EAAS,CAAK,GACd,EAAa,UAAU;EAE3B,OACE,EAAS,CAAK;CAElB,GAAG;EAAC;EAAO;EAAkB;CAAa,CAAC;CAO3C,OALA,EAAM,iBACJ,EAAiB,GACV,IACN,CAAC,GAAO,CAAgB,CAAC,GAErB;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telegraph/helpers",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "repository": "https://github.com/knocklabs/telegraph/tree/main/packages/helpers",
5
5
  "author": "@knocklabs",
6
6
  "license": "MIT",
@@ -31,14 +31,14 @@
31
31
  "devDependencies": {
32
32
  "@knocklabs/eslint-config": "^0.0.5",
33
33
  "@knocklabs/typescript-config": "^0.0.2",
34
- "@telegraph/prettier-config": "^0.0.7",
35
- "@telegraph/vite-config": "^0.0.15",
36
- "@types/react": "^19.2.9",
37
- "eslint": "^8.56.0",
38
- "react": "^19.2.3",
39
- "react-dom": "^19.2.3",
40
- "typescript": "^5.9.3",
41
- "vite": "^6.4.1"
34
+ "@telegraph/prettier-config": "^0.0.8",
35
+ "@telegraph/vite-config": "^0.0.17",
36
+ "@types/react": "^19.2.14",
37
+ "eslint": "^10.4.0",
38
+ "react": "^19.2.6",
39
+ "react-dom": "^19.2.6",
40
+ "typescript": "^6.0.2",
41
+ "vite": "^8.0.14"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": "^18.0.0 || ^19.0.0",