Skip to content

Commit 39e68a1

Browse files
dawolff-mscompulim
andauthored
Add ability to customize Web Chat by extending UI without re-implementing existing controls (#4539)
* moving AccessKeySinkSurface into Composer * moving BasicWebChat CSS logic (removing unused) * exposing BasicWebChat internal components for reuse * adding test case for changes * adding recomposing sample for changes * updating changelog * undoing most changes, leaving just the exports * updating sample to match newest changes * Add screenshot Co-authored-by: William Wong <[email protected]>
1 parent e15d3a8 commit 39e68a1

20 files changed

+36242
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2222

2323
## [Unreleased]
2424

25+
### Added
26+
27+
- Added ability for developers to customize Web Chat by extending the default UI without having to re-implement existing components. [@dawolff-ms](https://github.com/dawolff-ms) in PR [#4539](https://github.com/microsoft/BotFramework-WebChat/pull/4539)
28+
2529
### Fixed
2630

2731
- Fixes [#4558](https://github.com/microsoft/BotFramework-WebChat/issues/4558). In high contrast mode, "Retry" link button should use link color as defined by [CSS System Colors](https://w3c.github.io/csswg-drafts/css-color/#css-system-colors), by [@beyackle2](https://github.com/beyackle2) in PR [#4537](https://github.com/microsoft/BotFramework-WebChat/pull/4537)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<!DOCTYPE html>
2+
<html lang="en-US">
3+
<head>
4+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
<script crossorigin="anonymous" src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
6+
<script crossorigin="anonymous" src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
7+
<script crossorigin="anonymous" src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
8+
<script crossorigin="anonymous" src="/test-harness.js"></script>
9+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
10+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
11+
</head>
12+
<body>
13+
<div id="webchat"></div>
14+
<script type="text/babel" data-presets="env,stage-3,react">
15+
const {
16+
ReactDOM: { render },
17+
WebChat: {
18+
Components: { AccessKeySinkSurface, BasicToaster, BasicTranscript, BasicConnectivityStatus, BasicSendBox, Composer },
19+
createDirectLine
20+
}
21+
} = window;
22+
23+
run(async function () {
24+
const directLine = await createDirectLine({ token: await testHelpers.token.fetchDirectLineToken() });
25+
const store = testHelpers.createStore();
26+
27+
const classes = {
28+
surface: "surface-class-name",
29+
sendBox: "send-box-class-name",
30+
transcript: "transcript-class-name",
31+
customComponent: "custom-component-class-name"
32+
}
33+
const role = "main";
34+
35+
render(
36+
<Composer directLine={directLine} store={store}>
37+
<AccessKeySinkSurface className={classes.surface} role={role}>
38+
<BasicToaster />
39+
<BasicSendBox className={classes.sendBox} />
40+
<BasicConnectivityStatus />
41+
<div className={classes.customComponent}>This is a custom component.</div>
42+
<BasicTranscript className={classes.transcript} />
43+
</AccessKeySinkSurface>
44+
</Composer>,
45+
document.getElementById('webchat')
46+
);
47+
48+
await pageConditions.uiConnected();
49+
50+
const container = document.getElementById('webchat');
51+
expect(container?.firstChild).not.toBeUndefined();
52+
53+
const surface = container.firstChild;
54+
expect(surface.className).toContain(classes.surface);
55+
expect(surface.role).toEqual(role);
56+
57+
const components = surface.childNodes;
58+
expect(components[0].className).toContain("toaster");
59+
expect(components[1].className).toContain(classes.sendBox);
60+
expect(components[2].textContent).toContain("Connectivity Status");
61+
expect(components[3].className).toContain(classes.customComponent);
62+
expect(components[4].className).toContain("keyboard-help");
63+
expect(components[5].className).toContain(classes.transcript);
64+
65+
await host.snapshot();
66+
});
67+
</script>
68+
</body>
69+
</html>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */
2+
3+
describe('customization', () => {
4+
test('should render BasicWebChat components in a different structure', () => runHTML('customization.basicWebChat.restructure.html'));
5+
});

packages/component/src/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import ReactWebChat, { ReactWebChatProps } from './ReactWebChat';
44

55
import Composer, { ComposerProps } from './Composer';
66

7+
import AccessKeySinkSurface from './Utils/AccessKeySink/Surface';
8+
79
import BasicWebChat, { BasicWebChatProps } from './BasicWebChat';
10+
import BasicConnectivityStatus from './BasicConnectivityStatus';
11+
import BasicSendBox from './BasicSendBox';
12+
import BasicToaster from './BasicToaster';
13+
import BasicTranscript from './BasicTranscript';
814

915
import Avatar from './Activity/Avatar';
1016
import Bubble from './Activity/Bubble';
@@ -51,6 +57,13 @@ const Components = {
5157
Composer,
5258
Localize,
5359

60+
// Components for restructuring BasicWebChat
61+
AccessKeySinkSurface,
62+
BasicConnectivityStatus,
63+
BasicSendBox,
64+
BasicToaster,
65+
BasicTranscript,
66+
5467
// Components for recomposing activities and attachments
5568
AudioContent,
5669
FileContent,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# We are hitting an issue around react-scripts preflight check, and it is by design.
2+
# When react-scripts start supporting newer babel-jest@24, we could remove this check skip.
3+
# https://github.com/facebook/create-react-app/issues/4167
4+
5+
SKIP_PREFLIGHT_CHECK=true
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
# misc
15+
.DS_Store
16+
.env.local
17+
.env.development.local
18+
.env.test.local
19+
.env.production.local
20+
21+
npm-debug.log*
22+
yarn-debug.log*
23+
yarn-error.log*
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# Extending Web Chat with custom UI components
2+
3+
This sample shows how to extend the default UI for Web Chat to include your own UI components. This allows developers to create features within the Web Chat UI without having to re-implement already existing UI elements. For example, this sample will show how to insert a custom button between the chat transcript and the chat message box.
4+
5+
# Test out the hosted sample
6+
7+
- [Try out MockBot](https://microsoft.github.io/BotFramework-WebChat/06.recomposing-ui/e.extending-ui)
8+
9+
# Things to try out
10+
11+
- Click the custom "Say Hello!" button.
12+
13+
# Code
14+
15+
This project is based on [`create-react-app`](https://github.com/facebook/create-react-app).
16+
17+
The completed code contains multiple files. You can start by reading [`App.js`](https://github.com/microsoft/BotFramework-WebChat/tree/main/samples/06.recomposing-ui/e.extending-ui/src/App.js), which replaces the default Web Chat renderer with [`<CustomWebChat>`](https://github.com/microsoft/BotFramework-WebChat/tree/main/samples/06.recomposing-ui/e.extending-ui/src/CustomWebChat.js). The `<CustomWebChat>` component resembles the implementation of the [`<BasicWebChat>`](https://github.com/microsoft/BotFramework-WebChat/blob/main/packages/component/src/BasicWebChat.tsx) component that's used in the default Web Chat.
18+
19+
> Jump to [completed code](#completed-code) to see the end-result `App.js`, `CustomWebChat.js`, and `HelloButton.js`.
20+
21+
## Overview
22+
23+
### `App.js`
24+
25+
[`App.js`](https://github.com/microsoft/BotFramework-WebChat/tree/main/samples/06.recomposing-ui/e.extending-ui/src/App.js) will set up the container for hosting Web Chat components. The container will be using Direct Line chat channel. `getDirectLineToken()` asynchronously retrieves and sets the Direct Line token (you'll need to implement this yourself).
26+
27+
<!-- prettier-ignore-start -->
28+
```javascript
29+
const App = () => {
30+
const [directLine, setDirectLine] = React.useState();
31+
32+
if (!directLine) {
33+
getDirectLineToken().then(token => setDirectLine(createDirectLine({ token })));
34+
}
35+
...
36+
```
37+
<!-- prettier-ignore-end -->
38+
39+
We then pass the `directLine` token to the `Composer` component when it has a value. The `Composer` component is provided by Web Chat to allow developers to compose their own UI. Any component that is a descendant of the `Composer` will have access to the context of the chat through higher-order components and React Hooks.
40+
41+
<!-- prettier-ignore-start -->
42+
```javascript
43+
...
44+
return (
45+
<React.Fragment>
46+
{!!directLine && (
47+
<Components.Composer directLine={directLine}>
48+
<CustomWebChat />
49+
</Components.Composer>
50+
)}
51+
</React.Fragment>
52+
);
53+
}
54+
```
55+
<!-- prettier-ignore-end -->
56+
57+
### `CustomWebChat.js`
58+
59+
The `App.js` implementation places a component named `CustomWebChat` as a child of the `Composer`. The purpose of this component is to resemble the [`BasicWebChat`](https://github.com/microsoft/BotFramework-WebChat/blob/main/packages/component/src/BasicWebChat.tsx) component provided by the Web Chat components library, but provide a way for us to add our own custom components into the UI.
60+
61+
Lets start by first implementing the default `BasicWebChat` inside of `CustomWebChat`:
62+
63+
<!-- prettier-ignore-start -->
64+
```javascript
65+
import { Components } from 'botframework-webchat-component';
66+
67+
const CustomWebChat = () => {
68+
return (
69+
<Components.AccessKeySinkSurface>
70+
<Components.BasicToaster />
71+
<Components.BasicTranscript />
72+
<Components.BasicConnectivityStatus />
73+
<Components.BasicSendBox />
74+
</Components.AccessKeySinkSurface>
75+
);
76+
};
77+
```
78+
<!-- prettier-ignore-end -->
79+
80+
If you were to save, compile and run the code right now, you'll see the default Web Chat experience. This is because the `Basic*` components are the exact same components used by the `BasicWebChat` component. You can choose to modify these components however you like: remove, reorder, pass custom props (such as styling), etc. In this sample, however, we're going to demonstrate adding a completely new component to the UI.
81+
82+
### `HelloButton.js`
83+
84+
Our new component will be a "Say Hello!" button place right above the message box. When a user clicks this button, "Hello!" will be sent to the chat. This component will need to take advantage of the `useSendMessage` hook in order to be able to interact with the chat.
85+
86+
<!-- prettier-ignore-start -->
87+
```javascript
88+
import { hooks } from 'botframework-webchat-component';
89+
90+
const { useSendMessage } = hooks;
91+
92+
const HelloButton = () => {
93+
const sendMessage = useSendMessage();
94+
95+
return (
96+
<button onClick={() => sendMessage("Hello!")}>Say Hello!</button>
97+
);
98+
};
99+
```
100+
<!-- prettier-ignore-end -->
101+
102+
We can then place our `HelloButton` component in the `CustomWebChat` component wherever we want. In our scenario, we want it right above the `BasicSendBox`.
103+
104+
<!-- prettier-ignore-start -->
105+
```javascript
106+
import { Components } from 'botframework-webchat-component';
107+
import HelloButton from './HelloButton';
108+
109+
const CustomWebChat = () => {
110+
return (
111+
<Components.AccessKeySinkSurface>
112+
<Components.BasicToaster />
113+
<Components.BasicTranscript />
114+
<Components.BasicConnectivityStatus />
115+
<HelloButton />
116+
<Components.BasicSendBox />
117+
</Components.AccessKeySinkSurface>
118+
);
119+
};
120+
```
121+
<!-- prettier-ignore-end -->
122+
123+
If you save, compile and run the code, you'll see a "Say Hello!" button right above the message box. When you click it, "Hello!" will be sent to the chat!
124+
125+
## Completed Code
126+
127+
`App.js` (simplified):
128+
129+
<!-- prettier-ignore-start -->
130+
```javascript
131+
import { createDirectLine } from 'botframework-webchat';
132+
import { Components } from 'botframework-webchat-component';
133+
import React from 'react';
134+
import CustomWebChat from './CustomWebChat';
135+
136+
// In this demo, we are using Direct Line token from MockBot.
137+
// To talk to your bot, you should use the token exchanged using your Direct Line secret.
138+
// You should never put the Direct Line secret in the browser or client app.
139+
// https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-direct-line-3-0-authentication
140+
141+
async function getDirectLineToken() {
142+
const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' });
143+
const { token } = await res.json();
144+
145+
return token;
146+
}
147+
148+
const App = () => {
149+
const [directLine, setDirectLine] = React.useState();
150+
151+
if (!directLine) {
152+
getDirectLineToken().then(token => setDirectLine(createDirectLine({ token })));
153+
}
154+
155+
return (
156+
<React.Fragment>
157+
{!!directLine && (
158+
<Components.Composer directLine={directLine}>
159+
<CustomWebChat />
160+
</Components.Composer>
161+
)}
162+
</React.Fragment>
163+
);
164+
};
165+
166+
export default App;
167+
```
168+
<!-- prettier-ignore-end -->
169+
170+
`CustomWebChat.js`:
171+
172+
<!-- prettier-ignore-start -->
173+
```javascript
174+
import React from 'react';
175+
import { Components } from 'botframework-webchat-component';
176+
import HelloButton from './HelloButton';
177+
178+
const CustomWebChat = () => {
179+
return (
180+
<Components.AccessKeySinkSurface>
181+
<Components.BasicToaster />
182+
<Components.BasicTranscript />
183+
<Components.BasicConnectivityStatus />
184+
<HelloButton />
185+
<Components.BasicSendBox />
186+
</Components.AccessKeySinkSurface>
187+
);
188+
};
189+
190+
export default CustomWebChat;
191+
```
192+
<!-- prettier-ignore-end -->
193+
194+
`HelloButton.js`:
195+
196+
<!-- prettier-ignore-start -->
197+
```javascript
198+
import React from 'react';
199+
import { hooks } from 'botframework-webchat-component';
200+
201+
const { useSendMessage } = hooks;
202+
203+
const HelloButton = () => {
204+
const sendMessage = useSendMessage();
205+
206+
return (
207+
<button onClick={() => sendMessage("Hello!")}>Say Hello!</button>
208+
);
209+
};
210+
211+
export default HelloButton;
212+
```
213+
<!-- prettier-ignore-end -->
214+
215+
# Further reading
216+
217+
## Full list of Web Chat hosted samples
218+
219+
View the list of [available Web Chat samples](https://github.com/microsoft/BotFramework-WebChat/tree/main/samples)

0 commit comments

Comments
 (0)