Skip to content

Commit

Permalink
[Fluent] Add copy button (#5259)
Browse files Browse the repository at this point in the history
* Add copy button

* Add copy button tests

* Put copy button inside bubble

* Fix screenshot

* Fix build pipeline

* Localize copy text

* Hide toolbox when not needed

* Loosen valibot parse

* Fix copy button text

* Use CSS animation to flip copied text

* Add clipboard permissions

* Add Copy button entry

* Fallback properly

* Use keyboard to tap the Copy button

* Put "Copy" button under main text only

* Clean up

* Remove unused var

* Incorporate PR feedback

* Incorporate PR feedbacks

* Incorporate PR feedbacks

* Incorporate PR feedbacks

* Add tests for dark vs. light and Fluent vs. Copilot

* Sort/dedupe CSS vars

* Prefer backgroundDisabled than background1Disabled

* Fixing padding-block
  • Loading branch information
compulim authored Aug 16, 2024
1 parent 451de26 commit 302d546
Show file tree
Hide file tree
Showing 39 changed files with 2,903 additions and 51 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
!/packages/test/fluent-bundle/dist
!/packages/test/harness/dist
!/packages/test/page-object/dist
!/packages/test/web-server/dist
!/serve-test.json
1 change: 1 addition & 0 deletions .github/workflows/pull-request-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ jobs:
./packages/test/fluent-bundle/dist/
./packages/test/harness/
./packages/test/page-object/dist/
./packages/test/web-server/dist/
./serve-test.json
./testharness.dockerfile
./testharness2.dockerfile
Expand Down
13 changes: 7 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/

### Added

- (Experimental) Added pre-chat message with starter prompts, in PR [#5255](https://github.com/microsoft/BotFramework-WebChat/issues/5255), by [@compulim](https://github.com/compulim)
- (Experimental) Added pre-chat message with starter prompts in Fluent UI, in PR [#5255](https://github.com/microsoft/BotFramework-WebChat/issues/5255), by [@compulim](https://github.com/compulim)
- (Experimental) Added `isPrimary` props to Fluent UI send box. When set, will wire up with `useSendBoxValue` and works with starter prompts in pre-chat message, in PR [#5257](https://github.com/microsoft/BotFramework-WebChat/issues/5257), by [@compulim](https://github.com/compulim)
- (Experimental) Expand Fluent theme support to activities and transcript, in PR [#5258](https://github.com/microsoft/BotFramework-WebChat/pull/5258), by [@OEvgeny](https://github.com/OEvgeny)
- Added new Fluent UI theme variant "copilot" with updated styling and components, in PR [#5258](https://github.com/microsoft/BotFramework-WebChat/pull/5258), by [@OEvgeny](https://github.com/OEvgeny)
- Introduced `ActivityDecorator` component for enhanced message styling and layout, in PR [#5258](https://github.com/microsoft/BotFramework-WebChat/pull/5258), by [@OEvgeny](https://github.com/OEvgeny)
- Added `CopilotMessageHeader` component for displaying bot information in the "copilot" variant, in PR [#5258](https://github.com/microsoft/BotFramework-WebChat/pull/5258), by [@OEvgeny](https://github.com/OEvgeny)
- Updated Fluent theme styling to improve accessibility and visual consistency, in PR [#5258](https://github.com/microsoft/BotFramework-WebChat/pull/5258), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed header font in copilot variant, in PR [#5261](https://github.com/microsoft/BotFramework-WebChat/pull/5261), by [@OEvgeny](https://github.com/OEvgeny)
- Added new Fluent UI theme variant "copilot" with updated styling and components, in PR [#5258](https://github.com/microsoft/BotFramework-WebChat/pull/5258), by [@OEvgeny](https://github.com/OEvgeny)
- Introduced `ActivityDecorator` component for enhanced message styling and layout, in PR [#5258](https://github.com/microsoft/BotFramework-WebChat/pull/5258), by [@OEvgeny](https://github.com/OEvgeny)
- Added `CopilotMessageHeader` component for displaying bot information in the "copilot" variant, in PR [#5258](https://github.com/microsoft/BotFramework-WebChat/pull/5258), by [@OEvgeny](https://github.com/OEvgeny)
- Updated Fluent theme styling to improve accessibility and visual consistency, in PR [#5258](https://github.com/microsoft/BotFramework-WebChat/pull/5258), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed header font in copilot variant, in PR [#5261](https://github.com/microsoft/BotFramework-WebChat/pull/5261), by [@OEvgeny](https://github.com/OEvgeny)
- (Experimental) Added "Copy" button to bot messages in Fluent UI if it contains keyword `AllowCopy`, in PR [#5259](https://github.com/microsoft/BotFramework-WebChat/pull/5259), by [@compulim](https://github.com/compulim)

### 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.
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.
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
122 changes: 122 additions & 0 deletions __tests__/html/fluentTheme/copyButton.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<!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/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" style="position: relative"></main>
<script type="text/babel">
run(async function () {
const {
React,
ReactDOM: { render },
WebChat: { FluentThemeProvider, ReactWebChat }
} = window; // Imports in UMD fashion.

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

const App = () => (
<React.Fragment>
<ReactWebChat directLine={directLine} store={store} />
<div style={{ gap: 8, position: 'absolute', top: 0, width: '100%' }}>
<label>
<div>Plain text box</div>
<input
data-testid="plain-text-box"
spellCheck={false}
style={{ background: '#f0f0f0', border: 0, height: 50, padding: 0, width: '100%' }}
type="textbox"
/>
</label>
<label>
<div>Rich text box</div>
<div
contentEditable={true}
data-testid="rich-text-box"
spellCheck={false}
style={{ background: '#f0f0f0', border: 0, height: 50, width: '100%' }}
/>
</label>
</div>
</React.Fragment>
);

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

await pageConditions.uiConnected();

expect(window.isSecureContext).toBe(true);

await host.sendDevToolsCommand('Browser.setPermission', {
permission: { name: 'clipboard-write' },
setting: 'granted'
});

await expect(navigator.permissions.query({ name: 'clipboard-write' })).resolves.toHaveProperty(
'state',
'granted'
);

await directLine.emulateIncomingActivity({
entities: [
{
'@context': 'https://schema.org',
'@id': '',
'@type': 'Message',
keywords: ['AllowCopy'],
type: 'https://schema.org/Message'
}
],
text: 'Mollit *aute* **aute** dolor ea ex magna incididunt nostrud sit nisi.',
type: 'message'
});

await pageConditions.numActivitiesShown(1);

// WHEN: Focus on the "Copy" button via keyboard.
await host.click(document.querySelector(`[data-testid="${WebChat.testIds.sendBoxTextBox}"]`));
await host.sendShiftTab(2);
await host.sendKeys('ENTER');

// THEN: Should focus on the "Copy" button
const copyButton = document.querySelector(`[data-testid="${WebChat.testIds.copyButton}"]`);

expect(document.activeElement).toBe(copyButton);
await host.snapshot();

// WHEN: Press ENTER on the "Copy" button.
await host.sendKeys('ENTER');

// THEN: The copy button should change to "Copied".
await host.snapshot();

// WHEN: Paste into plain text and rich text text box.
await host.click(document.querySelector('[data-testid="plain-text-box"]'));
await host.sendKeys('+CONTROL', 'v', '-CONTROL');

await host.click(document.querySelector('[data-testid="rich-text-box"]'));
await host.sendKeys('+CONTROL', 'v', '-CONTROL');

await host.click(document.querySelector(`[data-testid="${WebChat.testIds.sendBoxTextBox}"]`));

// Sleep for 1 second for the "Copied" text to go away.
await testHelpers.sleep(500);

// THEN: Plain text box should contains plain text, while rich text box should contains rich text.
await host.snapshot();
});
</script>
</body>
</html>
5 changes: 5 additions & 0 deletions __tests__/html/fluentTheme/copyButton.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('Fluent theme applied', () => {
test('copy button should work', () => runHTML('fluentTheme/copyButton'));
});
123 changes: 123 additions & 0 deletions __tests__/html/fluentTheme/copyButton.layout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<!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="/test-harness.js"></script>
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script crossorigin="anonymous" src="/__dist__/fluent-bundle.production.min.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>
.fui-FluentProvider {
display: contents;

--webchat__color--surface: var(--colorNeutralBackground3);
}
</style>
</head>
<body>
<main id="webchat" style="position: relative"></main>
<script type="text/babel">
run(async function () {
const {
Fluent: { createDarkTheme, createLightTheme, FluentProvider },
React,
ReactDOMClient: { createRoot },
WebChat: { FluentThemeProvider, ReactWebChat }
} = window; // Imports in UMD fashion.

const { directLine, store } = testHelpers.createDirectLineEmulator();
const searchParams = new URLSearchParams(location.search);
const styleOptions = {
botAvatarBackgroundColor: '#304E7A',
botAvatarImage:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAK0lEQVQ4T2P8z8Dwn4GKgHHUQIpDczQMKQ5ChtEwHA1DMkJgNNmQEWhoWgBMAiftPRtHngAAAABJRU5ErkJggg=='
};

const App = () => <ReactWebChat directLine={directLine} store={store} styleOptions={styleOptions} />;

const customBrandRamp = {
10: '#124C32',
20: '#1A5B3E',
30: '#216A4A',
40: '#297956',
50: '#308861',
60: '#38976D',
70: '#40A779',
80: '#158051',
90: '#4FC590',
100: '#56D49C',
110: '#5EE3A8',
120: '#79E8B7',
130: '#94ECC5',
140: '#AFF1D3',
150: '#C9F6E2',
160: '#E4FAF1'
};

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

root.render(
<FluentProvider
theme={
searchParams.get('theme') === 'dark'
? createDarkTheme(customBrandRamp)
: createLightTheme(customBrandRamp)
}
>
<FluentThemeProvider variant={searchParams.get('variant') || ''}>
<App />
</FluentThemeProvider>
</FluentProvider>
);

await pageConditions.uiConnected();

await directLine.emulateIncomingActivity({
// TODO: Attachment is buggy now: clipped into the text content now and not aligned horizontally.
// attachments: [
// {
// contentType: 'image/jpeg',
// contentUrl:
// 'https://raw.githubusercontent.com/compulim/BotFramework-MockBot/master/public/assets/surface1.jpg'
// }
// ],
entities: [
{
'@context': 'https://schema.org',
'@id': '',
'@type': 'Message',
keywords: ['AllowCopy'],
type: 'https://schema.org/Message',
citation: [
{
'@id':
'https://bing.com/',
'@type': 'Claim',
claimInterpreter: {
'@type': 'Project',
slogan: 'Surfaced with Azure OpenAI',
url: 'https://www.microsoft.com/en-us/ai/responsible-ai'
},
position: '1'
}
]
}
],
from: {
name: 'Copilot',
role: 'bot'
},
text: 'Mollit *aute* **aute** dolor[1] ea ex magna incididunt nostrud sit nisi.\n\n[1]: https://bing.com/ "Ex voluptate est dolore"',
type: 'message'
});

await pageConditions.numActivitiesShown(1);

// THEN: "Copy" button should appear after the message.
await host.snapshot();
});
</script>
</body>
</html>
13 changes: 13 additions & 0 deletions __tests__/html/fluentTheme/copyButton.layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */

describe('Fluent theme applied', () => {
describe.each([
['dark', 'fluent'],
['dark', 'copilot'],
['light', 'fluent'],
['light', 'copilot']
])('with %s theme and %s variant', (theme, variant) =>
test('copy button should layout properly', () =>
runHTML(`fluentTheme/copyButton.layout?${new URLSearchParams({ theme, variant }).toString()}`))
);
});
17 changes: 15 additions & 2 deletions __tests__/html/withEmoji.5.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
Expand Down Expand Up @@ -56,11 +56,24 @@
await host.sendKeys('ABC');
expect(getTextWithCaret()).toBe('ABC|');

// Make sure we have write permissions to the clipboard.
expect(window.isSecureContext).toBe(true);

await host.sendDevToolsCommand('Browser.setPermission', {
permission: { name: 'clipboard-write' },
setting: 'granted'
});

await expect(navigator.permissions.query({ name: 'clipboard-write' })).resolves.toHaveProperty(
'state',
'granted'
);

// In WebDriver, CTRL + X do not cut to clipboard.
// We cannot do CTRL + A followed by CTRL + X here.
// Instead, we are writing to clipboard directly.
await host.sendKeys('+CONTROL', 'A', '-CONTROL');
navigator.clipboard?.writeText(document.activeElement.value) || document.execCommand('copy');
await (navigator.clipboard?.writeText(document.activeElement.value) || document.execCommand('copy'));

await host.sendKeys('BACK_SPACE');
expect(getTextWithCaret()).toBe('|');
Expand Down
1 change: 1 addition & 0 deletions docker-compose-wsl2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ services:
dockerfile: testharness2.dockerfile
ports:
- '5081:80'
- '5443:443'
stop_grace_period: 0s
volumes:
- ./__tests__/html/:/var/web/__tests__/html/
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"precommit:eslint:support-cldr-data": "cd packages && cd support && cd cldr-data && npm run precommit:eslint",
"precommit:eslint:test-harness": "cd packages && cd test && cd harness && npm run precommit:eslint",
"precommit:eslint:test-page-object": "cd packages && cd test && cd page-object && npm run precommit:eslint",
"precommit:eslint:web-server": "cd packages && cd test && cd web-server && npm run precommit:eslint",
"precommit:typecheck": "concurrently --raw \"npm run precommit:typecheck:*\"",
"precommit:typecheck:api": "cd packages && cd api && npm run precommit:typecheck",
"precommit:typecheck:bundle": "cd packages && cd bundle && npm run precommit:typecheck",
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/localization/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
"CONNECTIVITY_STATUS_ALT_SLOW_CONNECTION": "Taking longer than usual to connect.",
"CONNECTIVITY_STATUS_ALT": "Connectivity Status: $1",
"_CONNECTIVITY_STATUS_ALT.comment": "This is for screen reader. $1 will be one of \"CONNECTIVITY_STATUS_ALT_\"*.",
"COPY_BUTTON_TEXT": "Copy",
"COPY_BUTTON_COPIED_TEXT": "Copied",
"_COPY_BUTTON_COPIED.comment": "After clicking on the copy button, this text will show briefly",
"FILE_CONTENT_ALT": "'$1'",
"FILE_CONTENT_DOWNLOADABLE_ALT": "Download file '$1'",
"FILE_CONTENT_DOWNLOADABLE_WITH_SIZE_ALT": "Download file '$1' of size $2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
:global(.webchat-fluent) .activity-toolbox {
display: flex;
padding-block: var(--webchat__bubble--block-padding);
padding-block-start: 0; /* Specifically set block-start to 0. */
padding-inline: var(--webchat__bubble--inline-padding);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { WebChatActivity } from 'botframework-webchat-core';
import React, { memo, useMemo } from 'react';
import { array, looseObject, optional, parse, string } from 'valibot';
import { useStyles } from '../../styles';
import getMessageEntity from '../../utils/getMessageEntity';
import styles from './ActivityToolbox.module.css';
import CopyButton from './CopyButton';

type Props = Readonly<{ activity: WebChatActivity }>;

const activitySchema = looseObject({
entities: optional(array(looseObject({ type: string() }))),
type: string()
});

const ActivityToolbox = (props: Props) => {
const activity = useMemo(() => parse(activitySchema, props.activity), [props.activity]);
const classNames = useStyles(styles);

const allowCopy = useMemo(() => getMessageEntity(activity)?.keywords.includes('AllowCopy'), [activity]);

return allowCopy ? (
<div className={classNames['activity-toolbox']}>
<CopyButton activity={activity} />
</div>
) : null;
};

ActivityToolbox.displayName = 'ActivityToolbox';

export default memo(ActivityToolbox);
Loading

0 comments on commit 302d546

Please sign in to comment.