Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
- Resolved [#2661](https://github.com/microsoft/BotFramework-WebChat/issues/2661) and [#5352](https://github.com/microsoft/BotFramework-WebChat/issues/5352). Added speech recognition continuous mode with barge-in support, in PR [#5426](https://github.com/microsoft/BotFramework-WebChat/pull/5426), by [@RushikeshGavali](https://github.com/RushikeshGavali) and [@compulim](https://github.com/compulim)
- Set `styleOptions.speechRecognitionContinuous` to `true` with a Web Speech API provider with continuous mode support
- Added support of [contentless activity in livestream](https://github.com/microsoft/BotFramework-WebChat/blob/main/docs/LIVESTREAMING.md#scenario-3-interim-activities-with-no-content), in PR [#5430](https://github.com/microsoft/BotFramework-WebChat/pull/5430), by [@compulim](https://github.com/compulim)
- Added sliding dots typing indicator in Fluent theme, in PR [#5447](https://github.com/microsoft/BotFramework-WebChat/pull/5447), by [@compulim](https://github.com/compulim)

### Changed

Expand Down
114 changes: 114 additions & 0 deletions __tests__/html2/fluentTheme/typingIndicator.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.6/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="/test-harness.js"></script>
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
<script crossorigin="anonymous" src="/__dist__/botframework-webchat-fluent-theme.production.min.js"></script>
</head>
<body>
<main id="webchat"></main>
<script type="importmap">
{
"imports": {
"@testduet/wait-for": "https://unpkg.com/@testduet/wait-for@main/dist/wait-for.mjs",
"jest-mock": "https://esm.sh/jest-mock",
"react-dictate-button/internal": "https://unpkg.com/react-dictate-button@main/dist/react-dictate-button.internal.mjs"
}
}
</script>
<script type="module">
import { waitFor } from '@testduet/wait-for';

const isLivestream = new URL(location).searchParams.has('livestream');

run(async function () {
const {
React: { createElement },
ReactDOM: { render },
WebChat: { FluentThemeProvider, ReactWebChat }
} = window; // Imports in UMD fashion.

// TBD: We cannot pause SMIL animation via reduced motion yet.
// await host.sendDevToolsCommand('Emulation.setEmulatedMedia', {
// features: [{ name: 'prefers-reduced-motion', value: 'reduce' }]
// });

const { directLine, store } = testHelpers.createDirectLineEmulator();

render(
createElement(FluentThemeProvider, {}, createElement(ReactWebChat, { directLine, store })),
document.getElementById('webchat')
);

await pageConditions.uiConnected();

// WHEN: Receive a bot message.
await directLine.emulateIncomingActivity({
from: { id: 'u-00001', name: 'Bot', role: 'bot' },
id: 'a-00001',
text: 'Hello, World!',
type: 'message'
});

// WHEN: Bot send either a contentless livestream or typing activity.
await directLine.emulateIncomingActivity({
...(isLivestream
? {
channelData: {
streamSequence: 1,
streamType: 'streaming'
}
}
: {}),
from: { id: 'u-00001', name: 'Bot', role: 'bot' },
id: 'a-00002',
type: 'typing'
});

// THEN: Should show typing indicator.
await waitFor(() => expect(pageElements.typingIndicator()).toBeTruthy());

const svg = pageElements.typingIndicator().querySelector('svg');

svg?.pauseAnimations();
svg?.setCurrentTime(0);

// THEN: Should match snapshot.
await host.snapshot('local');

// ---

// WHEN: Bot send either a contentless livestream or typing activity.
await directLine.emulateIncomingActivity({
...(isLivestream
? {
channelData: {
streamId: 'a-00002',
streamType: 'final'
}
}
: {
channelData: {
webChat: {
styleOptions: { typingAnimationDuration: 0 }
}
}
}),
from: { id: 'u-00001', name: 'Bot', role: 'bot' },
id: 'a-00002',
type: 'typing'
});

// THEN: Should hide typing indicator.
await waitFor(() => expect(pageElements.typingIndicator()).toBeFalsy());

// THEN: Should match snapshot.
await host.snapshot('local');
});
</script>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions __tests__/html2/fluentTheme/typingIndicator.livestream.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<script>
location = './typingIndicator?livestream';
</script>
</head>
<body></body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/component/src/Assets/TypingAnimation.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from 'react';

import ScreenReaderText from '../ScreenReaderText';
import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject';
import testIds from '../testIds';
import useStyleSet from '../hooks/useStyleSet';

const { useDirection, useLocalizer } = hooks;
Expand Down Expand Up @@ -31,6 +32,7 @@ const TypingAnimation = () => {
rootClassName,
typingAnimationStyleSet + ''
)}
data-testid={testIds.typingIndicator}
/>
</React.Fragment>
);
Expand Down
1 change: 1 addition & 0 deletions packages/component/src/testIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const testIds = {
copyButton: 'copy button',
sendBoxSpeechBox: 'send box speech box',
sendBoxTextBox: 'send box text area',
typingIndicator: 'typing indicator',
viewCodeButton: 'view code button'
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,17 @@ import cx from 'classnames';
import React, { Fragment, memo, type ReactNode } from 'react';

import { useVariantClassName } from '../../styles';
import SlidingDots from '../assets/SlidingDots';
import styles from './ActivityLoader.module.css';

const loadingAnimationUrl =
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCA0MDAgMjAiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iYSIgeDE9IjAiIHgyPSIxMDAlIiB5MT0iMCIgeTI9IjAiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAlIj48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJzdG9wLWNvbG9yIiBkdXI9IjJzIiBrZXlUaW1lcz0iMDswLjI7MC4zMzswLjU7MC42NjswLjg7MSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iI2FkNWFlMTsjYWQ1YWUxOyMwRTk0RTE7IzBFOTRFMTsjNjY5ZmMyOyM2NjlmYzI7I2FkNWFlMSIvPjwvc3RvcD48c3RvcCBvZmZzZXQ9IjUwJSI+PGFuaW1hdGUgYXR0cmlidXRlTmFtZT0ic3RvcC1jb2xvciIgZHVyPSIycyIga2V5VGltZXM9IjA7MC4yOzAuMzM7MC41OzAuNjY7MC44OzEiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiB2YWx1ZXM9IiNlOTYxOGQ7I2U5NjE4ZDsjNTdBQjgyOyM1N0FCODI7IzYzNzdlMDsjNjM3N2UwOyNlOTYxOGQiLz48L3N0b3A+PHN0b3Agb2Zmc2V0PSIxMDAlIj48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJzdG9wLWNvbG9yIiBkdXI9IjJzIiBrZXlUaW1lcz0iMDswLjI7MC4zMzswLjU7MC42NjswLjg7MSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iI2ZkOWU1ZjsjZmQ5ZTVmOyNDNkMyMjU7I0M2QzIyNTsjOWI4MGVjOyM5YjgwZWM7I2ZkOWU1ZiIvPjwvc3RvcD48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48ZyBmaWxsPSJ1cmwoI2EpIj48cmVjdCBoZWlnaHQ9IjIwIiByeD0iMTAiPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9IngiIGR1cj0iMnMiIGtleVRpbWVzPSIwOzAuNTswLjY2OzEiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiB2YWx1ZXM9IjI2OzI2OzA7MCIvPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9IndpZHRoIiBkdXI9IjJzIiBrZXlUaW1lcz0iMDswLjI7MC4zMzswLjU7MC42NjsxIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgdmFsdWVzPSIyMDsyMDszMDszMDsyMDsyMCIvPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9Im9wYWNpdHkiIGR1cj0iMnMiIGtleVRpbWVzPSIwOzAuNTswLjY2OzEiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiB2YWx1ZXM9IjE7MTswOzAiLz48L3JlY3Q+PHJlY3QgaGVpZ2h0PSIyMCIgcng9IjEwIj48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJ4IiBkdXI9IjJzIiBrZXlUaW1lcz0iMDswLjI7MC4zMzswLjU7MC42NjswLjg7MSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iNjI7NjI7NzI7NzI7MjY7MjY7MCIvPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9IndpZHRoIiBkdXI9IjJzIiBrZXlUaW1lcz0iMDswLjI7MC4zMzswLjU7MC42NjswLjg7MSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iMTA0OzEwNDsyMDsyMDs3MDs3MDsyMCIvPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9Im9wYWNpdHkiIGR1cj0iMnMiIGtleVRpbWVzPSIwOzAuODsxIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgdmFsdWVzPSIxOzE7MCIvPjwvcmVjdD48cmVjdCBoZWlnaHQ9IjIwIiByeD0iMTAiPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9IngiIGR1cj0iMnMiIGtleVRpbWVzPSIwOzAuMjswLjMzOzAuNTswLjY2OzAuODsxIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgdmFsdWVzPSIxODI7MTgyOzEwODsxMDg7MTEyOzExMjsyNiIvPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9IndpZHRoIiBkdXI9IjJzIiBrZXlUaW1lcz0iMDswLjI7MC4zMzswLjU7MC42NjsxIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgdmFsdWVzPSIyMDsyMDs2MDs2MDsyMDsyMCIvPjwvcmVjdD48cmVjdCBoZWlnaHQ9IjIwIiByeD0iMTAiPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9IngiIGR1cj0iMnMiIGtleVRpbWVzPSIwOzAuMjswLjMzOzAuNTswLjY2OzAuODsxIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgdmFsdWVzPSIyMTg7MjE4OzE4NDsxODQ7MTQ4OzE0ODs2MiIvPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9IndpZHRoIiBkdXI9IjJzIiBrZXlUaW1lcz0iMDswLjI7MC4zMzswLjU7MC42NjswLjg7MSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iNjA7NjA7ODA7ODA7NDA7NDA7MTA0Ii8+PC9yZWN0PjxyZWN0IGhlaWdodD0iMjAiIHJ4PSIxMCI+PGFuaW1hdGUgYXR0cmlidXRlTmFtZT0ieCIgZHVyPSIycyIga2V5VGltZXM9IjA7MC4yOzAuMzM7MC41OzAuNjY7MC44OzEiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiB2YWx1ZXM9IjI5NDsyOTQ7MjgwOzI4MDsyMDQ7MjA0OzE4MiIvPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9IndpZHRoIiBkdXI9IjJzIiBrZXlUaW1lcz0iMDswLjI7MC4zMzswLjU7MC42NjswLjg7MSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iNDA7NDA7MjA7MjA7ODA7ODA7MjAiLz48L3JlY3Q+PHJlY3QgaGVpZ2h0PSIyMCIgcng9IjEwIj48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJ4IiBkdXI9IjJzIiBrZXlUaW1lcz0iMDswLjI7MC4zMzswLjU7MC42NjswLjg7MSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iMzUwOzM1MDszMTY7MzE2OzMwMDszMDA7MjE4Ii8+PGFuaW1hdGUgYXR0cmlidXRlTmFtZT0id2lkdGgiIGR1cj0iMnMiIGtleVRpbWVzPSIwOzAuMjswLjMzOzAuNTswLjY2OzAuODsxIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgdmFsdWVzPSIyMDsyMDs2MDs2MDsyMDsyMDs2MCIvPjwvcmVjdD48cmVjdCBoZWlnaHQ9IjIwIiByeD0iMTAiPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9IngiIGR1cj0iMnMiIGtleVRpbWVzPSIwOzAuMjswLjMzOzAuNTswLjY2OzAuODsxIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgdmFsdWVzPSIzODY7Mzg2OzM5MjszOTI7MzM2OzMzNjsyOTQiLz48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJ3aWR0aCIgZHVyPSIycyIga2V5VGltZXM9IjA7MC41OzAuNjY7MSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iMjA7MjA7NDA7NDAiLz48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJvcGFjaXR5IiBkdXI9IjJzIiBrZXlUaW1lcz0iMDswLjU7MC42NjsxIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgdmFsdWVzPSIwOzA7MTsxIi8+PC9yZWN0PjxyZWN0IHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgcng9IjEwIj48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJ4IiBkdXI9IjJzIiBrZXlUaW1lcz0iMDswLjI7MC4zMzswLjU7MC42NjswLjg7MSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iNDIyOzQyMjs0Mjg7NDI4OzM5MjszOTI7MzUwIi8+PGFuaW1hdGUgYXR0cmlidXRlTmFtZT0ib3BhY2l0eSIgZHVyPSIycyIga2V5VGltZXM9IjA7MC44OzEiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiB2YWx1ZXM9IjA7MDsxIi8+PC9yZWN0PjwvZz48L3N2Zz4=';

function FluentActivityLoader({ children }: Readonly<{ children?: ReactNode | undefined }>) {
const classNames = useStyles(styles);
const variantClassName = useVariantClassName(classNames);

return (
<Fragment>
{children}
<img
alt=""
className={cx(classNames['activity-loader'], variantClassName)}
role="presentation"
src={loadingAnimationUrl}
/>
<SlidingDots className={cx(classNames['activity-loader'], variantClassName)} />
</Fragment>
);
}
Expand Down
176 changes: 176 additions & 0 deletions packages/fluent-theme/src/components/assets/SlidingDots.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import React, { memo, useMemo } from 'react';

type SlidingDotsProps = Readonly<{
className?: string | undefined;
}>;

function SlidingDots({ className }: SlidingDotsProps) {
const id = useMemo(() => `webchat__sliding-dots-asset--${crypto.randomUUID()}`, []);
Comment thread
OEvgeny marked this conversation as resolved.
Outdated

return (
<svg
className={className}
height="20"
role="presentation"
viewBox="0 0 400 20"
width="400"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
{/* eslint-disable-next-line react/forbid-dom-props */}
<linearGradient gradientUnits="userSpaceOnUse" id={id} x1="0" x2="100%" y1="0" y2="0">
<stop offset="0%">
<animate
attributeName="stop-color"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;0.8;1"
repeatCount="indefinite"
values="#ad5ae1;#ad5ae1;#0E94E1;#0E94E1;#669fc2;#669fc2;#ad5ae1"
/>
</stop>
<stop offset="50%">
<animate
attributeName="stop-color"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;0.8;1"
repeatCount="indefinite"
values="#e9618d;#e9618d;#57AB82;#57AB82;#6377e0;#6377e0;#e9618d"
/>
</stop>
<stop offset="100%">
<animate
attributeName="stop-color"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;0.8;1"
repeatCount="indefinite"
values="#fd9e5f;#fd9e5f;#C6C225;#C6C225;#9b80ec;#9b80ec;#fd9e5f"
/>
</stop>
</linearGradient>
</defs>
<g fill={`url(#${id})`}>
<rect height="20" rx="10">
<animate attributeName="x" dur="2s" keyTimes="0;0.5;0.66;1" repeatCount="indefinite" values="26;26;0;0" />
<animate
attributeName="width"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;1"
repeatCount="indefinite"
values="20;20;30;30;20;20"
/>
<animate attributeName="opacity" dur="2s" keyTimes="0;0.5;0.66;1" repeatCount="indefinite" values="1;1;0;0" />
</rect>
<rect height="20" rx="10">
<animate
attributeName="x"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;0.8;1"
repeatCount="indefinite"
values="62;62;72;72;26;26;0"
/>
<animate
attributeName="width"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;0.8;1"
repeatCount="indefinite"
values="104;104;20;20;70;70;20"
/>
<animate attributeName="opacity" dur="2s" keyTimes="0;0.8;1" repeatCount="indefinite" values="1;1;0" />
</rect>
<rect height="20" rx="10">
<animate
attributeName="x"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;0.8;1"
repeatCount="indefinite"
values="182;182;108;108;112;112;26"
/>
<animate
attributeName="width"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;1"
repeatCount="indefinite"
values="20;20;60;60;20;20"
/>
</rect>
<rect height="20" rx="10">
<animate
attributeName="x"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;0.8;1"
repeatCount="indefinite"
values="218;218;184;184;148;148;62"
/>
<animate
attributeName="width"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;0.8;1"
repeatCount="indefinite"
values="60;60;80;80;40;40;104"
/>
</rect>
<rect height="20" rx="10">
<animate
attributeName="x"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;0.8;1"
repeatCount="indefinite"
values="294;294;280;280;204;204;182"
/>
<animate
attributeName="width"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;0.8;1"
repeatCount="indefinite"
values="40;40;20;20;80;80;20"
/>
</rect>
<rect height="20" rx="10">
<animate
attributeName="x"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;0.8;1"
repeatCount="indefinite"
values="350;350;316;316;300;300;218"
/>
<animate
attributeName="width"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;0.8;1"
repeatCount="indefinite"
values="20;20;60;60;20;20;60"
/>
</rect>
<rect height="20" rx="10">
<animate
attributeName="x"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;0.8;1"
repeatCount="indefinite"
values="386;386;392;392;336;336;294"
/>
<animate
attributeName="width"
dur="2s"
keyTimes="0;0.5;0.66;1"
repeatCount="indefinite"
values="20;20;40;40"
/>
<animate attributeName="opacity" dur="2s" keyTimes="0;0.5;0.66;1" repeatCount="indefinite" values="0;0;1;1" />
</rect>
<rect height="20" rx="10" width="20">
<animate
attributeName="x"
dur="2s"
keyTimes="0;0.2;0.33;0.5;0.66;0.8;1"
repeatCount="indefinite"
values="422;422;428;428;392;392;350"
/>
<animate attributeName="opacity" dur="2s" keyTimes="0;0.8;1" repeatCount="indefinite" values="0;0;1" />
</rect>
</g>
</svg>
);
}

export default memo(SlidingDots);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
:global(.webchat-fluent) .sliding-dots-typing-indicator {
align-self: start;
display: flex;
height: 16px;
margin: auto var(--webchat-spacingHorizontalMNudge);
}

:global(.webchat-fluent) .sliding-dots-typing-indicator__image {
height: 6px;
width: auto;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { testIds } from 'botframework-webchat-component';
import { useStyles } from 'botframework-webchat-styles/react';
import cx from 'classnames';
import React, { memo } from 'react';

import { useVariantClassName } from '../../styles';
import SlidingDots from '../assets/SlidingDots';
import styles from './SlidingDotsTypingIndicator.module.css';

function SlidingDotsTypingIndicator() {
const classNames = useStyles(styles);
const variantClassName = useVariantClassName(classNames);
Comment thread
compulim marked this conversation as resolved.

return (
<div
className={cx(classNames['sliding-dots-typing-indicator'], variantClassName)}
data-testid={testIds.typingIndicator}
>
<SlidingDots className={cx(classNames['sliding-dots-typing-indicator__image'])} />
</div>
);
}

export default memo(SlidingDotsTypingIndicator);
Loading