Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fluent] Add copy button #5259

Merged
merged 27 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3f67c50
Add copy button
compulim Aug 9, 2024
0c21ada
Add copy button tests
compulim Aug 9, 2024
e4f0e1b
Put copy button inside bubble
compulim Aug 14, 2024
0b41780
Fix screenshot
compulim Aug 14, 2024
7106a65
Fix build pipeline
compulim Aug 14, 2024
5f180dc
Localize copy text
compulim Aug 14, 2024
1d109c3
Hide toolbox when not needed
compulim Aug 14, 2024
e38fa8a
Loosen valibot parse
compulim Aug 14, 2024
5a96897
Fix copy button text
compulim Aug 14, 2024
ad92fc7
Use CSS animation to flip copied text
compulim Aug 15, 2024
457cf64
Add clipboard permissions
compulim Aug 15, 2024
b56a5f7
Add Copy button entry
compulim Aug 15, 2024
d91c590
Fallback properly
compulim Aug 15, 2024
69e8131
Use keyboard to tap the Copy button
compulim Aug 15, 2024
ab4f1f2
Put "Copy" button under main text only
compulim Aug 15, 2024
c85aed5
Clean up
compulim Aug 15, 2024
645e7ac
Remove unused var
compulim Aug 15, 2024
b270079
Incorporate PR feedback
compulim Aug 15, 2024
3791145
Incorporate PR feedbacks
compulim Aug 15, 2024
9182f3b
Incorporate PR feedbacks
compulim Aug 15, 2024
c4ec602
Incorporate PR feedbacks
compulim Aug 15, 2024
8b501fe
Add tests for dark vs. light and Fluent vs. Copilot
compulim Aug 15, 2024
d2d1c64
Merge branch 'main' into feat-fluent-copy-button
compulim Aug 15, 2024
a41fa3f
Sort/dedupe CSS vars
compulim Aug 15, 2024
dbaf6a0
Prefer backgroundDisabled than background1Disabled
compulim Aug 15, 2024
968353b
Merge branch 'main' into feat-fluent-copy-button
compulim Aug 15, 2024
7bf13db
Fixing padding-block
compulim Aug 15, 2024
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 .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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ 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)
- (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.
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'));
});
64 changes: 64 additions & 0 deletions __tests__/html/fluentTheme/copyButton.withAttachments.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<!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 = () => <ReactWebChat directLine={directLine} store={store} />;

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

await pageConditions.uiConnected();

await directLine.emulateIncomingActivity({
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'
}
],
text: 'Mollit *aute* **aute** dolor ea ex magna incididunt nostrud sit nisi.',
type: 'message'
});

await pageConditions.numActivitiesShown(1);

// THEN: "Copy" button should appear after the message.
await host.snapshot();
});
</script>
</body>
</html>
5 changes: 5 additions & 0 deletions __tests__/html/fluentTheme/copyButton.withAttachments.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 with attachments', () => {
test('copy button should layout properly', () => runHTML('fluentTheme/copyButton.withAttachments'));
});
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 @@ -69,6 +69,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,5 @@
:global(.webchat-fluent) .activity-toolbox {
display: flex;
padding-block: 0 var(--webchat__bubble--block-padding);
compulim marked this conversation as resolved.
Show resolved Hide resolved
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);
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
:global(.webchat-fluent) .activity-toolbox__base-button {
align-items: center;
align-self: stretch;
compulim marked this conversation as resolved.
Show resolved Hide resolved
appearance: none;
background: var(--webchat-colorNeutralBackground1);
border-radius: var(--webchat-borderRadiusMedium);
border: 1px solid var(--webchat-colorNeutralStroke1);
color: var(--webchat-colorNeutralForeground1);
display: flex;
gap: var(--webchat-spacingHorizontalXS);
justify-content: center;
padding: 5px var(--webchat-spacingHorizontalM);
}

:global(.webchat-fluent) .activity-toolbox__base-button:hover {
background: var(--webchat-colorNeutralBackground1Hover);
border: 1px solid var(--webchat-colorNeutralStroke1Hover);
compulim marked this conversation as resolved.
Show resolved Hide resolved
color: var(--webchat-colorNeutralForeground1Hover);
}

:global(.webchat-fluent) .activity-toolbox__base-button:active {
background: var(--webchat-colorNeutralBackground1Pressed);
border: 1px solid var(--webchat-colorNeutralStroke1Pressed);
compulim marked this conversation as resolved.
Show resolved Hide resolved
color: var(--webchat-colorNeutralForeground1Pressed);
}

:global(.webchat-fluent) .activity-toolbox__base-button:focus-visible {
background: var(--webchat-colorNeutralBackground1);
outline: var(--webchat-strokeWidthThick) solid var(--webchat-colorStrokeFocus2);
outline-offset: calc(var(--webchat-strokeWidthThick) * -1);
}

:global(.webchat-fluent) .activity-toolbox__base-button:disabled {
background: var(--webchat-colorNeutralBackgroundDisabled);
border: 1px solid var(--webchat-colorNeutralStrokeDisabled);
compulim marked this conversation as resolved.
Show resolved Hide resolved
color: var(--webchat-colorNeutralForegroundDisabled);
}

:global(.webchat-fluent) .activity-toolbox__base-button-icon {
height: 20px;
width: 20px;
}
Loading
Loading