Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ Breaking changes in this release:
- Fixed virtual keyboard should show up on tap after being suppressed, in iOS 26.2, by [@compulim](https://github.com/compulim) in PR [#5678](https://github.com/microsoft/BotFramework-WebChat/pull/5678)
- Fixed compatibility with `create-react-app` by adding file extension to `core-js` imports, by [@compulim](https://github.com/compulim) in PR [#5680](https://github.com/microsoft/BotFramework-WebChat/pull/5680)
- Fixed virtual keyboard should be collapsed after being suppressed, in iOS 26.3, by [@compulim](https://github.com/compulim) in PR [#5757](https://github.com/microsoft/BotFramework-WebChat/pull/5757)
- Fixed Fluent/Copilot typing indicator animation background color, in PR [#5770](https://github.com/microsoft/BotFramework-WebChat/pull/5770), by [@OEvgeny](https://github.com/OEvgeny)

## [4.18.0] - 2024-07-10

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<script>
location = './typingIndicator.scroll?theme=fluent&variant=copilot&fluentTheme=dark';
</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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<script>
location = './typingIndicator.scroll?theme=fluent&fluentTheme=dark';
</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.
261 changes: 152 additions & 109 deletions __tests__/html2/typing/typingIndicator.scroll.html
Original file line number Diff line number Diff line change
@@ -1,127 +1,170 @@
<!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>
<style>
#webchat .webchat__typing-indicator {
background-image: url(data:image/gif;base64,R0lGODlhQAAYAPUAAOzv8evu8Ort7+fq7Ons7ujr7eXo6uTn6ebp6+Xn6ezu8OPm6N/i49/i5OHk5uDj5d/h49PV17/BwsLExdja3MvNz72/wL7Awc/S08TGyMDCw9TW2NbZ2tvd39nc3d3f4ens7bO0tbS2t7GztLW3uK6vsMnLzamqq7m7vK2ur6yurp2en6KjpJeXmI2NjpGSkpOUlJiYmZqbmwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQABwD/ACwAAAAAQAAYAAAG8ECAcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvtegOGgTAwLBQPRQNAMAw0HGRAPA4gVAMUjWW/KA89IiqCHkJqbHImMS4uKxFjcnVFdgpOeHuXEghCZgAeKYKgHURsiYumHI9DdlQSmHsYkEIooKAoRQErpqYndKpVrXvAFn60tL0QL7qLMWKxQqtSwhbARSXFgkUPycoyIKmRrJfBG36zxRKjACzKLiG94ELNTwwXroecByPFfQCGABAwdHX79owKmQoTMkRYxWlIhkAZEhQSIoDMhxItZJDgR4YMNADNKHkZSbKkyZMoU6pcybKly5dBAAA7); background-color: black;
}
</style>
</head>
<body>
<main id="webchat"></main>
<script type="importmap">

<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script type="importmap">
{
"imports": {
"@fluentui/react-provider": "/__dist__/packages/test/test-assets/out/@fluentui/react-provider.js",
"@fluentui/tokens": "/__dist__/packages/test/test-assets/out/@fluentui/tokens.js",
"@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"
"react": "https://esm.sh/react@18",
"react-dom": "https://esm.sh/react-dom@18",
"react-dom/": "https://esm.sh/react-dom@18/",
"react-jsx-runtime": "https://esm.sh/react@18/jsx-runtime"
}
}
</script>
<script type="module">
import { waitFor } from '@testduet/wait-for';
<script crossorigin="anonymous" src="/test-harness.js"></script>
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script type="module">
import React from 'react';
window.React = React;
</script>
<script defer crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
<script defer crossorigin="anonymous" src="/__dist__/botframework-webchat-fluent-theme.production.min.js"></script>
<style>
main {
display: flex;
}

#webchat .webchat__typing-indicator {
background-image: url(data:image/gif;base64,R0lGODlhQAAYAPUAAOzv8evu8Ort7+fq7Ons7ujr7eXo6uTn6ebp6+Xn6ezu8OPm6N/i49/i5OHk5uDj5d/h49PV17/BwsLExdja3MvNz72/wL7Awc/S08TGyMDCw9TW2NbZ2tvd39nc3d3f4ens7bO0tbS2t7GztLW3uK6vsMnLzamqq7m7vK2ur6yurp2en6KjpJeXmI2NjpGSkpOUlJiYmZqbmwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQABwD/ACwAAAAAQAAYAAAG8ECAcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvtegOGgTAwLBQPRQNAMAw0HGRAPA4gVAMUjWW/KA89IiqCHkJqbHImMS4uKxFjcnVFdgpOeHuXEghCZgAeKYKgHURsiYumHI9DdlQSmHsYkEIooKAoRQErpqYndKpVrXvAFn60tL0QL7qLMWKxQqtSwhbARSXFgkUPycoyIKmRrJfBG36zxRKjACzKLiG94ELNTwwXroecByPFfQCGABAwdHX79owKmQoTMkRYxWlIhkAZEhQSIoDMhxItZJDgR4YMNADNKHkZSbKkyZMoU6pcybKly5dBAAA7);
background-color: black;
}
</style>
</head>

<body>
<main id="webchat"></main>
<script type="module">
import { FluentProvider } from '@fluentui/react-provider';
import { webDarkTheme, webLightTheme } from '@fluentui/tokens';
import { waitFor } from '@testduet/wait-for';
import React from 'react';
import { createRoot } from 'react-dom/client';

const { searchParams } = new URL(location);
const fluentTheme = searchParams.get('fluentTheme');
const isFluentTheme = searchParams.get('theme') === 'fluent';
const isLivestream = searchParams.has('livestream');
const variant = searchParams.get('variant');

run(async function () {
await host.sendDevToolsCommand('Emulation.setEmulatedMedia', {
features: [
{ name: 'prefers-reduced-motion', value: 'reduce' },
...(fluentTheme === 'dark' || fluentTheme === 'light'
? [{ name: 'prefers-color-scheme', value: fluentTheme }]
: [])
]
});

const isLivestream = new URL(location).searchParams.has('livestream');
const { directLine, store } = testHelpers.createDirectLineEmulator();

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

const theme =
fluentTheme === 'dark' || window.matchMedia('(prefers-color-scheme: dark)').matches
? webDarkTheme
: webLightTheme;

const root = createRoot(document.getElementById('webchat'));

root.render(
React.createElement(
FluentProvider,
{ theme },
React.createElement(
FluentThemeProvider,
variant ? { variant } : {},
React.createElement(ReactWebChat, { directLine, store })
)
)
);
} else {
renderWebChat({ directLine, store }, document.getElementById('webchat'));
}

await host.sendDevToolsCommand('Emulation.setEmulatedMedia', {
features: [{ name: 'prefers-reduced-motion', value: 'reduce' }]
});
await pageConditions.uiConnected();

const { directLine, store } = testHelpers.createDirectLineEmulator();
// WHEN: Receive a bot message.
await directLine.emulateIncomingActivity({
from: { id: 'u-00001', name: 'Bot', role: 'bot' },
id: 'a-00001',
text: 'In minim amet culpa adipisicing aliqua culpa minim culpa officia culpa laboris non commodo. Velit nisi ut sit amet in sunt eu voluptate Lorem eu do sint proident aute. Nulla nisi commodo consectetur anim id non ut est anim veniam occaecat excepteur dolor nulla. Adipisicing et dolor ex cillum sit ipsum amet labore officia dolor non ad aliquip officia. Irure quis occaecat cupidatat ut commodo culpa eiusmod ipsum pariatur. Excepteur aliqua consectetur anim laborum enim ipsum tempor occaecat voluptate.',
type: 'message'
});

renderWebChat(
{ directLine, store },
document.getElementById('webchat')
);
await directLine.emulateIncomingActivity({
from: { id: 'u-00001', name: 'Bot', role: 'bot' },
id: 'a-00002',
text: 'Ad minim fugiat sint et laboris consectetur eu ut in nisi fugiat cillum est labore. Et proident tempor veniam ex est incididunt Lorem. Culpa sit id eu voluptate.',
type: 'message'
});

await pageConditions.uiConnected();

// WHEN: Receive a bot message.
await directLine.emulateIncomingActivity({
from: { id: 'u-00001', name: 'Bot', role: 'bot' },
id: 'a-00001',
text: 'In minim amet culpa adipisicing aliqua culpa minim culpa officia culpa laboris non commodo. Velit nisi ut sit amet in sunt eu voluptate Lorem eu do sint proident aute. Nulla nisi commodo consectetur anim id non ut est anim veniam occaecat excepteur dolor nulla. Adipisicing et dolor ex cillum sit ipsum amet labore officia dolor non ad aliquip officia. Irure quis occaecat cupidatat ut commodo culpa eiusmod ipsum pariatur. Excepteur aliqua consectetur anim laborum enim ipsum tempor occaecat voluptate.',
type: 'message'
});

await directLine.emulateIncomingActivity({
from: { id: 'u-00001', name: 'Bot', role: 'bot' },
id: 'a-00002',
text: 'Ad minim fugiat sint et laboris consectetur eu ut in nisi fugiat cillum est labore. Et proident tempor veniam ex est incididunt Lorem. Culpa sit id eu voluptate.',
type: 'message'
});

await directLine.emulateIncomingActivity({
from: { id: 'u-00001', name: 'Bot', role: 'bot' },
id: 'a-00003',
text: 'Est voluptate eiusmod ad Lorem irure amet sint ea aliqua labore eu do nostrud exercitation. Non adipisicing non amet laborum. Anim fugiat minim cupidatat consequat ipsum minim ex mollit commodo ut aliqua quis consequat dolore. Cupidatat tempor laborum consectetur eiusmod cillum do consequat ad pariatur amet magna aliquip occaecat officia.',
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-00004',
type: 'typing'
});

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

// 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-00004',
streamType: 'final'
}
await directLine.emulateIncomingActivity({
from: { id: 'u-00001', name: 'Bot', role: 'bot' },
id: 'a-00003',
text: 'Est voluptate eiusmod ad Lorem irure amet sint ea aliqua labore eu do nostrud exercitation. Non adipisicing non amet laborum. Anim fugiat minim cupidatat consequat ipsum minim ex mollit commodo ut aliqua quis consequat dolore. Cupidatat tempor laborum consectetur eiusmod cillum do consequat ad pariatur amet magna aliquip occaecat officia.',
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-00004',
type: 'typing'
});

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

// 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-00004',
streamType: 'final'
}
}
: {
channelData: {
webChat: {
styleOptions: { typingAnimationDuration: 0 }
}
: {
channelData: {
webChat: {
styleOptions: { typingAnimationDuration: 0 }
}
}
}),
from: { id: 'u-00001', name: 'Bot', role: 'bot' },
id: 'a-00005',
type: 'typing'
});

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

