diff --git a/.changelog/6047-fix-activity-styles.md b/.changelog/6047-fix-activity-styles.md new file mode 100644 index 0000000000..0655eaa750 --- /dev/null +++ b/.changelog/6047-fix-activity-styles.md @@ -0,0 +1,3 @@ +### Fixed + +- Fixed styles lost after a React 19.2 `` hides and shows Web Chat, in PR [#6047](https://github.com/microsoft/BotFramework-WebChat/pull/6047), by [@OEvgeny](https://github.com/OEvgeny) diff --git a/__tests__/html2/basic/twoWebChats.unmountOne.html b/__tests__/html2/basic/twoWebChats.unmountOne.html new file mode 100644 index 0000000000..3b1f12afd0 --- /dev/null +++ b/__tests__/html2/basic/twoWebChats.unmountOne.html @@ -0,0 +1,80 @@ + + + + Two Web Chats share their styles: the one left keeps them when the other is unmounted + + + + +
+
+
+
+ + + + diff --git a/__tests__/html2/basic/twoWebChats.unmountOne.html.snap-1.png b/__tests__/html2/basic/twoWebChats.unmountOne.html.snap-1.png new file mode 100644 index 0000000000..bfa6b485e1 Binary files /dev/null and b/__tests__/html2/basic/twoWebChats.unmountOne.html.snap-1.png differ diff --git a/__tests__/html2/basic/twoWebChats.unmountOne.html.snap-2.png b/__tests__/html2/basic/twoWebChats.unmountOne.html.snap-2.png new file mode 100644 index 0000000000..09b504cb9a Binary files /dev/null and b/__tests__/html2/basic/twoWebChats.unmountOne.html.snap-2.png differ diff --git a/__tests__/html2/react19/activity.hideShow.fluent.html b/__tests__/html2/react19/activity.hideShow.fluent.html new file mode 100644 index 0000000000..608e62f9ce --- /dev/null +++ b/__tests__/html2/react19/activity.hideShow.fluent.html @@ -0,0 +1,74 @@ + + + + React 19: Web Chat with the Fluent theme keeps its styles when an <Activity> hides and shows it + + + + + + + + + +
+ + + diff --git a/__tests__/html2/react19/activity.hideShow.fluent.html.snap-1.png b/__tests__/html2/react19/activity.hideShow.fluent.html.snap-1.png new file mode 100644 index 0000000000..0ece986df8 Binary files /dev/null and b/__tests__/html2/react19/activity.hideShow.fluent.html.snap-1.png differ diff --git a/__tests__/html2/react19/activity.hideShow.fluent.html.snap-2.png b/__tests__/html2/react19/activity.hideShow.fluent.html.snap-2.png new file mode 100644 index 0000000000..0ece986df8 Binary files /dev/null and b/__tests__/html2/react19/activity.hideShow.fluent.html.snap-2.png differ diff --git a/__tests__/html2/react19/activity.hideShow.html b/__tests__/html2/react19/activity.hideShow.html new file mode 100644 index 0000000000..a70b087ecf --- /dev/null +++ b/__tests__/html2/react19/activity.hideShow.html @@ -0,0 +1,74 @@ + + + + React 19: Web Chat keeps its styles when an <Activity> hides and shows it + + + +
+ + + + diff --git a/__tests__/html2/react19/activity.hideShow.html.snap-1.png b/__tests__/html2/react19/activity.hideShow.html.snap-1.png new file mode 100644 index 0000000000..c22dbf469d Binary files /dev/null and b/__tests__/html2/react19/activity.hideShow.html.snap-1.png differ diff --git a/__tests__/html2/react19/activity.hideShow.html.snap-2.png b/__tests__/html2/react19/activity.hideShow.html.snap-2.png new file mode 100644 index 0000000000..c22dbf469d Binary files /dev/null and b/__tests__/html2/react19/activity.hideShow.html.snap-2.png differ diff --git a/__tests__/html2/react19/activity.hideShow.sharedStyles.html b/__tests__/html2/react19/activity.hideShow.sharedStyles.html new file mode 100644 index 0000000000..30fc1b53e0 --- /dev/null +++ b/__tests__/html2/react19/activity.hideShow.sharedStyles.html @@ -0,0 +1,91 @@ + + + + React 19: Web Chat shown again by an <Activity> keeps its shared styles when the other unmounts + + + + +
+
+
+
+ + + + diff --git a/__tests__/html2/react19/activity.hideShow.sharedStyles.html.snap-1.png b/__tests__/html2/react19/activity.hideShow.sharedStyles.html.snap-1.png new file mode 100644 index 0000000000..6e43ee5068 Binary files /dev/null and b/__tests__/html2/react19/activity.hideShow.sharedStyles.html.snap-1.png differ diff --git a/__tests__/html2/react19/activity.hideShow.sharedStyles.html.snap-2.png b/__tests__/html2/react19/activity.hideShow.sharedStyles.html.snap-2.png new file mode 100644 index 0000000000..0eedbf0893 Binary files /dev/null and b/__tests__/html2/react19/activity.hideShow.sharedStyles.html.snap-2.png differ diff --git a/__tests__/html2/react19/activity.hideShow.styleOptions.html b/__tests__/html2/react19/activity.hideShow.styleOptions.html new file mode 100644 index 0000000000..35281e5501 --- /dev/null +++ b/__tests__/html2/react19/activity.hideShow.styleOptions.html @@ -0,0 +1,73 @@ + + + + React 19: Web Chat applies the style options changed while an <Activity> hides it + + + +
+ + + + diff --git a/__tests__/html2/react19/activity.hideShow.styleOptions.html.snap-1.png b/__tests__/html2/react19/activity.hideShow.styleOptions.html.snap-1.png new file mode 100644 index 0000000000..b3008e93ac Binary files /dev/null and b/__tests__/html2/react19/activity.hideShow.styleOptions.html.snap-1.png differ diff --git a/__tests__/html2/react19/strictMode.unmount.styles.html b/__tests__/html2/react19/strictMode.unmount.styles.html new file mode 100644 index 0000000000..4aaaf7da04 --- /dev/null +++ b/__tests__/html2/react19/strictMode.unmount.styles.html @@ -0,0 +1,63 @@ + + + + React 19 in Strict Mode: Web Chat keeps its styles, and removes them when unmounted + + + +
+ + + + diff --git a/__tests__/html2/react19/strictMode.unmount.styles.html.snap-1.png b/__tests__/html2/react19/strictMode.unmount.styles.html.snap-1.png new file mode 100644 index 0000000000..c22dbf469d Binary files /dev/null and b/__tests__/html2/react19/strictMode.unmount.styles.html.snap-1.png differ diff --git a/__tests__/html2/strictMode/unmount.styles.html b/__tests__/html2/strictMode/unmount.styles.html new file mode 100644 index 0000000000..755cd6b01c --- /dev/null +++ b/__tests__/html2/strictMode/unmount.styles.html @@ -0,0 +1,63 @@ + + + + Strict Mode: Web Chat keeps its styles on React 18, and removes them when unmounted + + + +
+ + + + diff --git a/__tests__/html2/strictMode/unmount.styles.html.snap-1.png b/__tests__/html2/strictMode/unmount.styles.html.snap-1.png new file mode 100644 index 0000000000..c22dbf469d Binary files /dev/null and b/__tests__/html2/strictMode/unmount.styles.html.snap-1.png differ diff --git a/packages/component/src/hooks/internal/useEmotion.ts b/packages/component/src/hooks/internal/useEmotion.ts index 67ad06e6bd..5b57ed5ef2 100644 --- a/packages/component/src/hooks/internal/useEmotion.ts +++ b/packages/component/src/hooks/internal/useEmotion.ts @@ -1,23 +1,35 @@ import createEmotion from '@emotion/css/create-instance'; -import { useEffect, useMemo } from 'react'; +import { useLayoutEffect, useMemo } from 'react'; import createCSSKey from '../../Utils/createCSSKey'; const sharedEmotionInstances = []; export default function useEmotion(nonce?: string, container?: Node) { - const emotion = useMemo(() => { - const sharedEmotion = sharedEmotionInstances.find( - ({ sheet }) => sheet.nonce === nonce && sheet.container === container - ); - const emotion = sharedEmotion ?? createEmotion({ container, key: `webchat--css-${createCSSKey()}`, nonce }); + const emotion = useMemo( + () => + sharedEmotionInstances.find(({ sheet }) => sheet.nonce === nonce && sheet.container === container) ?? + createEmotion({ container, key: `webchat--css-${createCSSKey()}`, nonce }), + [container, nonce] + ); - sharedEmotionInstances.push(emotion); + // Ref count in the effect, not in render: a hidden (React 19.2+) and Strict Mode run the cleanup, then the + // effect again, without rendering again. + // Layout effect: restores the styles before paint, and before appends its own, as on mount. + useLayoutEffect(() => { + const { cache, sheet } = emotion; - return emotion; - }, [container, nonce]); + // The last release removed the style elements, but the class names are still in use: insert their rules again. + if (!sharedEmotionInstances.includes(emotion) && sheet.tags.some(tag => !tag.isConnected)) { + sheet.flush(); + + for (const [className, styles] of Object.entries(cache.registered)) { + cache.insert(`.${className}`, { name: className, styles }, sheet, false); + } + } - useEffect( - () => () => { + sharedEmotionInstances.push(emotion); + + return () => { const index = sharedEmotionInstances.lastIndexOf(emotion); // Reduce ref count for the specific emotion instance. @@ -29,9 +41,8 @@ export default function useEmotion(nonce?: string, container?: Node) { child.remove(); } } - }, - [emotion] - ); + }; + }, [emotion]); return emotion; }