diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20215a771d..8014750865 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]
+## [4.15.1] - 2022-03-04
+
+### Fixed
+
+- Fixes [#4196](https://github.com/microsoft/BotFramework-WebChat/issues/4196). Should render/mount to a detached DOM node without errors, by [@compulim](https://github.com/compulim), in PR [#4197](https://github.com/microsoft/BotFramework-WebChat/issues/4197)
+
## [4.15.0] - 2022-03-03
### Breaking changes
diff --git a/__tests__/__image_snapshots__/html/simple-detached-js-should-render-on-a-detached-node-1-snap.png b/__tests__/__image_snapshots__/html/simple-detached-js-should-render-on-a-detached-node-1-snap.png
new file mode 100644
index 0000000000..7384212f82
Binary files /dev/null and b/__tests__/__image_snapshots__/html/simple-detached-js-should-render-on-a-detached-node-1-snap.png differ
diff --git a/__tests__/html/simple.detached.html b/__tests__/html/simple.detached.html
new file mode 100644
index 0000000000..acb7b71a09
--- /dev/null
+++ b/__tests__/html/simple.detached.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__tests__/html/simple.detached.js b/__tests__/html/simple.detached.js
new file mode 100644
index 0000000000..c806d29ae9
--- /dev/null
+++ b/__tests__/html/simple.detached.js
@@ -0,0 +1,3 @@
+/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */
+
+test('should render on a detached node', () => runHTML('simple.detached.html'));
diff --git a/package-lock.json b/package-lock.json
index 1e36e9030f..57f69ae77f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "botframework-webchat-root",
- "version": "4.15.0",
+ "version": "4.15.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index b0493151d2..d7e3638539 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "botframework-webchat-root",
- "version": "4.15.0",
+ "version": "4.15.1",
"private": true,
"files": [
"lib/**/*"
diff --git a/packages/component/src/BasicTranscript.tsx b/packages/component/src/BasicTranscript.tsx
index 3bf014ed2b..a4f854b6b4 100644
--- a/packages/component/src/BasicTranscript.tsx
+++ b/packages/component/src/BasicTranscript.tsx
@@ -267,16 +267,22 @@ const InternalTranscript = forwardRef(
if (scrollableElement && activityBoundingBoxElement) {
// ESLint conflict with TypeScript. The result of getClientRects() is not an Array but DOMRectList, and cannot be destructured.
// eslint-disable-next-line prefer-destructuring
- const { height: activityHeight, y: activityY } = activityBoundingBoxElement.getClientRects()[0];
+ const activityBoundingBoxElementClientRect = activityBoundingBoxElement.getClientRects()[0];
// ESLint conflict with TypeScript. The result of getClientRects() is not an Array but DOMRectList, and cannot be destructured.
// eslint-disable-next-line prefer-destructuring
- const { height: scrollableHeight } = scrollableElement.getClientRects()[0];
- const activityOffsetTop = activityY + scrollableElement.scrollTop;
+ const scrollableElementClientRect = scrollableElement.getClientRects()[0];
- const scrollTop = Math.min(activityOffsetTop, activityOffsetTop - scrollableHeight + activityHeight);
+ // If either the activity or the transcript scrollable is not on DOM, we will not scroll the view.
+ if (activityBoundingBoxElementClientRect && scrollableElementClientRect) {
+ const { height: activityHeight, y: activityY } = activityBoundingBoxElementClientRect;
+ const { height: scrollableHeight } = scrollableElementClientRect;
+ const activityOffsetTop = activityY + scrollableElement.scrollTop;
- scrollToBottomScrollTo(scrollTop, { behavior });
+ const scrollTop = Math.min(activityOffsetTop, activityOffsetTop - scrollableHeight + activityHeight);
+
+ scrollToBottomScrollTo(scrollTop, { behavior });
+ }
}
}
},
@@ -342,7 +348,14 @@ const InternalTranscript = forwardRef(
// "getClientRects()" is not returning an array, thus, it is not destructurable.
// eslint-disable-next-line prefer-destructuring
- const { bottom: scrollableClientBottom } = scrollableElement.getClientRects()[0];
+ const scrollableElementClientRect = scrollableElement.getClientRects()[0];
+
+ // If the scrollable is not mounted, we cannot measure which activity is in view. Thus, we will not fire any events.
+ if (!scrollableElementClientRect) {
+ return;
+ }
+
+ const { bottom: scrollableClientBottom } = scrollableElementClientRect;
// Find the activity just above scroll view bottom.
// If the scroll view is already on top, get the first activity.
@@ -352,7 +365,14 @@ const InternalTranscript = forwardRef(
? activityElements
.reverse()
// Add subpixel tolerance
- .find(([, element]) => element.getClientRects()[0].bottom < scrollableClientBottom + 1)
+ .find(([, element]) => {
+ // "getClientRects()" is not returning an array, thus, it is not destructurable.
+ // eslint-disable-next-line prefer-destructuring
+ const elementClientRect = element.getClientRects()[0];
+
+ // If the activity is not attached to DOM tree, we should not count it as "bottommost visible activity", as it is not visible.
+ return elementClientRect && elementClientRect.bottom < scrollableClientBottom + 1;
+ })
: activityElements[0]
)?.[0];