diff --git a/packages/hyperion-autologging/src/ALSurface.ts b/packages/hyperion-autologging/src/ALSurface.ts index 9688cf51..082dbb5a 100644 --- a/packages/hyperion-autologging/src/ALSurface.ts +++ b/packages/hyperion-autologging/src/ALSurface.ts @@ -13,7 +13,7 @@ import * as IReactElementVisitor from 'hyperion-react/src/IReactElementVisitor'; import * as IReactFlowlet from "hyperion-react/src/IReactFlowlet"; import * as IReactPropsExtension from "hyperion-react/src/IReactPropsExtension"; import * as Types from "hyperion-util/src/Types"; -import type * as React from 'react'; +import * as React from 'react'; import { ALFlowletDataType, IALFlowlet } from "./ALFlowletManager"; import { AUTO_LOGGING_NON_INTERACTIVE_SURFACE, AUTO_LOGGING_SURFACE, SURFACE_SEPARATOR, SURFACE_WRAPPER_ATTRIBUTE_NAME } from './ALSurfaceConsts'; import * as ALSurfaceContext from "./ALSurfaceContext"; @@ -66,7 +66,7 @@ export type ALSurfaceProps = Readonly<{ metadata?: ALMetadataEvent['metadata']; uiEventMetadata?: EventMetadata, capability?: ALSurfaceCapability, - nodeRef?: React.RefObject, + nodeRef?: React.Ref, }>; export type ALSurfaceRenderer = (node: React.ReactNode) => React.ReactElement; @@ -236,6 +236,17 @@ function setupDomElementSurfaceAttribute(options: InitOptions): void { }); } +function getRefElement(ref: React.Ref | undefined | null): T | null { + if (!ref) return null; + if (typeof ref === 'function') { + // Function refs don't expose current; you can't get element here safely + // unless you track it yourself, so return null + return null; + } else if ('current' in ref) { + return ref.current; + } + return null; +} export function init(options: InitOptions): ALSurfaceHOC { const { flowletManager, channel } = options; @@ -259,7 +270,7 @@ export function init(options: InitOptions): ALSurfaceHOC { const { surface: parentSurface, nonInteractiveSurface: parentNonInteractiveSurface } = surfaceCtx; let addSurfaceWrapper = props.nodeRef == null; - let localRef = ReactModule.useRef(); + const localRef = ReactModule.useRef(null); // empty .capability field is default, means all enabled! const capability = props.capability ?? proxiedContext?.mainContext.capability; @@ -323,6 +334,16 @@ export function init(options: InitOptions): ALSurfaceHOC { const isProxy = proxiedContext != null; + const mergedRef = (node: Element | null) => { + const htmlNode = node instanceof HTMLElement ? node : null; + if (typeof props.nodeRef === 'function') { + props.nodeRef(htmlNode); + } else if (props.nodeRef && 'current' in props.nodeRef) { + (props.nodeRef as React.MutableRefObject).current = node; + } + localRef.current = node; + }; + metadata.original_call_flowlet = callFlowlet.getFullName(); metadata.surface_capability = surfaceCapabilityToString(capability); // Update the metadata on every render to ensure it stays current @@ -342,9 +363,10 @@ export function init(options: InitOptions): ALSurfaceHOC { */ const nodeRef = props.nodeRef ?? localRef; + ReactModule.useLayoutEffect(() => { __DEV__ && assert(nodeRef != null, "Invalid surface effect without a ref: " + surface); - const element = nodeRef.current; + const element = getRefElement(nodeRef as React.Ref); if (element == null) { return; } @@ -435,7 +457,7 @@ export function init(options: InitOptions): ALSurfaceHOC { [SURFACE_WRAPPER_ATTRIBUTE_NAME]: "1", style: { display: 'contents' }, [domAttributeName]: domAttributeValue, - ref: localRef, // addSurfaceWrapper would have been false if a rep was passed in props + ref: mergedRef, // addSurfaceWrapper would have been false if a rep was passed in props }, props.children ); diff --git a/packages/hyperion-react-testapp/src/App.tsx b/packages/hyperion-react-testapp/src/App.tsx index fdaff6e4..4f3a333c 100644 --- a/packages/hyperion-react-testapp/src/App.tsx +++ b/packages/hyperion-react-testapp/src/App.tsx @@ -18,6 +18,7 @@ import { PortalBodyContainerComponent } from './component/PortalComponent'; import TextComponent from './component/TextComponent'; import RecursiveFuncComponent from './component/RecursiveFuncComponent'; import SVGClickComponent from './component/SVGClickComponent'; +import { RefNodeComponent } from './component/RefNodeComponent'; function InitComp() { const [count, setCount] = React.useState(0); @@ -60,6 +61,9 @@ const Modes = { , + 'RefNode': () =>
+ +
, }; type ModeNames = keyof typeof Modes; const PersistedOptionValue = new LocalStoragePersistentData( diff --git a/packages/hyperion-react-testapp/src/component/RefNodeComponent.tsx b/packages/hyperion-react-testapp/src/component/RefNodeComponent.tsx new file mode 100644 index 00000000..028505af --- /dev/null +++ b/packages/hyperion-react-testapp/src/component/RefNodeComponent.tsx @@ -0,0 +1,71 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved. + */ + +import React, { useEffect, useRef, useCallback, useState } from 'react'; +import { SurfaceComp } from './Surface'; + +// Utility to merge multiple refs +function mergeRefs(...refs: (React.Ref | undefined)[]): React.RefCallback { + return (node: T) => { + refs.forEach(ref => { + if (typeof ref === 'function') { + ref(node); + } else if (ref && 'current' in ref) { + (ref as React.MutableRefObject).current = node; + } + }); + }; +} + +const ComponenetWithRefs: React.FC<{ externalRef?: React.Ref }> = ({ externalRef }) => { + const internalRef = useRef(null); + + const combinedRef = mergeRefs(externalRef, internalRef); + + useEffect(() => { + if (internalRef.current) { + internalRef.current.style.border = '2px solid blue'; + internalRef.current.textContent = 'Merged using useMergeRefs!'; + } + }, []); + + return ( +
+ A box with merged refs. +
+ ); +}; + +// Component that uses a merged ref +type RefNodeComponentProps = { + externalRef?: React.Ref; +}; + +export const RefNodeComponent: React.FC = () => { + const [element, setElement] = useState(null); + + // External callback ref + const externalRef = (node: HTMLElement | null) => { + console.log('External ref called with:', node); + setElement(node); + }; + + return ( + +
+

React Merged Ref Example

+ + {element && ( +

+ The tag name of the externally tracked element is: {element.tagName} +

+ )} +
+
+ ); + +};