Skip to content

Commit 2a3d1d3

Browse files
OEvgenyCopilot
andauthored
feat: Add clipboard paste and drag-and-drop file support to both themes (#5829)
* feat: Add clipboard paste and drag-and-drop file support to both themes Add paste and drag-and-drop support for file attachments in the send box, consistent across both basic and fluent themes. Users can now paste files from the clipboard or drag files onto the chat to attach them. - Basic theme: Add onPaste handler, DropZone component, and handleAddFiles helper - Fluent theme: Add onPaste handler (drag-and-drop already existed) - All file types supported, respects disableFileUpload style option Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> fix: Destructure props in DropZone to satisfy ESLint rule Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> fix: Address PR review feedback - Gate paste and drop on uiState === 'disabled' in both themes - Respect sendAttachmentOn: 'attach' in basic theme handleAddFiles - Hide DropZone when UI is disabled Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> test: Add E2E tests for paste and drag-and-drop in basic theme - Add pasteFile.html: tests pasting files into send box, verifies attachment bar shows items and multiple pastes accumulate - Add dragAndDrop.upload.html: tests drag-and-drop flow in basic theme, verifies drop zone appears, files are attached on drop - Add sendBoxDropZone test ID to basic theme DropZone component Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> fix: Remove snapshot assertions from new E2E tests New tests cannot include baseline snapshot images without running in the CI Docker environment. Use assertion-only checks instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> test: Restore snapshot assertions for consistency with existing tests Baseline snapshot images need to be generated in the CI Docker environment. The tests will fail until baselines are committed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> test: Add baseline snapshots and fix pasteFile test - Generate baseline snapshot images for both E2E tests using Docker Selenium environment - Fix pasteFile test to use text/plain files instead of fake image/png to avoid thumbnail decode errors in browser Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> fix: Regenerate snapshots via jest-server for correct 360x640 viewport Previous snapshots used wrong viewport (780x441) because they bypassed the jest-server which sets the window to 360x640. Now generated through the jest-server proxy matching CI configuration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> fix: Update snapshots for position:relative on send box main div The DropZone component requires position:relative on .webchat__send-box__main for its absolute positioning overlay. This causes minor rendering differences in existing snapshot tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Check WIP Changelog and use js ext * Changelog * Remove width constraint * Fix types * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @OEvgeny * Fix snaps * Fix nits * Fix tsd * Review * Fix props type --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 8fcd02a commit 2a3d1d3

29 files changed

Lines changed: 518 additions & 118 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ Breaking changes in this release:
163163
- Added core mute/unmute functionality for speech-to-speech via `useRecorder` hook (silent chunks keep server connection alive), in PR [#5688](https://github.com/microsoft/BotFramework-WebChat/pull/5688), by [@pranavjoshi](https://github.com/pranavjoshi001)
164164
- 🧪 Added incremental streaming Markdown renderer for livestreaming, in PR [#5799](https://github.com/microsoft/BotFramework-WebChat/pull/5799), by [@OEvgeny](https://github.com/OEvgeny)
165165
- Fixed streaming Markdown renderer to preserve link reference definitions during incremental rendering and recover on error, in PR [#5808](https://github.com/microsoft/BotFramework-WebChat/pull/5808), by [@OEvgeny](https://github.com/OEvgeny)
166+
- Added clipboard paste and drag-and-drop file support to both basic and fluent themes, in PR [#5829](https://github.com/microsoft/BotFramework-WebChat/pull/5829), by [@OEvgeny](https://github.com/OEvgeny)
166167

167168
### Changed
168169

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
<head>
4+
<title>Drag and drop file upload (basic theme)</title>
5+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
6+
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone@7.8.7/babel.min.js"></script>
7+
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.6/umd/react.production.min.js"></script>
8+
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js"></script>
9+
<script crossorigin="anonymous" src="/test-harness.js"></script>
10+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
11+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
12+
<script crossorigin="anonymous" src="/__dist__/botframework-webchat-fluent-theme.production.min.js"></script>
13+
</head>
14+
<body>
15+
<main id="webchat"></main>
16+
<script>
17+
run(async function () {
18+
const { directLine, store } = testHelpers.createDirectLineEmulator();
19+
20+
renderWebChat(
21+
{
22+
directLine,
23+
store
24+
},
25+
document.getElementById('webchat')
26+
);
27+
28+
await pageConditions.uiConnected();
29+
30+
const dataTransfer = new DataTransfer();
31+
32+
dataTransfer.items.add(new File([new ArrayBuffer(100)], 'simple.txt'));
33+
34+
// WHEN: Dragging a file into document.
35+
const dragEnterDocumentEvent = new DragEvent('dragenter', {
36+
bubbles: true,
37+
cancelable: true,
38+
dataTransfer
39+
});
40+
41+
document.dispatchEvent(dragEnterDocumentEvent);
42+
43+
// THEN: Should render the drop zone.
44+
await pageConditions.became(
45+
'Drop zone should appear',
46+
() => !!document.querySelector(`[data-testid="${WebChat.testIds['sendBoxDropZone']}"]`),
47+
1000
48+
);
49+
50+
await host.snapshot('local');
51+
52+
// WHEN: Dragging into the drop zone.
53+
const dragEnterDropZoneEvent = new DragEvent('dragenter', {
54+
bubbles: true,
55+
cancelable: true,
56+
dataTransfer
57+
});
58+
59+
document
60+
.querySelector(`[data-testid="${WebChat.testIds['sendBoxDropZone']}"]`)
61+
.dispatchEvent(dragEnterDropZoneEvent);
62+
63+
// THEN: Should render drop zone in droppable state.
64+
await host.snapshot('local');
65+
66+
// WHEN: Dropping into the drop zone.
67+
const dropEvent = new DragEvent('drop', {
68+
bubbles: true,
69+
cancelable: true,
70+
dataTransfer
71+
});
72+
73+
document.querySelector(`[data-testid="${WebChat.testIds['sendBoxDropZone']}"]`).dispatchEvent(dropEvent);
74+
75+
// THEN: An attachment should appear in the attachment bar.
76+
await pageConditions.became(
77+
'Attachment bar item should appear',
78+
() => !!pageElements.byTestId(WebChat.testIds.sendBoxAttachmentBarItem),
79+
1000
80+
);
81+
82+
await host.snapshot('local');
83+
84+
// WHEN: Dragging a file into document again.
85+
const dragEnterDocumentEvent1 = new DragEvent('dragenter', {
86+
bubbles: true,
87+
cancelable: true,
88+
dataTransfer
89+
});
90+
91+
document.dispatchEvent(dragEnterDocumentEvent1);
92+
93+
// THEN: Should render the drop zone again.
94+
await pageConditions.became(
95+
'Drop zone should appear again',
96+
() => !!document.querySelector(`[data-testid="${WebChat.testIds['sendBoxDropZone']}"]`),
97+
1000
98+
);
99+
100+
// WHEN: Dragging a file over the document.
101+
const dragOverDocumentEvent = new DragEvent('dragover', {
102+
bubbles: true,
103+
cancelable: true,
104+
dataTransfer
105+
});
106+
107+
document.dispatchEvent(dragOverDocumentEvent);
108+
109+
// THEN: The default browser behavior should be prevented.
110+
await pageConditions.became(
111+
'DragOver event preventDefault is called',
112+
() => dragOverDocumentEvent.defaultPrevented,
113+
1000
114+
);
115+
116+
// WHEN: Dropping out of the drop zone.
117+
const dropEvent1 = new DragEvent('drop', {
118+
bubbles: true,
119+
cancelable: true,
120+
dataTransfer
121+
});
122+
123+
document.body.dispatchEvent(dropEvent1);
124+
125+
// THEN: Should render single attachment (no duplicate from out-of-zone drop).
126+
await host.snapshot('local');
127+
});
128+
</script>
129+
</body>
130+
</html>
7.59 KB
Loading
8.13 KB
Loading
6.05 KB
Loading
6.05 KB
Loading
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
<head>
4+
<title>Paste file into send box</title>
5+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
6+
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone@7.8.7/babel.min.js"></script>
7+
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.6/umd/react.production.min.js"></script>
8+
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js"></script>
9+
<script crossorigin="anonymous" src="/test-harness.js"></script>
10+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
11+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
12+
<script crossorigin="anonymous" src="/__dist__/botframework-webchat-fluent-theme.production.min.js"></script>
13+
</head>
14+
<body>
15+
<main id="webchat"></main>
16+
<script>
17+
run(async function () {
18+
const { directLine, store } = testHelpers.createDirectLineEmulator();
19+
20+
renderWebChat(
21+
{
22+
directLine,
23+
store
24+
},
25+
document.getElementById('webchat')
26+
);
27+
28+
await pageConditions.uiConnected();
29+
30+
// WHEN: Pasting a file into the send box text area.
31+
const textBox = document.querySelector(`[data-testid="${WebChat.testIds['sendBoxTextBox']}"]`);
32+
const dataTransfer = new DataTransfer();
33+
34+
dataTransfer.items.add(new File([new ArrayBuffer(100)], 'test-document.txt', { type: 'text/plain' }));
35+
36+
const pasteEvent = new ClipboardEvent('paste', {
37+
bubbles: true,
38+
cancelable: true,
39+
clipboardData: dataTransfer
40+
});
41+
42+
textBox.dispatchEvent(pasteEvent);
43+
44+
// THEN: The paste event should be prevented (file handled by webchat).
45+
expect(pasteEvent.defaultPrevented).toBeTruthy();
46+
47+
// THEN: An attachment should appear in the attachment bar.
48+
await pageConditions.became(
49+
'Attachment bar item should appear',
50+
() => !!pageElements.byTestId(WebChat.testIds.sendBoxAttachmentBarItem),
51+
1000
52+
);
53+
54+
await host.snapshot('local');
55+
56+
// WHEN: Pasting a second file.
57+
const dataTransfer2 = new DataTransfer();
58+
59+
dataTransfer2.items.add(new File([new ArrayBuffer(200)], 'test-spreadsheet.csv', { type: 'text/csv' }));
60+
61+
const pasteEvent2 = new ClipboardEvent('paste', {
62+
bubbles: true,
63+
cancelable: true,
64+
clipboardData: dataTransfer2
65+
});
66+
67+
textBox.dispatchEvent(pasteEvent2);
68+
69+
// THEN: Both attachments should appear (accumulated, not replaced).
70+
await pageConditions.became(
71+
'Two attachment bar items should appear',
72+
() => document.querySelectorAll(`[data-testid="${WebChat.testIds.sendBoxAttachmentBarItem}"]`).length === 2,
73+
1000
74+
);
75+
76+
await host.snapshot('local');
77+
});
78+
</script>
79+
</body>
80+
</html>
6.05 KB
Loading
6.19 KB
Loading
58.5 KB
Loading

0 commit comments

Comments
 (0)