Skip to content

Commit ee2c5f2

Browse files
OEvgenycompulim
andauthored
Fix: update adaptive card in a single mutation to avoid layout shifts (#5494)
* Fix: re-render adaptive card in a single pass * Fix test * Changelog and deps * Apply suggestions from code review Co-authored-by: William Wong <compulim@users.noreply.github.com> --------- Co-authored-by: William Wong <compulim@users.noreply.github.com>
1 parent ebe5fac commit ee2c5f2

File tree

6 files changed

+132
-7
lines changed

6 files changed

+132
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,6 +1352,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
13521352
- Fixes [#3750](https://github.com/microsoft/BotFramework-WebChat/issues/3750). Debump Node.js engines requirements for some packages to `12.0.0`, by [@compulim](https://github.com/compulim) in PR [#3753](https://github.com/microsoft/BotFramework-WebChat/pull/3753)
13531353
- Fixes [#3760](https://github.com/microsoft/BotFramework-WebChat/issues/3760). Use `<ErrorBoundary>` to wrap around attachment renderer, by [@compulim](https://github.com/compulim) in PR [#3761](https://github.com/microsoft/BotFramework-WebChat/pull/3761)
13541354
- Fixes [#3764](https://github.com/microsoft/BotFramework-WebChat/issues/3764). Added `role="group"` to the focusable transcript to enable `aria-activedescendant`, by [@compulim](https://github.com/compulim) in PR [#3765](https://github.com/microsoft/BotFramework-WebChat/issues/3765)
1355+
- Fixed re-rendering Adaptive Card should not cause momentarily layout shift, by [@OEvgeny](https://github.com/OEvgeny), in PR [#5494](https://github.com/microsoft/BotFramework-WebChat/issues/5494)
13551356

13561357
### Changed
13571358

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
4+
<head>
5+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
6+
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone@7.8.7/babel.min.js"></script>
7+
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.6/umd/react.development.js"></script>
8+
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.development.js"></script>
9+
<script crossorigin="anonymous" src="/test-harness.js"></script>
10+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
11+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
12+
</head>
13+
14+
<body>
15+
<main id="webchat"></main>
16+
<script>
17+
const {
18+
React: { useMemo },
19+
WebChat: {
20+
hooks: { useDirection }
21+
}
22+
} = window;
23+
24+
run(async function () {
25+
const { directLine, store } = testHelpers.createDirectLineEmulator();
26+
27+
renderWebChat(
28+
{
29+
directLine,
30+
store,
31+
},
32+
document.getElementById('webchat')
33+
);
34+
35+
await pageConditions.uiConnected();
36+
37+
await directLine.emulateIncomingActivity({
38+
from: {
39+
role: "bot"
40+
},
41+
id: "a-00002",
42+
timestamp: 0,
43+
type: "message",
44+
attachments: [
45+
{
46+
contentType: 'application/vnd.microsoft.card.adaptive',
47+
content: {
48+
type: 'AdaptiveCard',
49+
body: [
50+
{
51+
type: 'TextBlock',
52+
text: 'This is the initial message',
53+
wrap: true
54+
}
55+
],
56+
actions: [
57+
{
58+
type: 'Action.Submit',
59+
title: 'Submit card'
60+
}
61+
],
62+
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
63+
version: '1.0'
64+
}
65+
}
66+
]
67+
});
68+
69+
const adaptiveCardEl = document.querySelector('.webchat__adaptive-card-renderer');
70+
const [currentRenderedChild] = adaptiveCardEl.children;
71+
72+
await new Promise(resolve => setTimeout(resolve, 200));
73+
74+
let observedMutations;
75+
new MutationObserver((mutations) => {
76+
observedMutations = mutations;
77+
}).observe(adaptiveCardEl, { childList: true });
78+
79+
// WHEN: we receive a new activity with the same ID
80+
await directLine.emulateIncomingActivity({
81+
from: {
82+
role: "bot"
83+
},
84+
id: "a-00002",
85+
timestamp: 0,
86+
type: "message",
87+
attachments: [
88+
{
89+
contentType: 'application/vnd.microsoft.card.adaptive',
90+
content: {
91+
type: 'AdaptiveCard',
92+
body: [
93+
{
94+
type: 'TextBlock',
95+
text: 'This is the message',
96+
wrap: true
97+
}
98+
],
99+
actions: [
100+
{
101+
type: 'Action.Submit',
102+
title: 'Submit card'
103+
}
104+
],
105+
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
106+
version: '1.0'
107+
}
108+
}
109+
]
110+
});
111+
112+
// THEN: the Adaptive Card is re-rendered resulting in a single DOM mutation
113+
expect(observedMutations.length).toBe(1);
114+
expect(observedMutations[0].type).toBe('childList');
115+
expect(observedMutations[0].addedNodes[0]).toBe(adaptiveCardEl.children[0]);
116+
expect(observedMutations[0].removedNodes[0]).toBe(currentRenderedChild);
117+
118+
await host.snapshot('local');
119+
});
120+
</script>
121+
</body>
122+
123+
</html>
10.4 KB
Loading

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/bundle/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
"shiki": "2.3.2",
139139
"swiper": "8.4.7",
140140
"url-search-params-polyfill": "8.2.5",
141+
"use-ref-from": "^0.1.0",
141142
"uuid": "8.3.2",
142143
"valibot": "1.1.0",
143144
"web-speech-cognitive-services": "8.1.1",

packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardRenderer.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import classNames from 'classnames';
88
import React, {
99
memo,
1010
useCallback,
11+
useEffect,
1112
useLayoutEffect,
1213
useMemo,
1314
useRef,
1415
type KeyboardEventHandler,
1516
type MouseEventHandler
1617
} from 'react';
1718
import { any, boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot';
19+
import { useRefFrom } from 'use-ref-from';
1820

1921
import useStyleSet from '../../hooks/useStyleSet';
2022
import useAdaptiveCardsHostConfig from '../hooks/useAdaptiveCardsHostConfig';
@@ -214,15 +216,12 @@ function AdaptiveCardRenderer(props: AdaptiveCardRendererProps) {
214216
}, [adaptiveCard, handleExecuteAction]);
215217

216218
useLayoutEffect(() => {
217-
const { current } = contentRef;
218-
219-
current?.appendChild(element);
220-
221-
return () => {
222-
current?.removeChild(element);
223-
};
219+
contentRef.current?.replaceChildren(element);
224220
}, [contentRef, element]);
225221

222+
const elementRef = useRefFrom(element);
223+
useEffect(() => () => elementRef.current?.remove(), [elementRef]);
224+
226225
// Apply all mods regardless whether the element changed or not.
227226
// This is because we have undoed mods when we call the `useXXXModEffect` hook.
228227
useLayoutEffect(() => {

0 commit comments

Comments
 (0)