diff --git a/packages/hyperion-autologging/src/ALDOMSnaptshotPublisher.ts b/packages/hyperion-autologging/src/ALDOMSnaptshotPublisher.ts index b4085d56..50da5ddb 100644 --- a/packages/hyperion-autologging/src/ALDOMSnaptshotPublisher.ts +++ b/packages/hyperion-autologging/src/ALDOMSnaptshotPublisher.ts @@ -14,7 +14,7 @@ import * as ALSurfaceVisibilityPublisher from "./ALSurfaceVisibilityPublisher"; import { setEventExtension } from "./ALEventExtension"; -type TrackingChannels = (ALUIEventPublisher.InitOptions & ALSurfaceVisibilityPublisher.InitOptions)['channel']; +type TrackingChannels = (Omit & ALSurfaceVisibilityPublisher.InitOptions)['channel']; type TrackingEvents = ChannelEventType; type TrackingEventNames = (keyof TrackingEvents) & ('al_ui_event' | 'al_surface_visibility_event'); // & is added to ensure the event names are a correct subset @@ -84,4 +84,4 @@ export function publish(options: InitOptions): void { } } }); -} \ No newline at end of file +} diff --git a/packages/hyperion-autologging/src/ALElementValuePublisher.ts b/packages/hyperion-autologging/src/ALElementValuePublisher.ts index c77115af..bd845331 100644 --- a/packages/hyperion-autologging/src/ALElementValuePublisher.ts +++ b/packages/hyperion-autologging/src/ALElementValuePublisher.ts @@ -25,7 +25,7 @@ import { ALSurfaceData } from "./ALSurfaceData"; export type InitOptions = Types.Options< - ALUIEventPublisher.InitOptions & + Omit & ALSharedInitOptions> >; diff --git a/packages/hyperion-autologging/src/ALInteractableDOMElement.ts b/packages/hyperion-autologging/src/ALInteractableDOMElement.ts index c13aad2e..d8217d3a 100644 --- a/packages/hyperion-autologging/src/ALInteractableDOMElement.ts +++ b/packages/hyperion-autologging/src/ALInteractableDOMElement.ts @@ -71,6 +71,8 @@ type InteractableAncestorCache = { [index: string]: (Element | null)[]; } +export const IGNORE_INTERACTIVITY_VP = `ignoreInteractivity`; + let getInteractableImpl: (node: Element, eventName: UIEventConfig['eventName'], requireHandlerAssigned: boolean) => Element | null = (node, eventName, requireHandlerAssigned) => { function getInteractableOptimized(node: Element, eventName: UIEventConfig['eventName'], requireHandlerAssigned: boolean, selectorString?: string): Element | null { /** @@ -152,10 +154,9 @@ let ignoreInteractiveElement: (node: Element) => boolean = node => { (node.clientHeight === window.innerHeight && node.clientWidth === window.innerWidth); } - const IgnoreInteractivity = `ignoreInteractivity`; function ignoreInteractiveElementOptimized(node: Element): boolean { let shouldIgnore: boolean | undefined; - shouldIgnore = getVirtualPropertyValue(node, IgnoreInteractivity); + shouldIgnore = getVirtualPropertyValue(node, IGNORE_INTERACTIVITY_VP); if (shouldIgnore === false || shouldIgnore === true) { return shouldIgnore; } @@ -163,7 +164,7 @@ let ignoreInteractiveElement: (node: Element) => boolean = node => { if (shouldIgnore) { // In some cases, the size of the component changes slightly and can change the answer from false to true. // So only caching the true values to be safe. - setVirtualPropertyValue(node, IgnoreInteractivity, shouldIgnore); + setVirtualPropertyValue(node, IGNORE_INTERACTIVITY_VP, shouldIgnore); } return shouldIgnore; } @@ -274,6 +275,7 @@ let installHandlers = () => { } }); + // for (const eventHandler of [ // IGlobalEventHandlers.onabort, // IGlobalEventHandlers.onanimationcancel, @@ -525,7 +527,7 @@ function getTextFromElementsByIds(domSource: ALDOMTextSource, source: ALElementT for (let i = 0; i < indirectSources.length; i++) { if (i) { - results.push({ text: " ", source, elements: []}); // Insert space between values + results.push({ text: " ", source, elements: [] }); // Insert space between values } domSource.element = indirectSources[i]; diff --git a/packages/hyperion-autologging/src/ALUIEventPublisher.ts b/packages/hyperion-autologging/src/ALUIEventPublisher.ts index a1499e91..346631b7 100644 --- a/packages/hyperion-autologging/src/ALUIEventPublisher.ts +++ b/packages/hyperion-autologging/src/ALUIEventPublisher.ts @@ -5,7 +5,7 @@ 'use strict'; import type * as Types from "hyperion-util/src/Types"; -import { intercept } from "hyperion-core/src/intercept"; +import { intercept, setVirtualPropertyValue } from "hyperion-core/src/intercept"; import * as IEvent from "hyperion-dom/src/IEvent"; import { setTriggerFlowlet } from "hyperion-flowlet/src/TriggerFlowlet"; import { TimedTrigger } from "hyperion-timed-trigger/src/TimedTrigger"; @@ -13,7 +13,7 @@ import performanceAbsoluteNow from "hyperion-util/src/performanceAbsoluteNow"; import ALElementInfo from './ALElementInfo'; import * as ALEventIndex from "./ALEventIndex"; import { ALID, getOrSetAutoLoggingID } from "./ALID"; -import { ALElementTextEvent, TrackEventHandlerConfig, enableUIEventHandlers, getElementTextEvent, getInteractable, isTrackedEvent } from "./ALInteractableDOMElement"; +import { ALElementTextEvent, IGNORE_INTERACTIVITY_VP, TrackEventHandlerConfig, enableUIEventHandlers, getElementTextEvent, getInteractable, isTrackedEvent } from "./ALInteractableDOMElement"; import { ReactComponentData } from "./ALReactUtils"; import { getSurfacePath } from "./ALSurfaceUtils"; import { ALElementEvent, ALExtensibleEvent, ALFlowletEvent, ALLoggableEvent, ALMetadataEvent, ALPageEvent, ALReactElementEvent, ALSharedInitOptions, ALTimedEvent, Metadata } from "./ALType"; @@ -21,6 +21,7 @@ import * as ALUIEventGroupPublisher from "./ALUIEventGroupPublisher"; import * as Flags from "hyperion-globals/src/Flags"; import { getCurrMainPageUrl } from "./MainPageUrl"; import { ALSurfaceData, ALSurfaceEvent } from "./ALSurfaceData"; +import * as IReactDOM from "hyperion-react/src/IReactDOM"; /** @@ -146,6 +147,9 @@ export type UIEventConfig = UIEventConfigMap[keyof Omit & { + react: { + IReactDOMClientModule: IReactDOM.IReactDOMClientModuleExports | Promise; + } uiEvents: Array; } >; @@ -239,9 +243,32 @@ export function getCurrentUIEventData(): ALUIEventData | null | undefined { * and filtering of events is configured via {@link UIEventConfig}. */ export function publish(options: InitOptions): void { - const { uiEvents, flowletManager, channel } = options; + const { uiEvents, flowletManager, channel, react } = options; + + if (react.IReactDOMClientModule instanceof Promise) { + react.IReactDOMClientModule.then(iReactDOMClientModule => { + publish({ + ...options, + react: { + ...options.react, + IReactDOMClientModule: iReactDOMClientModule, + } + }) + }); + return; + } + // Initialize root interception, for ignoring these containers as interactable elements + react.IReactDOMClientModule.createRoot.onBeforeCallMapperAdd(args => { + const [container] = args; + if (container instanceof Element) { + setVirtualPropertyValue(container, IGNORE_INTERACTIVITY_VP, true); + } + + return args; + }); + // Track all events uiEvents.forEach((eventConfig => { const { eventName, diff --git a/packages/hyperion-autologging/src/AutoLogging.ts b/packages/hyperion-autologging/src/AutoLogging.ts index 3d5930e9..766dd0ca 100644 --- a/packages/hyperion-autologging/src/AutoLogging.ts +++ b/packages/hyperion-autologging/src/AutoLogging.ts @@ -51,7 +51,7 @@ type PluginInit = (channel: Channel) => void; export type InitOptions = Types.Options< ALSharedInitOptions & { - react: (ALSurface.InitOptions & ALTriggerFlowlet.InitOptions)['react']; + react: (ALSurface.InitOptions & ALTriggerFlowlet.InitOptions & ALUIEventPublisher.InitOptions)['react']; enableReactComponentVisitors?: boolean; componentNameValidator?: ComponentNameValidator; flowletPublisher?: PublicInitOptions | null; @@ -190,7 +190,8 @@ export function init(options: InitOptions): boolean { if (options.uiEventPublisher) { ALUIEventPublisher.publish({ ...sharedOptions, - ...options.uiEventPublisher + ...options.uiEventPublisher, + react: options.react, }); ALHoverPublisher.publish({ diff --git a/packages/hyperion-autologging/test/ALDOMSnapshotPublisher.test.ts b/packages/hyperion-autologging/test/ALDOMSnapshotPublisher.test.ts index 1fa0f62d..6781bc4e 100644 --- a/packages/hyperion-autologging/test/ALDOMSnapshotPublisher.test.ts +++ b/packages/hyperion-autologging/test/ALDOMSnapshotPublisher.test.ts @@ -12,12 +12,15 @@ import * as ALUIEventPublisher from "../src/ALUIEventPublisher"; import * as DomFragment from "./DomFragment"; import * as ALDOMSnapshotPublisher from "../src/ALDOMSnaptshotPublisher"; import { ALChannelCustomEvent } from "../src/ALCustomEvent"; +import * as IReactDOM from "hyperion-react/src/IReactDOM"; +import * as ReactDOMClient from "react-dom/client"; describe("dom snapshot publisher", () => { test("dom snapshot taken for clicks", (done) => { const flowletManager = new ALFlowletManager(); const channel = new Channel(); + const IReactDOMClientModule = IReactDOM.interceptDOMClient("react-dom/client", ReactDOMClient, []); ALUIEventPublisher.publish({ flowletManager, @@ -27,7 +30,10 @@ describe("dom snapshot publisher", () => { cacheElementReactInfo: true, eventName: 'click', } - ] + ], + react: { + IReactDOMClientModule + } }); ALDOMSnapshotPublisher.publish({ flowletManager, diff --git a/packages/hyperion-autologging/test/ALUIEventPublisher.test.ts b/packages/hyperion-autologging/test/ALUIEventPublisher.test.ts index 7b7f8ee3..0cdfe01d 100644 --- a/packages/hyperion-autologging/test/ALUIEventPublisher.test.ts +++ b/packages/hyperion-autologging/test/ALUIEventPublisher.test.ts @@ -10,12 +10,15 @@ import { Channel } from "hyperion-channel/src/Channel"; import { ALFlowletManager } from "../src/ALFlowletManager"; import * as ALUIEventPublisher from "../src/ALUIEventPublisher"; import * as DomFragment from "./DomFragment"; +import * as IReactDOM from "hyperion-react/src/IReactDOM"; +import * as ReactDOMClient from "react-dom/client"; describe("UI event publisher", () => { test("meta data transfer between capture and bubble", (done) => { const flowletManager = new ALFlowletManager(); const channel = new Channel(); + const IReactDOMClientModule = IReactDOM.interceptDOMClient("react-dom/client", ReactDOMClient, []); ALUIEventPublisher.publish({ flowletManager, @@ -25,7 +28,10 @@ describe("UI event publisher", () => { cacheElementReactInfo: true, eventName: 'click', } - ] + ], + react: { + IReactDOMClientModule, + } }); channel.addListener('al_ui_event_capture', event => { diff --git a/packages/hyperion-react-testapp/src/AutoLoggingWrapper.ts b/packages/hyperion-react-testapp/src/AutoLoggingWrapper.ts index b4a9d543..9cf7effc 100644 --- a/packages/hyperion-react-testapp/src/AutoLoggingWrapper.ts +++ b/packages/hyperion-react-testapp/src/AutoLoggingWrapper.ts @@ -10,6 +10,7 @@ import * as IReactDOM from "hyperion-react/src/IReactDOM"; import { ClientSessionID, getDomainSessionID } from "hyperion-util/src/ClientSessionID"; import React from 'react'; import * as ReactDOM from "react-dom"; +import * as ReactDOMClient from "react-dom/client"; import ReactDev from "react/jsx-dev-runtime"; import { SyncChannel } from "./Channel"; import { FlowletManager } from "./FlowletManager"; @@ -37,7 +38,8 @@ export function init() { const IReactModule = IReact.intercept("react", React, []) const IJsxRuntimeModule = IReact.interceptRuntime("react/jsx-dev-runtime", ReactDev as any, []); - const IReactDOMModule = IReactDOM.intercept("react-dom", ReactDOM, []); + const IReactDOMModule = IReactDOM.interceptDOM("react-dom", ReactDOM, []); + const IReactDOMClientModule = IReactDOM.interceptDOMClient("react-dom/client", ReactDOMClient, []); const channel = SyncChannel; @@ -82,6 +84,7 @@ export function init() { react: { ReactModule: React as any, IReactDOMModule, + IReactDOMClientModule, IReactModule, IJsxRuntimeModule, }, diff --git a/packages/hyperion-react/src/IReactDOM.ts b/packages/hyperion-react/src/IReactDOM.ts index 8fa88cf9..582a218b 100644 --- a/packages/hyperion-react/src/IReactDOM.ts +++ b/packages/hyperion-react/src/IReactDOM.ts @@ -3,6 +3,7 @@ */ import type ReactDOM from "react-dom"; +import type { createRoot } from "react-dom/client"; import { InterceptedModuleExports, interceptModuleExports, ModuleExportsKeys } from 'hyperion-core/src/IRequire'; @@ -13,9 +14,24 @@ export type ReactDOMModuleExports = { export type IReactDOMModuleExports = InterceptedModuleExports; let IReactDOMModule: IReactDOMModuleExports | null = null; -export function intercept(moduleId: string, moduleExports: ReactDOMModuleExports, failedExportsKeys?: ModuleExportsKeys): IReactDOMModuleExports { +export type ReactDOMClientModuleExports = { + createRoot: typeof createRoot; +} + +export type IReactDOMClientModuleExports = InterceptedModuleExports; +let IReactDOMClientModule: IReactDOMClientModuleExports | null = null; + + +export function interceptDOM(moduleId: string, moduleExports: ReactDOMModuleExports, failedExportsKeys?: ModuleExportsKeys): IReactDOMModuleExports { if (!IReactDOMModule) { IReactDOMModule = interceptModuleExports(moduleId, moduleExports, ['createPortal'], failedExportsKeys); } return IReactDOMModule; } + +export function interceptDOMClient(moduleId: string, moduleExports: ReactDOMClientModuleExports, failedExportsKeys?: ModuleExportsKeys): IReactDOMClientModuleExports { + if (!IReactDOMClientModule) { + IReactDOMClientModule = interceptModuleExports(moduleId, moduleExports, ['createRoot'], failedExportsKeys); + } + return IReactDOMClientModule; +}