Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Legends:
- Fixed the keyboard focus indicator around the transcript ignoring the `transcriptVisualKeyboardIndicatorColor` style option and using the text color instead; in the Fluent theme, it now uses the Fluent focus stroke color, in PR [#5888](https://github.com/microsoft/BotFramework-WebChat/pull/5888), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed undefined or misnamed CSS variables: `CopilotMessageHeader` is limited to the maximum message bubble width again instead of spanning the full transcript width in the Fluent variant, citation links in the Fluent theme use the brand stroke color for their hover border instead of the text color, and `--webchat-borderFlair-color3Fade` and `--webchat-borderLoader-color3Fade` now set the third fade color of the border flair and loader, in PR [#5890](https://github.com/microsoft/BotFramework-WebChat/pull/5890), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed the keyboard focus indicator of audio and video attachments, and of videos in animation cards: the bubble clipped the browser focus ring, and the rest was covered by the audio player or not visible over a dark video; it is now shown on all four sides of the player, fixes [#5740](https://github.com/microsoft/BotFramework-WebChat/issues/5740), in PR [#5887](https://github.com/microsoft/BotFramework-WebChat/pull/5887), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed the "Copied" confirmation of the copy button of code blocks disappearing too quickly: it is now shown for 5 seconds in all themes, like the Copy button of messages; the button is no longer an assertive live region, which announced "Copied" on top of its new name and "Copy" again when the confirmation ended, related to [#5857](https://github.com/microsoft/BotFramework-WebChat/issues/5857), in PR [#5892](https://github.com/microsoft/BotFramework-WebChat/pull/5892), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed syntax-highlighted code having less than 4.5:1 color contrast: comments in the light code block theme (`github-light-default`) were 4.36:1 in the Fluent theme and now use the muted text color of the theme, `#656d76` (5.03:1), in PR [#5893](https://github.com/microsoft/BotFramework-WebChat/pull/5893), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed code blocks becoming empty when Web Chat is rendered with messages already in the store, such as again with the same store or from a saved state: code is shown without highlighting until the syntax highlighter is loaded, in PR [#5893](https://github.com/microsoft/BotFramework-WebChat/pull/5893), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed the native controls of audio and video players, such as volume and "more options", not being reachable with <kbd>TAB</kbd> and <kbd>SHIFT</kbd> + <kbd>TAB</kbd> inside an activity: the focus now moves through every control of the player and wraps within the activity; the players of audio, video, and animation cards are now named after the card title, fixes [#5738](https://github.com/microsoft/BotFramework-WebChat/issues/5738) and [#5709](https://github.com/microsoft/BotFramework-WebChat/issues/5709), in PR [#5891](https://github.com/microsoft/BotFramework-WebChat/pull/5891), by [@OEvgeny](https://github.com/OEvgeny)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone@7.8.7/babel.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone@7.26.5/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>
Expand All @@ -11,7 +11,16 @@
</head>
<body>
<main id="webchat" style="position: relative"></main>
<script type="text/babel">
<script type="importmap">
{
"imports": {
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for"
}
}
</script>
<script data-type="module" type="text/babel">
import { waitFor } from '@testduet/wait-for';

run(async function () {
await host.sendDevToolsCommand('Browser.setPermission', {
permission: { name: 'clipboard-write' },
Expand All @@ -28,11 +37,12 @@
WebChat: { ReactWebChat, testIds }
} = window; // Imports in UMD fashion.

const { directLine, store } = testHelpers.createDirectLineEmulator();
const clock = lolex.createClock();
const { directLine, store } = testHelpers.createDirectLineEmulator({ ponyfill: clock });

const App = () => (
<React.Fragment>
<ReactWebChat directLine={directLine} store={store} />
<ReactWebChat directLine={directLine} ponyfill={clock} store={store} />
<div style={{ gap: 8, position: 'absolute', top: 0, width: '100%' }}>
<label>
<div>Paste box</div>
Expand All @@ -48,6 +58,10 @@

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

await pageConditions.webChatRendered();

clock.tick(600);

await pageConditions.uiConnected();

await directLine.emulateIncomingActivity({
Expand Down Expand Up @@ -108,17 +122,20 @@
await host.sendKeys('ENTER');

// THEN: The copy button should change to "Copied".
// NOTE: "Copied" will eventually turn back into normal mode, this is time-sensitive screenshot.
await host.snapshot('local', { stable: false });
await waitFor(async () => expect(await testHelpers.getAccessibleName(copyButton)).toBe('Copied'));
await host.snapshot('local');

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

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

// Sleep for 1 second for the checkmark to go away.
await testHelpers.sleep(500);
// WHEN: 5 seconds after pressing the "Copy" button.
clock.tick(5_000);

// THEN: The checkmark should go away.
await waitFor(async () => expect(await testHelpers.getAccessibleName(copyButton)).toBe('Copy'));

// WHEN: Hiding Web Chat and showing it back.
document.getElementById('webchat').style.display = 'none';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,25 @@
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone@7.8.7/babel.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone@7.26.5/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>
</head>
<body>
<main id="webchat" style="position: relative;"></main>
<script type="text/babel">
<main id="webchat" style="position: relative"></main>
<script type="importmap">
{
"imports": {
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for"
}
}
</script>
<script data-type="module" type="text/babel">
import { waitFor } from '@testduet/wait-for';

run(async function () {
await host.sendDevToolsCommand('Browser.setPermission', {
permission: { name: 'clipboard-write' },
Expand All @@ -28,6 +37,7 @@
WebChat: { ReactWebChat, testIds }
} = window; // Imports in UMD fashion.

// Using the real clock: with a fake clock, the message would arrive before the code highlighter is loaded.
Comment thread
OEvgeny marked this conversation as resolved.
const { directLine, store } = testHelpers.createDirectLineEmulator();

const App = () => (
Expand Down Expand Up @@ -91,17 +101,19 @@
await host.sendKeys('ENTER');

// THEN: The copy button should change to "Copied".
// NOTE: The screenshot is time-sensitive, do not wait until stable.
await host.snapshot('local', { stable: false });
await waitFor(async () => expect(await testHelpers.getAccessibleName(copyButton)).toBe('Copied'));
await host.snapshot('local');

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

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

// Sleep for 1 second for the checkmark to go away.
await testHelpers.sleep(500);
// THEN: The checkmark should go away 5 seconds after pressing the "Copy" button.
await waitFor(async () => expect(await testHelpers.getAccessibleName(copyButton)).toBe('Copy'), {
timeout: 10_000
});

// WHEN: Hiding Web Chat and showing it back.
document.getElementById('webchat').style.display = 'none';
Expand Down
86 changes: 66 additions & 20 deletions __tests__/html2/markdown/codeBlockCopyButton/behavior.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,25 @@
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone@7.8.7/babel.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone@7.26.5/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>
</head>
<body>
<main id="webchat" style="position: relative;"></main>
<script type="text/babel">
<main id="webchat" style="position: relative"></main>
<script type="importmap">
{
"imports": {
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for"
}
}
</script>
<script data-type="module" type="text/babel">
import { waitFor } from '@testduet/wait-for';

run(async function () {
await host.sendDevToolsCommand('Browser.setPermission', {
permission: { name: 'clipboard-write' },
Expand All @@ -28,11 +37,12 @@
WebChat: { ReactWebChat, testIds }
} = window; // Imports in UMD fashion.

const { directLine, store } = testHelpers.createDirectLineEmulator();
const clock = lolex.createClock();
const { directLine, store } = testHelpers.createDirectLineEmulator({ ponyfill: clock });

const App = () => (
<React.Fragment>
<ReactWebChat directLine={directLine} store={store} />
<ReactWebChat directLine={directLine} ponyfill={clock} store={store} />
<div style={{ gap: 8, position: 'absolute', top: 0, width: '100%' }}>
<label>
<div>Paste box</div>
Expand All @@ -48,6 +58,10 @@

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

await pageConditions.webChatRendered();

clock.tick(600);

await pageConditions.uiConnected();

await directLine.emulateIncomingActivity({
Expand All @@ -72,40 +86,72 @@

await pageConditions.numActivitiesShown(1);

const copyButton = document.querySelector(`[data-testid="${testIds.codeBlockCopyButton}"]`);

const hideAndShowWebChat = () => {
document.getElementById('webchat').style.display = 'none';
document.body.offsetWidth; // Need for browser to refresh the layout.
document.getElementById('webchat').style.display = '';
};

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

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

// THEN: Should focus on the "Copy" button.
expect(document.activeElement).toBe(copyButton);
expect(await testHelpers.getAccessibleName(copyButton)).toBe('Copy');
await host.snapshot('local');

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

// THEN: The copy button should change to "Copied".
// NOTE: "Copied" will eventually turn back into normal mode, this is time-sensitive screenshot.
await host.snapshot('local', { stable: false });
// THEN: The copy button should change to "Copied", including its accessible name.
await waitFor(async () => expect(await testHelpers.getAccessibleName(copyButton)).toBe('Copied'));
await host.snapshot('local');

// WHEN: Paste into plain text and rich text text box.
// WHEN: 1 second later, Web Chat is hidden and shown again.
clock.tick(1_000);
hideAndShowWebChat();

// THEN: "Copied" should still be shown.
// It should be a plain button: not a toggle button, and not a live region, which would raise another event for "Copied" and an assertive "Copy" later.
const { name, properties, role } = await testHelpers.getAccessibilityNode(copyButton);

expect(name.value).toBe('Copied');
expect(role.value).toBe('button');
expect(properties.map(property => property.name)).not.toContain('live');
expect(properties.map(property => property.name)).not.toContain('pressed');

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

await host.click(document.querySelector(`[data-testid="${WebChat.testIds.sendBoxTextBox}"]`));
// THEN: Should paste the code.
expect(document.querySelector('[data-testid="paste box"]').value).toBe(
'In do pariatur id enim nisi adipisicing incididunt consectetur do velit quis aliqua ad.\n\nEt proident est fugiat duis exercitation qui sunt amet ipsum.\n'
);

// WHEN: Just before 5 seconds after pressing the "Copy" button.
clock.tick(3_999);

// THEN: "Copied" should still be shown.
expect(await testHelpers.getAccessibleName(copyButton)).toBe('Copied');

// WHEN: 5 seconds after pressing the "Copy" button.
clock.tick(1);

// Sleep for 1 second for the checkmark to go away.
await testHelpers.sleep(500);
// THEN: The "Copy" button should be back to normal.
await waitFor(async () => expect(await testHelpers.getAccessibleName(copyButton)).toBe('Copy'));

// WHEN: Hiding Web Chat and showing it back.
document.getElementById('webchat').style.display = 'none';
document.body.offsetWidth; // Need for browser to refresh the layout.
document.getElementById('webchat').style.display = '';
await host.click(document.querySelector(`[data-testid="${testIds.sendBoxTextBox}"]`));
hideAndShowWebChat();

// THEN: The "Copy" button should kept at normal.
// THEN: The "Copy" button should be kept at normal.
await host.snapshot('local');
expect(await testHelpers.getAccessibleName(copyButton)).toBe('Copy');
});
</script>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
import { useStyles } from '@msinternal/botframework-webchat-styles/react';
import { hooks } from 'botframework-webchat-api';
import cx from 'classnames';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import React, { memo, useCallback, useEffect, useState } from 'react';
import { instance, nullable, object, optional, pipe, readonly, string, type InferInput } from 'valibot';

import useAutoResetState from '../../../hooks/internal/useAutoResetState';
import { useQueueStaticElement } from '../../../providers/LiveRegionTwin';
import refObject from '../../../types/internal/refObject';
import ActivityButton from './ActivityButton';
Expand All @@ -28,12 +29,11 @@ type ActivityCopyButtonProps = InferInput<typeof activityCopyButtonPropsSchema>;
function ActivityCopyButton(props: ActivityCopyButtonProps) {
const { className, targetRef } = validateProps(activityCopyButtonPropsSchema, props);

const [{ clearTimeout, setTimeout }] = usePonyfill();
const [copied, setCopied] = useState(false);
const [permissionGranted, setPermissionGranted] = useState(false);
const [ponyfill] = usePonyfill();
const [copied, setCopied] = useAutoResetState(false, COPIED_DURATION, ponyfill);
const [uiState] = useUIState();
const classNames = useStyles(styles);
const copiedTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>();
const localize = useLocalizer();
const queueStaticElement = useQueueStaticElement();

Expand All @@ -55,17 +55,10 @@ function ActivityCopyButton(props: ActivityCopyButtonProps) {
.catch(error => console.error(`botframework-webchat-fluent-theme: Failed to copy to clipboard.`, error));

// Activating again restarts the confirmation.
copiedTimeoutRef.current && clearTimeout(copiedTimeoutRef.current);
copiedTimeoutRef.current = setTimeout(() => setCopied(false), COPIED_DURATION);
setCopied(true);

queueStaticElement(<div className={classNames['activity-copy-button__copy-announcement']}>{copiedText}</div>);
}, [classNames, clearTimeout, copiedText, copiedTimeoutRef, queueStaticElement, setCopied, setTimeout, targetRef]);

useEffect(
() => () => copiedTimeoutRef.current && clearTimeout(copiedTimeoutRef.current),
[clearTimeout, copiedTimeoutRef]
);
}, [classNames, copiedText, queueStaticElement, setCopied, targetRef]);

useEffect(() => {
let unmounted = false;
Expand Down
Loading
Loading