Skip to content

Commit c5490d1

Browse files
authored
Follow up PR for sliding dots animation (#5448)
* Add comments * Remove variant class name * Add <AssetComposer> * Add entry * Align to state hooks pattern * Privatize useAssetURL * Fix ESLint
1 parent aff78e0 commit c5490d1

File tree

9 files changed

+94
-15
lines changed

9 files changed

+94
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
8383
- 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)
8484
- Set `styleOptions.speechRecognitionContinuous` to `true` with a Web Speech API provider with continuous mode support
8585
- 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)
86-
- 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)
86+
- Added sliding dots typing indicator in Fluent theme, in PR [#5447](https://github.com/microsoft/BotFramework-WebChat/pull/5447) and PR [#5448](https://github.com/microsoft/BotFramework-WebChat/pull/5448), by [@compulim](https://github.com/compulim)
8787

8888
### Changed
8989

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { type ContextOf } from 'botframework-webchat-api';
2+
import React, { memo, useEffect, useMemo, type ReactNode } from 'react';
3+
4+
import { type AssetName } from './AssetName';
5+
import Context from './private/Context';
6+
7+
type ContextType = ContextOf<typeof Context>;
8+
9+
type AssetComposerProps = Readonly<{
10+
children?: ReactNode | undefined;
11+
}>;
12+
13+
const SLIDING_DOTS_SVG_STRING =
14+
'<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>';
15+
16+
const AssetComposer = memo(({ children }: AssetComposerProps) => {
17+
const slidingDotsURL = useMemo(
18+
() => URL.createObjectURL(new Blob([SLIDING_DOTS_SVG_STRING], { type: 'image/svg+xml' })),
19+
[]
20+
);
21+
22+
useEffect(() => () => URL.revokeObjectURL(slidingDotsURL), [slidingDotsURL]);
23+
24+
const context = useMemo<ContextType>(
25+
() =>
26+
Object.freeze({
27+
urlStateMap: new Map<AssetName, readonly [URL]>([['sliding dots', Object.freeze([new URL(slidingDotsURL)])]])
28+
}),
29+
[slidingDotsURL]
30+
);
31+
32+
return <Context.Provider value={context}>{children}</Context.Provider>;
33+
});
34+
35+
AssetComposer.displayName = 'AssetComposer';
36+
37+
export default AssetComposer;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type AssetName = 'sliding dots';

packages/fluent-theme/src/components/assets/SlidingDots.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@ import { hooks } from 'botframework-webchat-component';
22
import React, { memo, useCallback, useEffect, useRef } from 'react';
33
import { useRefFrom } from 'use-ref-from';
44

5+
import useAssetURL from './private/useAssetURL';
6+
57
const { useLocalizer, useShouldReduceMotion } = hooks;
68

79
type SlidingDotsProps = Readonly<{ className: string }>;
810

9-
const SLIDING_DOTS_SVG_STRING =
10-
'<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>';
11-
const SLIDING_DOTS_SVG_URL = URL.createObjectURL(new Blob([SLIDING_DOTS_SVG_STRING], { type: 'image/svg+xml' }));
12-
1311
const SlidingDots = ({ className }: SlidingDotsProps) => {
1412
const [shouldReduceMotion] = useShouldReduceMotion();
13+
const [url] = useAssetURL('sliding dots');
1514
const localize = useLocalizer();
1615
const objectElementRef = useRef<HTMLObjectElement>(null);
1716

@@ -49,7 +48,7 @@ const SlidingDots = ({ className }: SlidingDotsProps) => {
4948
<object
5049
aria-label={altText}
5150
className={className}
52-
data={SLIDING_DOTS_SVG_URL}
51+
data={url.href}
5352
onLoad={pauseOrUnpauseAnimations}
5453
ref={objectElementRef}
5554
type="image/svg+xml"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { createContext } from 'react';
2+
3+
import type { AssetName } from '../AssetName';
4+
5+
type ContextType = Readonly<{
6+
urlStateMap: ReadonlyMap<AssetName, readonly [URL]>;
7+
}>;
8+
9+
type ContextAsGetter<T extends Record<string, unknown>> =
10+
T extends Record<infer K, infer V> ? Record<K, { get(): V }> : never;
11+
12+
const defaultContextValue: ContextAsGetter<ContextType> = {
13+
urlStateMap: {
14+
get() {
15+
throw new Error('urlMap cannot be used outside of <AssetComposerContext>.');
16+
}
17+
}
18+
};
19+
20+
const Context = createContext<ContextType>(Object.create({}, defaultContextValue));
21+
22+
Context.displayName = 'AssetComposerContext';
23+
24+
export default Context;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { type AssetName } from '../AssetName';
2+
import useContext from './useContext';
3+
4+
export default function useAssetURL(assetName: AssetName): readonly [URL] {
5+
const urlState = useContext().urlStateMap.get(assetName);
6+
7+
if (!urlState) {
8+
throw new Error(`botframework-webchat-fluent-theme internal: Asset "${assetName}" was not found.`);
9+
}
10+
11+
return urlState;
12+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { type ContextOf } from 'botframework-webchat-api';
2+
import { useContext as useReactContext } from 'react';
3+
4+
import Context from './Context';
5+
6+
export default function useContext(): ContextOf<typeof Context> {
7+
return useReactContext(Context);
8+
}

packages/fluent-theme/src/components/typingIndicator/SlidingDotsTypingIndicator.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,14 @@ import { useStyles } from 'botframework-webchat-styles/react';
33
import cx from 'classnames';
44
import React, { memo } from 'react';
55

6-
import { useVariantClassName } from '../../styles';
76
import SlidingDots from '../assets/SlidingDots';
87
import styles from './SlidingDotsTypingIndicator.module.css';
98

109
function SlidingDotsTypingIndicator() {
1110
const classNames = useStyles(styles);
12-
const variantClassName = useVariantClassName(classNames);
1311

1412
return (
15-
<div
16-
className={cx(classNames['sliding-dots-typing-indicator'], variantClassName)}
17-
data-testid={testIds.typingIndicator}
18-
>
13+
<div className={classNames['sliding-dots-typing-indicator']} data-testid={testIds.typingIndicator}>
1914
<SlidingDots className={cx(classNames['sliding-dots-typing-indicator__image'])} />
2015
</div>
2116
);

packages/fluent-theme/src/private/FluentThemeProvider.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import React, { memo, type ReactNode } from 'react';
66

77
import { ActivityDecorator } from '../components/activity';
88
import ActivityLoader from '../components/activity/ActivityLoader';
9+
import AssetComposer from '../components/assets/AssetComposer';
910
import { isLinerMessageActivity, LinerMessageActivity } from '../components/linerActivity';
1011
import { isPreChatMessageActivity, PreChatMessageActivity } from '../components/preChatActivity';
1112
import { PrimarySendBox } from '../components/sendBox';
@@ -74,9 +75,11 @@ const FluentThemeProvider = ({ children, variant = 'fluent' }: Props) => (
7475
styles={styles}
7576
typingIndicatorMiddleware={typingIndicatorMiddleware}
7677
>
77-
<WebChatDecorator>
78-
<DecoratorComposer middleware={decoratorMiddleware}>{children}</DecoratorComposer>
79-
</WebChatDecorator>
78+
<AssetComposer>
79+
<WebChatDecorator>
80+
<DecoratorComposer middleware={decoratorMiddleware}>{children}</DecoratorComposer>
81+
</WebChatDecorator>
82+
</AssetComposer>
8083
</ThemeProvider>
8184
</TelephoneKeypadProvider>
8285
</WebChatTheme>

0 commit comments

Comments
 (0)