// THEN: Should match snapshot.
await host.snapshot('local');
}
}),
from: { id: 'u-00001', name: 'Bot', role: 'bot' },
id: 'a-00005',
type: 'typing'
});
</script>
</body>
</html>

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

// THEN: Should match snapshot.
await host.snapshot('local');
});
</script>
</body>

</html>
20 changes: 18 additions & 2 deletions packages/fluent-theme/src/components/assets/AssetComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ type AssetComposerProps = Readonly<{
const SLIDING_DOTS_SVG_STRING =
'<svg xmlns="http://www.w3.org/2000/svg" width="400" height="20" viewBox="0 0 400 20"><defs><linearGradient id="a" x1="0" x2="100%" y1="0" y2="0" gradientUnits="userSpaceOnUse"><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(#a)"><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 width="20" 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="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>';

const SLIDING_DOTS_REDUCED_MOTION_SVG_STRING = SLIDING_DOTS_SVG_STRING.replaceAll(
'repeatCount="indefinite"',
'repeatCount="0.01" fill="freeze"'
);

const AssetComposer = memo(({ children }: AssetComposerProps) => {
const slidingDotsURL = useMemo(
// Content is hardcoded.
Expand All @@ -18,14 +23,25 @@ const AssetComposer = memo(({ children }: AssetComposerProps) => {
[]
);

const slidingDotsReducedMotionURL = useMemo(
// Content is hardcoded.
// eslint-disable-next-line no-restricted-properties
() => URL.createObjectURL(new Blob([SLIDING_DOTS_REDUCED_MOTION_SVG_STRING], { type: 'image/svg+xml' })),
[]
);

useEffect(() => () => URL.revokeObjectURL(slidingDotsURL), [slidingDotsURL]);
useEffect(() => () => URL.revokeObjectURL(slidingDotsReducedMotionURL), [slidingDotsReducedMotionURL]);

const context = useMemo<ContextType>(
() =>
Object.freeze({
urlStateMap: new Map<AssetName, readonly [URL]>([['sliding dots', Object.freeze([new URL(slidingDotsURL)])]])
urlStateMap: new Map<AssetName, readonly [URL]>([
['sliding dots', Object.freeze([new URL(slidingDotsURL)])],
['sliding dots reduced-motion', Object.freeze([new URL(slidingDotsReducedMotionURL)])]
])
}),
[slidingDotsURL]
[slidingDotsURL, slidingDotsReducedMotionURL]
);

return <Context.Provider value={context}>{children}</Context.Provider>;
Expand Down
2 changes: 1 addition & 1 deletion packages/fluent-theme/src/components/assets/AssetName.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type AssetName = 'sliding dots';
export type AssetName = 'sliding dots' | 'sliding dots reduced-motion';
Loading
Loading