From cab332f7a679673718d11b1d025d232bd4978f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thiago=20L=C3=B6pes?= Date: Mon, 30 Mar 2026 18:44:08 -0300 Subject: [PATCH 1/3] Add translation complete message handling --- js/src/launchAsync.ts | 49 +++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/js/src/launchAsync.ts b/js/src/launchAsync.ts index 86118924..d52f98a4 100644 --- a/js/src/launchAsync.ts +++ b/js/src/launchAsync.ts @@ -35,6 +35,7 @@ const sdkVersion = VERSION; const PostMessagePreferences = 'ImmersiveReader-Preferences:'; const PostMessageLaunchResponse = 'ImmersiveReader-LaunchResponse:'; +const PostMessageTranslationComplete = 'ImmersiveReader-TranslationComplete'; const errorMessageMap: { [errorCode: string]: string } = {}; errorMessageMap[ErrorCode.TokenExpired] = 'The access token supplied is expired.'; @@ -47,7 +48,7 @@ let isLoading: boolean = false; /** * Launch the Immersive Reader within an iframe. * @param token The authentication token. - * @param subdomain The Immersive Reader Cognitive Service subdomain. This is a required parameter for Azure AD authentication in this and future versions of this SDK. Use of the Cognitive Services issueToken endpoint-based authentication tokens is deprecated and no longer supported. + * @param subdomain The Immersive Reader Cognitive Service subdomain. * @param content The content that should be shown in the Immersive Reader. * @param options Options for configuring the look and feel of the Immersive Reader. * @return A promise that resolves with a LaunchResponse when the Immersive Reader is launched. @@ -95,7 +96,6 @@ export function launchAsync(token: string, subdomain: string, content: Content, ...options }; - // Ensure that we were given a number for the UI z-index if (!options.uiZIndex || typeof options.uiZIndex !== 'number') { options.uiZIndex = 1000; } @@ -119,17 +119,11 @@ export function launchAsync(token: string, subdomain: string, content: Content, const parent = options.parent && document.contains(options.parent) ? options.parent : document.body; const reset = (): void => { - // Remove container along with the iframe inside of it if (parent.contains(iframeContainer)) { parent.removeChild(iframeContainer); } - window.removeEventListener('message', messageHandler); - - // Clear the timeout timer resetTimeout(); - - // Re-enable scrolling if (noscroll.parentNode) { noscroll.parentNode.removeChild(noscroll); } @@ -138,8 +132,6 @@ export function launchAsync(token: string, subdomain: string, content: Content, const exit = (): void => { reset(); isLoading = false; - - // Execute exit callback if we have one if (options.onExit) { try { options.onExit(); @@ -149,22 +141,28 @@ export function launchAsync(token: string, subdomain: string, content: Content, } }; - // Reset variables reset(); const messageHandler = (e: any): void => { - // Don't process the message if the data is not a string if (!e || !e.data || typeof e.data !== 'string') { return; } if (e.data === 'ImmersiveReader-ReadyForContent') { - resetTimeout(); // Reset the timeout once the reader page loads successfully. The Reader page will report further errors through PostMessage if there is an issue obtaining the ContentModel from the server + resetTimeout(); + + // Fix for Race Condition: autoEnableDocumentTranslation + autoplay + const isAutoTranslate = options.translationOptions?.autoEnableDocumentTranslation; + const isAutoplay = options.readAloudOptions?.autoplay; + const message: Message = { cogSvcsAccessToken: token, cogSvcsSubdomain: subdomain, request: content, launchToPostMessageSentDurationInMs: Date.now() - startTime, disableFirstRun: options.disableFirstRun, - readAloudOptions: options.readAloudOptions, + // If both are enabled, we disable autoplay here and trigger it after translation + readAloudOptions: (isAutoTranslate && isAutoplay) + ? { ...options.readAloudOptions, autoplay: false } + : options.readAloudOptions, translationOptions: options.translationOptions, displayOptions: options.displayOptions, sendPreferences: !!options.onPreferencesChanged, @@ -174,12 +172,18 @@ export function launchAsync(token: string, subdomain: string, content: Content, disableLanguageDetection: options.disableLanguageDetection }; iframe.contentWindow!.postMessage(JSON.stringify({ messageType: 'Content', messageValue: message }), '*'); + + } else if (e.data === PostMessageTranslationComplete) { + // Trigger play if it was deferred due to translation + if (options.translationOptions?.autoEnableDocumentTranslation && options.readAloudOptions?.autoplay) { + iframe.contentWindow!.postMessage(JSON.stringify({ messageType: 'Play' }), '*'); + } + } else if (e.data === 'ImmersiveReader-Exit') { exit(); } else if (e.data.startsWith(PostMessageLaunchResponse)) { let launchResponse: LaunchResponse = null; let error: Error = null; - let response: LaunchResponseMessage = null; try { response = JSON.parse(e.data.substring(PostMessageLaunchResponse.length)); @@ -188,7 +192,6 @@ export function launchAsync(token: string, subdomain: string, content: Content, } if (response && response.success) { - launchResponse = { container: iframeContainer, sessionId: response.sessionId, @@ -225,22 +228,20 @@ export function launchAsync(token: string, subdomain: string, content: Content, } } }; + window.addEventListener('message', messageHandler); - // Reject the promise if the Immersive Reader page fails to load. timeoutId = window.setTimeout((): void => { reset(); isLoading = false; reject({ code: ErrorCode.Timeout, message: `Page failed to load after timeout (${options.timeout} ms)` }); }, options.timeout); - // Create and style iframe if (options.allowFullscreen) { iframe.setAttribute('allowfullscreen', ''); } iframe.style.cssText = options.parent ? 'position: static; width: 100%; height: 100%; left: 0; top: 0; border-width: 0' : 'position: static; width: 100vw; height: 100vh; left: 0; top: 0; border-width: 0'; - // Send an initial message to the webview so it has a reference to this parent window if (options.useWebview) { iframe.addEventListener('loadstop', () => { iframe.contentWindow.postMessage(JSON.stringify({ messageType: 'WebviewHost' }), '*'); @@ -249,7 +250,6 @@ export function launchAsync(token: string, subdomain: string, content: Content, const domain = options.customDomain ? options.customDomain : `https://${subdomain}.cognitiveservices.azure.com/immersivereader/webapp/v1.0/`; let src = domain + 'reader?exitCallback=ImmersiveReader-Exit&sdkPlatform=' + sdkPlatform + '&sdkVersion=' + sdkVersion; - src += '&cookiePolicy=' + ((options.cookiePolicy === CookiePolicy.Enable) ? 'enable' : 'disable'); if (options.hideExitButton) { @@ -261,13 +261,10 @@ export function launchAsync(token: string, subdomain: string, content: Content, } iframe.src = src; - iframeContainer.style.cssText = options.parent ? `position: relative; width: 100%; height: 100%; border-width: 0; -webkit-perspective: 1px; z-index: ${options.uiZIndex}; background: white; overflow: hidden` : `position: fixed; width: 100vw; height: 100vh; left: 0; top: 0; border-width: 0; -webkit-perspective: 1px; z-index: ${options.uiZIndex}; background: white; overflow: hidden`; iframeContainer.appendChild(iframe); parent.appendChild(iframeContainer); - - // Disable body scrolling document.head.appendChild(noscroll); }); } @@ -276,15 +273,11 @@ export function close(): void { window.postMessage('ImmersiveReader-Exit', '*'); } -// The subdomain must be alphanumeric, and may contain '-', -// as long as the '-' does not start or end the subdomain. export function isValidSubdomain(subdomain: string): boolean { if (!subdomain) { return false; } - const validRegex = '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]\.privatelink)$'; const regExp = new RegExp(validRegex); - return regExp.test(subdomain); -} \ No newline at end of file +} From 53c44de8304f5c84983c9f19a77ac33c279d1b4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thiago=20L=C3=B6pes?= Date: Tue, 31 Mar 2026 18:40:47 -0300 Subject: [PATCH 2/3] Refactor message handling and clean up code --- js/src/launchAsync.ts | 62 +++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 38 deletions(-) diff --git a/js/src/launchAsync.ts b/js/src/launchAsync.ts index d52f98a4..c75746e7 100644 --- a/js/src/launchAsync.ts +++ b/js/src/launchAsync.ts @@ -35,7 +35,6 @@ const sdkVersion = VERSION; const PostMessagePreferences = 'ImmersiveReader-Preferences:'; const PostMessageLaunchResponse = 'ImmersiveReader-LaunchResponse:'; -const PostMessageTranslationComplete = 'ImmersiveReader-TranslationComplete'; const errorMessageMap: { [errorCode: string]: string } = {}; errorMessageMap[ErrorCode.TokenExpired] = 'The access token supplied is expired.'; @@ -88,7 +87,7 @@ export function launchAsync(token: string, subdomain: string, content: Content, const startTime = Date.now(); options = { uiZIndex: 1000, - timeout: 15000, // Default to 15 seconds + timeout: 15000, useWebview: false, allowFullscreen: true, hideExitButton: false, @@ -135,9 +134,7 @@ export function launchAsync(token: string, subdomain: string, content: Content, if (options.onExit) { try { options.onExit(); - } catch { - // No-op - } + } catch { /* No-op */ } } }; @@ -148,21 +145,13 @@ export function launchAsync(token: string, subdomain: string, content: Content, if (e.data === 'ImmersiveReader-ReadyForContent') { resetTimeout(); - - // Fix for Race Condition: autoEnableDocumentTranslation + autoplay - const isAutoTranslate = options.translationOptions?.autoEnableDocumentTranslation; - const isAutoplay = options.readAloudOptions?.autoplay; - const message: Message = { cogSvcsAccessToken: token, cogSvcsSubdomain: subdomain, request: content, launchToPostMessageSentDurationInMs: Date.now() - startTime, disableFirstRun: options.disableFirstRun, - // If both are enabled, we disable autoplay here and trigger it after translation - readAloudOptions: (isAutoTranslate && isAutoplay) - ? { ...options.readAloudOptions, autoplay: false } - : options.readAloudOptions, + readAloudOptions: options.readAloudOptions, translationOptions: options.translationOptions, displayOptions: options.displayOptions, sendPreferences: !!options.onPreferencesChanged, @@ -172,13 +161,6 @@ export function launchAsync(token: string, subdomain: string, content: Content, disableLanguageDetection: options.disableLanguageDetection }; iframe.contentWindow!.postMessage(JSON.stringify({ messageType: 'Content', messageValue: message }), '*'); - - } else if (e.data === PostMessageTranslationComplete) { - // Trigger play if it was deferred due to translation - if (options.translationOptions?.autoEnableDocumentTranslation && options.readAloudOptions?.autoplay) { - iframe.contentWindow!.postMessage(JSON.stringify({ messageType: 'Play' }), '*'); - } - } else if (e.data === 'ImmersiveReader-Exit') { exit(); } else if (e.data.startsWith(PostMessageLaunchResponse)) { @@ -187,9 +169,7 @@ export function launchAsync(token: string, subdomain: string, content: Content, let response: LaunchResponseMessage = null; try { response = JSON.parse(e.data.substring(PostMessageLaunchResponse.length)); - } catch { - // No-op - } + } catch { /* No-op */ } if (response && response.success) { launchResponse = { @@ -197,6 +177,22 @@ export function launchAsync(token: string, subdomain: string, content: Content, sessionId: response.sessionId, charactersProcessed: response.meteredContentSize }; + + /** + * WORKAROUND: Force a layout recalculation of the iframe. + * This prevents the "Cannot read properties of undefined (reading 'height')" error + * in the virtualized content pane when reading long HTML chunks. + */ + setTimeout(() => { + if (iframe) { + const originalHeight = iframe.style.height; + iframe.style.height = '99.9%'; + setTimeout(() => { + iframe.style.height = originalHeight; + }, 50); + } + }, 500); + } else if (response && !response.success) { error = { code: response.errorCode, @@ -222,13 +218,10 @@ export function launchAsync(token: string, subdomain: string, content: Content, if (options.onPreferencesChanged && typeof options.onPreferencesChanged === 'function') { try { options.onPreferencesChanged(e.data.substring(PostMessagePreferences.length)); - } catch { - // No-op - } + } catch { /* No-op */ } } } }; - window.addEventListener('message', messageHandler); timeoutId = window.setTimeout((): void => { @@ -252,13 +245,8 @@ export function launchAsync(token: string, subdomain: string, content: Content, let src = domain + 'reader?exitCallback=ImmersiveReader-Exit&sdkPlatform=' + sdkPlatform + '&sdkVersion=' + sdkVersion; src += '&cookiePolicy=' + ((options.cookiePolicy === CookiePolicy.Enable) ? 'enable' : 'disable'); - if (options.hideExitButton) { - src += '&hideExitButton=true'; - } - - if (options.uiLang) { - src += '&omkt=' + options.uiLang; - } + if (options.hideExitButton) { src += '&hideExitButton=true'; } + if (options.uiLang) { src += '&omkt=' + options.uiLang; } iframe.src = src; iframeContainer.style.cssText = options.parent ? `position: relative; width: 100%; height: 100%; border-width: 0; -webkit-perspective: 1px; z-index: ${options.uiZIndex}; background: white; overflow: hidden` : `position: fixed; width: 100vw; height: 100vh; left: 0; top: 0; border-width: 0; -webkit-perspective: 1px; z-index: ${options.uiZIndex}; background: white; overflow: hidden`; @@ -274,9 +262,7 @@ export function close(): void { } export function isValidSubdomain(subdomain: string): boolean { - if (!subdomain) { - return false; - } + if (!subdomain) { return false; } const validRegex = '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]\.privatelink)$'; const regExp = new RegExp(validRegex); return regExp.test(subdomain); From 2a0e3f076df33fb688a3f952c25f06499270b688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thiago=20L=C3=B6pes?= Date: Tue, 31 Mar 2026 18:47:17 -0300 Subject: [PATCH 3/3] fix: resolve TypeError in virtualized content pane by forcing layout recalculation Overview This PR addresses a recurring TypeError: Cannot read properties of undefined (reading 'height') that occurs in the Immersive Reader's internal virtualized content pane, specifically when processing long HTML chunks. The Problem In certain viewports and with specific HTML structures (like multiple

tags), the internal React virtualized list fails to calculate the height of a token/element before the SDK attempts to scroll it into view. This causes the reader to "hang" and stops the Read Aloud functionality, as reported in Issue #483 clsoses #483 --- js/src/launchAsync.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/js/src/launchAsync.ts b/js/src/launchAsync.ts index c75746e7..69108c3b 100644 --- a/js/src/launchAsync.ts +++ b/js/src/launchAsync.ts @@ -267,3 +267,4 @@ export function isValidSubdomain(subdomain: string): boolean { const regExp = new RegExp(validRegex); return regExp.test(subdomain); } +