Skip to content

Commit

Permalink
feat: Add white-label CSS decorator (#5312)
Browse files Browse the repository at this point in the history
* Firefox implementation

* Simplify layout

* Cleanup

* Styles package

* Fix component build:types step

* Fix source maps

* Use quoted placeholder

* Add injected styles check

* Add built-in decorator tests

* Add component decorator to external fluent

* Configure fluent decorator

* Handle loader overflow

* Lower Chrome required version

* Move decorator to component

* Add missing env

* Add styles to workspaces

* Bail if unable to create styles

* Sort

* Readability

---------

Co-authored-by: William Wong <[email protected]>
  • Loading branch information
OEvgeny and compulim authored Sep 27, 2024
1 parent be287d7 commit ea7a875
Show file tree
Hide file tree
Showing 72 changed files with 640 additions and 169 deletions.
1 change: 1 addition & 0 deletions .eslintrc.production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ overrides:
no-restricted-globals: off

rules:
dot-notation: off
no-restricted-globals:
- error
- name: cancelAnimationFrame
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
- Added `nonce` for Fluent and `react-scroll-to-bottom` injected styles, in PR [#5196](https://github.com/microsoft/BotFramework-WebChat/pull/5196), by [@OEvgeny](https://github.com/OEvgeny)
- Updated `react-scroll-to-bottom` to version `4.2.1-main.53844f5`, in PR [#5196](https://github.com/microsoft/BotFramework-WebChat/pull/5196), by [@OEvgeny](https://github.com/OEvgeny)
- Updated `react-film` to version `3.1.1-main.f623bf6`, in PR [#5196](https://github.com/microsoft/BotFramework-WebChat/pull/5196), by [@OEvgeny](https://github.com/OEvgeny)
- (Experimental) Added CSS decorator support into Web Chat white-label experience, in PR [#5312](https://github.com/microsoft/BotFramework-WebChat/pull/5312), by [@OEvgeny](https://github.com/OEvgeny)
- Introduced `WebChatDecorator` component for adding animated borders to activities, in PR [#5312](https://github.com/microsoft/BotFramework-WebChat/pull/5312)
- Added new style options `borderAnimationColor1`, `borderAnimationColor2`, and `borderAnimationColor3` for customizing decorator colors, in PR [#5312](https://github.com/microsoft/BotFramework-WebChat/pull/5312)

### Changed

Expand Down
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: 1 addition & 1 deletion __tests__/html/customization.basicWebChat.restructure.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
expect(container?.firstChild).not.toBeUndefined();

const composer = container.firstChild;
expect(getComputedStyle(composer).getPropertyValue('--webchat__color--accent')).toBe('#0063B1');
expect(getComputedStyle(composer).getPropertyValue('--webchat__color--accent').trim()).toBe('#0063B1');

const surface = composer.children[1];
expect(surface.className).toContain(classes.surface);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */

describe('Fluent theme applied', () => {
test('with decorators', () => runHTML('fluentTheme/withDecorator'));
test('with decorators', () => runHTML('fluentTheme/withCustomDecorator'));
});
4 changes: 2 additions & 2 deletions __tests__/html/hooks.useInjectStyles.changeNonce.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
await renderWithFunction(() => useInjectStyles([styleElement], '1'));
await host.snapshot();

const allInjectedStyles1 = document.head.querySelectorAll('style:not([data-emotion])');
const allInjectedStyles1 = document.head.querySelectorAll('style:not([data-emotion]):not([data-webchat-injected])');

expect(allInjectedStyles1).toHaveLength(1);
expect(allInjectedStyles1[0]).toBe(styleElement);
Expand All @@ -64,7 +64,7 @@
// No color should be applied as nonce don't match.
await host.snapshot();

const allInjectedStyles2 = document.head.querySelectorAll('style:not([data-emotion])');
const allInjectedStyles2 = document.head.querySelectorAll('style:not([data-emotion]):not([data-webchat-injected])');

expect(allInjectedStyles2).toHaveLength(1);
expect(allInjectedStyles2[0]).toBe(styleElement);
Expand Down
8 changes: 4 additions & 4 deletions __tests__/html/hooks.useInjectStyles.changeRoot.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,16 @@
await renderWithFunction(() => useInjectStyles([styleElement]));
await host.snapshot();

expect([...document.head.querySelectorAll('style:not([data-emotion])')]).toEqual([styleElement]);
expect([...document.body.querySelectorAll('style:not([data-emotion])')]).toEqual([]);
expect([...document.head.querySelectorAll('style:not([data-emotion]):not([data-webchat-injected])')]).toEqual([styleElement]);
expect([...document.body.querySelectorAll('style:not([data-emotion]):not([data-webchat-injected])')]).toEqual([]);

await renderWithFunction(() => useInjectStyles([styleElement]), {
styleOptions: { stylesRoot: document.body }
});
await host.snapshot();

expect([...document.head.querySelectorAll('style:not([data-emotion])')]).toEqual([]);
expect([...document.body.querySelectorAll('style:not([data-emotion])')]).toEqual([styleElement]);
expect([...document.head.querySelectorAll('style:not([data-emotion]):not([data-webchat-injected])')]).toEqual([]);
expect([...document.body.querySelectorAll('style:not([data-emotion]):not([data-webchat-injected])')]).toEqual([styleElement]);
});
</script>
</body>
Expand Down
4 changes: 2 additions & 2 deletions __tests__/html/hooks.useInjectStyles.dupeElement.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@
await renderWithFunction(() => useInjectStyles([styleElement, styleElement]));
await host.snapshot();

const allInjectedStyles1 = document.head.querySelectorAll('style:not([data-emotion])');
const allInjectedStyles1 = document.head.querySelectorAll('style:not([data-emotion]):not([data-webchat-injected])');

expect(allInjectedStyles1).toHaveLength(1);
expect(allInjectedStyles1[0]).toBe(styleElement);

await renderWithFunction(() => useInjectStyles([styleElement, styleElement]));
await host.snapshot();

const allInjectedStyles2 = document.head.querySelectorAll('style:not([data-emotion])');
const allInjectedStyles2 = document.head.querySelectorAll('style:not([data-emotion]):not([data-webchat-injected])');

expect(allInjectedStyles2).toHaveLength(1);
expect(allInjectedStyles2[0]).toBe(styleElement);
Expand Down
6 changes: 3 additions & 3 deletions __tests__/html/hooks.useInjectStyles.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@
await renderWithFunction(() => useInjectStyles([styleElement1]));
await host.snapshot();

const allInjectedStyles1 = document.head.querySelectorAll('style:not([data-emotion])');
const allInjectedStyles1 = document.head.querySelectorAll('style:not([data-emotion]):not([data-webchat-injected])');

expect(allInjectedStyles1).toHaveLength(1);
expect(allInjectedStyles1[0]).toBe(styleElement1);

await renderWithFunction(() => useInjectStyles([styleElement1, styleElement2]));
await host.snapshot();

const allInjectedStyles2 = document.head.querySelectorAll('style:not([data-emotion])');
const allInjectedStyles2 = document.head.querySelectorAll('style:not([data-emotion]):not([data-webchat-injected])');

expect(allInjectedStyles2).toHaveLength(2);
expect(allInjectedStyles2[0]).toBe(styleElement1);
Expand All @@ -73,7 +73,7 @@
await renderWithFunction(() => useInjectStyles([styleElement1, styleElement2, styleElement3]));
await host.snapshot();

const allInjectedStyles3 = document.head.querySelectorAll('style:not([data-emotion])');
const allInjectedStyles3 = document.head.querySelectorAll('style:not([data-emotion]):not([data-webchat-injected])');

expect(allInjectedStyles3).toHaveLength(3);
expect(allInjectedStyles3[0]).toBe(styleElement1);
Expand Down
114 changes: 114 additions & 0 deletions __tests__/html/withBuiltinDecorator.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/@babel/standalone/babel.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/[email protected]/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 .border-loader__loader,
#webchat .border-flair {
animation-delay: -1s !important;
animation-play-state: paused !important;
}
</style>
</head>
<body>
<main id="webchat"></main>
<script type="text/babel">
run(async function () {
const {
React,
ReactDOM: { render },
WebChat: {
decorator: { WebChatDecorator },
ReactWebChat
}
} = window; // Imports in UMD fashion.

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

const App = () => <ReactWebChat
directLine={directLine}
store={store}
styleOptions={{
bubbleBorderRadius: 10,
typingAnimationBackgroundImage: `url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAUACgDASIAAhEBAxEB/8QAGgABAQACAwAAAAAAAAAAAAAAAAYCBwMFCP/EACsQAAECBQIEBQUAAAAAAAAAAAECAwAEBQYRBxITIjFBMlFhccFScoGh8f/EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwD0lctx023JVD9UeKOIcNoSNylkdcCMbauSmXHLOPUx8r4ZAcQtO1SM9Mj5iO1gtWo1syc7S2zMKYSptbIPNgnII8/5HBpRZ9RpaKjNVVCpUzLPAQ1nmA7qPl6fmAondRrcaqhkVTiiQrYXgglsH7vnpHc3DcNNoEimaqT4Q2s4bCRuUs+gEaLd05uNFVMmiS3o3YEwFDhlP1Z7e3WLzUuzahUKHRk0zM07TmeApvOFLGEjcM9+Xp6wFnbN0Uu5GnF0x4qW1je2tO1Sc9Djy9oRD6QWlU6PPzVSqjRlgtksttKPMcqBKiO3h/cIDacIQgEIQgEIQgP/2Q==')`
}}
/>;

render(
<WebChatDecorator>
<App />
</WebChatDecorator>,
document.getElementById('webchat')
);

await pageConditions.uiConnected();

await directLine.emulateIncomingActivity({
channelData: { streamSequence: 1, streamType: 'informative' },
from: {
id: 'u-00001',
name: 'Bot',
role: 'bot'
},
id: 't-00001',
text: 'Working on it...',
type: 'typing'
});

await pageConditions.numActivitiesShown(1);
await host.snapshot();

const attachments = [
{
content: {
type: 'AdaptiveCard',
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
version: '1.5',
actions: [
{ type: 'Action.Submit', title: 'Button 1' },
{
type: 'Action.ShowCard',
title: 'Show card',
card: {
type: 'AdaptiveCard',
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
version: '1.5',
actions: [
{ type: 'Action.Submit', title: 'Button 2' },
{ type: 'Action.Submit', title: 'Button 3' }
]
}
}
]
},
contentType: 'application/vnd.microsoft.card.adaptive'
}
];
await directLine.emulateIncomingActivity({
attachments,
channelData: { streamId: 't-00001', streamType: 'final' },
from: {
id: 'u-00001',
name: 'Bot',
role: 'bot'
},
id: 'm-00001',
text: 'Work completed!'
});

await pageConditions.numActivitiesShown(1);

// THEN: Should render the activity.
await host.snapshot();
});
</script>
</body>
</html>
5 changes: 5 additions & 0 deletions __tests__/html/withBuiltinDecorator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */

describe('built-in decorator', () => {
test('with decorators', () => runHTML('withBuiltinDecorator'));
});
34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"./packages/test/harness",
"./packages/test/web-server",
"./packages/core",
"./packages/styles",
"./packages/support/cldr-data-downloader",
"./packages/support/cldr-data",
"./packages/api",
Expand Down
29 changes: 29 additions & 0 deletions packages/api/src/StyleOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,35 @@ type StyleOptions = {
* @default document.head
*/
stylesRoot?: Node;

/**
* Border animation
*/

/**
* Border animation 1st color
*
* CSS variable: `--webchat__animation--border-color-1` CSS variable to adjust the color
*
* New in 4.19.0.
*/
borderAnimationColor1?: string;
/**
* Border animation 2nd color
*
* CSS variable: `--webchat__animation--border-color-2` CSS variable to adjust the color
*
* New in 4.19.0.
*/
borderAnimationColor2?: string;
/**
* Border animation 3rd color
*
* CSS variable: `--webchat__animation--border-color-3` CSS variable to adjust the color
*
* New in 4.19.0.
*/
borderAnimationColor3?: string;
};

// StrictStyleOptions is only used internally in Web Chat and for simplifying our code:
Expand Down
7 changes: 6 additions & 1 deletion packages/api/src/defaultStyleOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,12 @@ const DEFAULT_OPTIONS: Required<StyleOptions> = {

maxMessageLength: 2000,

stylesRoot: document.head
stylesRoot: document.head,

// Border animation
borderAnimationColor1: '#203C91',
borderAnimationColor2: '#4DD3FF',
borderAnimationColor3: '#2B8DD8'
};

export default DEFAULT_OPTIONS;
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export default function closest(element: HTMLElement, selector: string): HTMLEle

while (current) {
// "msMatchesSelector" is vendor-prefixed version of "matches".
// eslint-disable-next-line dot-notation
if ((current.matches || (current['msMatchesSelector'] as (selector: string) => boolean)).call(current, selector)) {
return current;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export default function renderAdaptiveCard(
// Because there could be timing difference between .parse and .render, we could be using wrong Markdown engine

// "onProcessMarkdown" is a static function but we are trying to scope it to the current object instead.
// eslint-disable-next-line dot-notation
adaptiveCard.constructor['onProcessMarkdown'] = (text: string, result: IMarkdownProcessingResult) => {
if (renderMarkdownAsHTML) {
result.outputHtml = renderMarkdownAsHTML(text);
Expand Down
3 changes: 0 additions & 3 deletions packages/bundle/src/index-es5.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/* eslint dot-notation: ["error", { "allowPattern": "^WebChat$" }] */
// window['WebChat'] is required for TypeScript

// Importing polyfills required for IE11/ES5.
import './polyfill';

Expand Down
Loading

0 comments on commit ea7a875

Please sign in to comment.