diff --git a/ACHIEVEMENTS.md b/ACHIEVEMENTS.md index 647a074c5d..9bb4ce7118 100644 --- a/ACHIEVEMENTS.md +++ b/ACHIEVEMENTS.md @@ -45,6 +45,15 @@ A curated list of major achievements by the Web Chat team. This document celebra ## 🎨 UI & Theming +### 📎 Attachment Preview for `sendAttachmentOn: "send"` + +**Goal:** Improve multi-file upload UX by introducing persistent attachment previews. +**By:** [@compulim](https://github.com/compulim) in [PR #5464](https://github.com/microsoft/BotFramework-WebChat/pull/5464) + +- Added `SendBoxAttachmentBar` to allow users to preview and remove attachments before sending. +- Previews switch between thumbnails and list mode based on count and accessibility settings. +- Enhances multi-folder upload workflows and aligns with modern messaging UX. + ### 🧾 Code Block Rendering & Highlighting System **Goal:** Unify and polish code block rendering across Markdown and UI components. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a39f3f111..bd90734047 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,8 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/ - Existing behavior will be kept and activities will be grouped by `sender` followed by `status` - `useGroupActivitiesByName` is favored over the existing `useGroupActivities` hook for performance reason - Middleware which support the new grouping name init argument should only compute the grouping if they match the grouping name, or the grouping name is not specified, otherwise, should do nothing and call the downstream middleware +- Resolved [#5463](https://github.com/microsoft/BotFramework-WebChat/issues/5463). Added attachment preview for `sendAttachmentOn: "send"`, in PR [#5464](https://github.com/microsoft/BotFramework-WebChat/pull/5464), by [@compulim](https://github.com/compulim) + - Attaching files will no longer remove previously attached files ### Changed diff --git a/__tests__/__image_snapshots__/html/on-send-js-with-send-attachment-on-of-send-should-send-attachments-when-the-send-button-is-clicked-1-snap.png b/__tests__/__image_snapshots__/html/on-send-js-with-send-attachment-on-of-send-should-send-attachments-when-the-send-button-is-clicked-1-snap.png index 7f290e4037..42e60134b2 100644 Binary files a/__tests__/__image_snapshots__/html/on-send-js-with-send-attachment-on-of-send-should-send-attachments-when-the-send-button-is-clicked-1-snap.png and b/__tests__/__image_snapshots__/html/on-send-js-with-send-attachment-on-of-send-should-send-attachments-when-the-send-button-is-clicked-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/simple-js-with-send-attachment-on-unset-should-send-attachments-when-the-send-button-is-clicked-1-snap.png b/__tests__/__image_snapshots__/html/simple-js-with-send-attachment-on-unset-should-send-attachments-when-the-send-button-is-clicked-1-snap.png index 7f290e4037..42e60134b2 100644 Binary files a/__tests__/__image_snapshots__/html/simple-js-with-send-attachment-on-unset-should-send-attachments-when-the-send-button-is-clicked-1-snap.png and b/__tests__/__image_snapshots__/html/simple-js-with-send-attachment-on-unset-should-send-attachments-when-the-send-button-is-clicked-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/simple-keyboard-only-js-with-send-attachment-on-unset-and-use-keyboard-for-the-flow-should-send-attachments-when-the-send-button-is-clicked-2-snap.png b/__tests__/__image_snapshots__/html/simple-keyboard-only-js-with-send-attachment-on-unset-and-use-keyboard-for-the-flow-should-send-attachments-when-the-send-button-is-clicked-2-snap.png index df26f8a083..42e60134b2 100644 Binary files a/__tests__/__image_snapshots__/html/simple-keyboard-only-js-with-send-attachment-on-unset-and-use-keyboard-for-the-flow-should-send-attachments-when-the-send-button-is-clicked-2-snap.png and b/__tests__/__image_snapshots__/html/simple-keyboard-only-js-with-send-attachment-on-unset-and-use-keyboard-for-the-flow-should-send-attachments-when-the-send-button-is-clicked-2-snap.png differ diff --git a/__tests__/__image_snapshots__/html/simple-keyboard-only-js-with-send-attachment-on-unset-and-use-keyboard-for-the-flow-should-send-attachments-when-the-send-button-is-clicked-3-snap.png b/__tests__/__image_snapshots__/html/simple-keyboard-only-js-with-send-attachment-on-unset-and-use-keyboard-for-the-flow-should-send-attachments-when-the-send-button-is-clicked-3-snap.png index 854959e267..8bdf239e57 100644 Binary files a/__tests__/__image_snapshots__/html/simple-keyboard-only-js-with-send-attachment-on-unset-and-use-keyboard-for-the-flow-should-send-attachments-when-the-send-button-is-clicked-3-snap.png and b/__tests__/__image_snapshots__/html/simple-keyboard-only-js-with-send-attachment-on-unset-and-use-keyboard-for-the-flow-should-send-attachments-when-the-send-button-is-clicked-3-snap.png differ diff --git a/__tests__/__image_snapshots__/html/use-send-box-attachments-js-call-use-send-box-attachments-hook-should-get-set-and-upload-attachments-1-snap.png b/__tests__/__image_snapshots__/html/use-send-box-attachments-js-call-use-send-box-attachments-hook-should-get-set-and-upload-attachments-1-snap.png index aeb10c88f6..855a8dd435 100644 Binary files a/__tests__/__image_snapshots__/html/use-send-box-attachments-js-call-use-send-box-attachments-hook-should-get-set-and-upload-attachments-1-snap.png and b/__tests__/__image_snapshots__/html/use-send-box-attachments-js-call-use-send-box-attachments-hook-should-get-set-and-upload-attachments-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/with-message-js-with-send-attachment-on-unset-should-send-attachments-with-a-message-1-snap.png b/__tests__/__image_snapshots__/html/with-message-js-with-send-attachment-on-unset-should-send-attachments-with-a-message-1-snap.png deleted file mode 100644 index 7f290e4037..0000000000 Binary files a/__tests__/__image_snapshots__/html/with-message-js-with-send-attachment-on-unset-should-send-attachments-with-a-message-1-snap.png and /dev/null differ diff --git a/__tests__/__image_snapshots__/html/with-message-js-with-send-attachment-on-unset-should-send-attachments-with-a-message-2-snap.png b/__tests__/__image_snapshots__/html/with-message-js-with-send-attachment-on-unset-should-send-attachments-with-a-message-2-snap.png deleted file mode 100644 index 79c39f275d..0000000000 Binary files a/__tests__/__image_snapshots__/html/with-message-js-with-send-attachment-on-unset-should-send-attachments-with-a-message-2-snap.png and /dev/null differ diff --git a/__tests__/html/assets/uploads/forzahorizon5.jpg b/__tests__/html/assets/uploads/forzahorizon5.jpg new file mode 100644 index 0000000000..8cae87a2ed Binary files /dev/null and b/__tests__/html/assets/uploads/forzahorizon5.jpg differ diff --git a/__tests__/html/assets/uploads/haloinfinite.jpg b/__tests__/html/assets/uploads/haloinfinite.jpg new file mode 100644 index 0000000000..dc890be74c Binary files /dev/null and b/__tests__/html/assets/uploads/haloinfinite.jpg differ diff --git a/__tests__/html/assets/uploads/minecraftdungeons.jpg b/__tests__/html/assets/uploads/minecraftdungeons.jpg new file mode 100644 index 0000000000..61116a2ea4 Binary files /dev/null and b/__tests__/html/assets/uploads/minecraftdungeons.jpg differ diff --git a/__tests__/html/sendAttachmentOn/useSendBoxAttachments.html b/__tests__/html/sendAttachmentOn/useSendBoxAttachments.html index b996aad966..6b1c0b680a 100644 --- a/__tests__/html/sendAttachmentOn/useSendBoxAttachments.html +++ b/__tests__/html/sendAttachmentOn/useSendBoxAttachments.html @@ -54,7 +54,7 @@ const fileReader = new FileReader(); fileReader.onerror = reject; - fileReader.onload = () => resolve(fileReader.result); + fileReader.onload = () => resolve(new URL(fileReader.result)); fileReader.readAsDataURL(thumbnailBlob); }); @@ -64,7 +64,7 @@ useSendBoxAttachments()[1](attachmentsRef.current); }); - // THEN: It should show checkmark on the button. + // THEN: It should show checkmark on the button and file preview. await host.snapshot(); // WHEN: `useSendBoxAttachments` hook is called to get the attachments. diff --git a/__tests__/html/sendAttachmentOn/withMessage.js b/__tests__/html/sendAttachmentOn/withMessage.js deleted file mode 100644 index d9de69f19c..0000000000 --- a/__tests__/html/sendAttachmentOn/withMessage.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ - -describe('with "sendAttachmentOn" unset', () => { - test('should send attachments with a message', () => runHTML('sendAttachmentOn/withMessage')); -}); diff --git a/__tests__/html2/avatar/layout.default.html b/__tests__/html2/avatar/layout.default.html index 78a28ae013..f809362bf2 100644 --- a/__tests__/html2/avatar/layout.default.html +++ b/__tests__/html2/avatar/layout.default.html @@ -56,6 +56,8 @@ await pageObjects.sendMessageViaSendBox('no avatar'); await pageConditions.numActivitiesShown(6); + await pageConditions.allOutgoingActivitiesSent(); + await host.snapshot('local', { skipCheckAccessibility: true }); await renderWebChat({ diff --git a/__tests__/html2/bubble/fixedWidth.html b/__tests__/html2/bubble/fixedWidth.html index 3daaa580f2..676e72ff55 100644 --- a/__tests__/html2/bubble/fixedWidth.html +++ b/__tests__/html2/bubble/fixedWidth.html @@ -109,6 +109,8 @@ await pageConditions.numActivitiesShown(5); + await pageConditions.allImagesLoaded(); + await host.snapshot('local'); }); diff --git a/__tests__/html2/sendBox/previewBeforeSend/clearAfterSend.html b/__tests__/html2/sendBox/previewBeforeSend/clearAfterSend.html new file mode 100644 index 0000000000..aa3dbe9625 --- /dev/null +++ b/__tests__/html2/sendBox/previewBeforeSend/clearAfterSend.html @@ -0,0 +1,70 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/sendBox/previewBeforeSend/clearAfterSend.html.snap-1.png b/__tests__/html2/sendBox/previewBeforeSend/clearAfterSend.html.snap-1.png new file mode 100644 index 0000000000..8d857983d5 Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/clearAfterSend.html.snap-1.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/clearAfterSend.html.snap-2.png b/__tests__/html2/sendBox/previewBeforeSend/clearAfterSend.html.snap-2.png new file mode 100644 index 0000000000..22e5f04aa6 Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/clearAfterSend.html.snap-2.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/clearAfterSend.html.snap-3.png b/__tests__/html2/sendBox/previewBeforeSend/clearAfterSend.html.snap-3.png new file mode 100644 index 0000000000..9711ca603c Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/clearAfterSend.html.snap-3.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/deleteButton.html b/__tests__/html2/sendBox/previewBeforeSend/deleteButton.html new file mode 100644 index 0000000000..231443ebf0 --- /dev/null +++ b/__tests__/html2/sendBox/previewBeforeSend/deleteButton.html @@ -0,0 +1,65 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/sendBox/previewBeforeSend/deleteButton.html.snap-1.png b/__tests__/html2/sendBox/previewBeforeSend/deleteButton.html.snap-1.png new file mode 100644 index 0000000000..ef517bd036 Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/deleteButton.html.snap-1.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/deleteButtonInTextMode.html b/__tests__/html2/sendBox/previewBeforeSend/deleteButtonInTextMode.html new file mode 100644 index 0000000000..063ec11017 --- /dev/null +++ b/__tests__/html2/sendBox/previewBeforeSend/deleteButtonInTextMode.html @@ -0,0 +1,71 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/sendBox/previewBeforeSend/deleteButtonInTextMode.html.snap-1.png b/__tests__/html2/sendBox/previewBeforeSend/deleteButtonInTextMode.html.snap-1.png new file mode 100644 index 0000000000..8bab288416 Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/deleteButtonInTextMode.html.snap-1.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/escapeKey.html b/__tests__/html2/sendBox/previewBeforeSend/escapeKey.html new file mode 100644 index 0000000000..a8f42447d2 --- /dev/null +++ b/__tests__/html2/sendBox/previewBeforeSend/escapeKey.html @@ -0,0 +1,65 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/sendBox/previewBeforeSend/escapeKey.html.snap-1.png b/__tests__/html2/sendBox/previewBeforeSend/escapeKey.html.snap-1.png new file mode 100644 index 0000000000..f7a945f7a3 Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/escapeKey.html.snap-1.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/escapeKey.html.snap-2.png b/__tests__/html2/sendBox/previewBeforeSend/escapeKey.html.snap-2.png new file mode 100644 index 0000000000..8d857983d5 Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/escapeKey.html.snap-2.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/filePreview.html b/__tests__/html2/sendBox/previewBeforeSend/filePreview.html new file mode 100644 index 0000000000..27518303d3 --- /dev/null +++ b/__tests__/html2/sendBox/previewBeforeSend/filePreview.html @@ -0,0 +1,50 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/sendBox/previewBeforeSend/filePreview.html.snap-1.png b/__tests__/html2/sendBox/previewBeforeSend/filePreview.html.snap-1.png new file mode 100644 index 0000000000..c9ec39de05 Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/filePreview.html.snap-1.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/styleOptions.maxHeight.html b/__tests__/html2/sendBox/previewBeforeSend/styleOptions.maxHeight.html new file mode 100644 index 0000000000..03b0525713 --- /dev/null +++ b/__tests__/html2/sendBox/previewBeforeSend/styleOptions.maxHeight.html @@ -0,0 +1,70 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/sendBox/previewBeforeSend/styleOptions.maxHeight.html.snap-1.png b/__tests__/html2/sendBox/previewBeforeSend/styleOptions.maxHeight.html.snap-1.png new file mode 100644 index 0000000000..aa4d823528 Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/styleOptions.maxHeight.html.snap-1.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/styleOptions.maxHeight.html.snap-2.png b/__tests__/html2/sendBox/previewBeforeSend/styleOptions.maxHeight.html.snap-2.png new file mode 100644 index 0000000000..9575089038 Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/styleOptions.maxHeight.html.snap-2.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/styleOptions.two.html b/__tests__/html2/sendBox/previewBeforeSend/styleOptions.two.html new file mode 100644 index 0000000000..cde966bee1 --- /dev/null +++ b/__tests__/html2/sendBox/previewBeforeSend/styleOptions.two.html @@ -0,0 +1,67 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/sendBox/previewBeforeSend/styleOptions.two.html.snap-1.png b/__tests__/html2/sendBox/previewBeforeSend/styleOptions.two.html.snap-1.png new file mode 100644 index 0000000000..1f66b26da6 Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/styleOptions.two.html.snap-1.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/styleOptions.zero.html b/__tests__/html2/sendBox/previewBeforeSend/styleOptions.zero.html new file mode 100644 index 0000000000..0f8de66bfb --- /dev/null +++ b/__tests__/html2/sendBox/previewBeforeSend/styleOptions.zero.html @@ -0,0 +1,57 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/sendBox/previewBeforeSend/styleOptions.zero.html.snap-1.png b/__tests__/html2/sendBox/previewBeforeSend/styleOptions.zero.html.snap-1.png new file mode 100644 index 0000000000..ba55e85531 Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/styleOptions.zero.html.snap-1.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/textOnly.html b/__tests__/html2/sendBox/previewBeforeSend/textOnly.html new file mode 100644 index 0000000000..61ea9ce7dc --- /dev/null +++ b/__tests__/html2/sendBox/previewBeforeSend/textOnly.html @@ -0,0 +1,60 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/sendBox/previewBeforeSend/textOnly.html.snap-1.png b/__tests__/html2/sendBox/previewBeforeSend/textOnly.html.snap-1.png new file mode 100644 index 0000000000..9be850ff3a Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/textOnly.html.snap-1.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/textOnlyMaxHeight.html b/__tests__/html2/sendBox/previewBeforeSend/textOnlyMaxHeight.html new file mode 100644 index 0000000000..3f7453d797 --- /dev/null +++ b/__tests__/html2/sendBox/previewBeforeSend/textOnlyMaxHeight.html @@ -0,0 +1,60 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/sendBox/previewBeforeSend/textOnlyMaxHeight.html.snap-1.png b/__tests__/html2/sendBox/previewBeforeSend/textOnlyMaxHeight.html.snap-1.png new file mode 100644 index 0000000000..7eefde618b Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/textOnlyMaxHeight.html.snap-1.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/threeImages.dark.html b/__tests__/html2/sendBox/previewBeforeSend/threeImages.dark.html new file mode 100644 index 0000000000..692fd621ab --- /dev/null +++ b/__tests__/html2/sendBox/previewBeforeSend/threeImages.dark.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/__tests__/html2/sendBox/previewBeforeSend/threeImages.dark.html.snap-1.png b/__tests__/html2/sendBox/previewBeforeSend/threeImages.dark.html.snap-1.png new file mode 100644 index 0000000000..b8a465a32d Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/threeImages.dark.html.snap-1.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/threeImages.dark.html.snap-2.png b/__tests__/html2/sendBox/previewBeforeSend/threeImages.dark.html.snap-2.png new file mode 100644 index 0000000000..7497718a27 Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/threeImages.dark.html.snap-2.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/threeImages.html b/__tests__/html2/sendBox/previewBeforeSend/threeImages.html new file mode 100644 index 0000000000..ee2187ebd1 --- /dev/null +++ b/__tests__/html2/sendBox/previewBeforeSend/threeImages.html @@ -0,0 +1,74 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/sendBox/previewBeforeSend/threeImages.html.snap-1.png b/__tests__/html2/sendBox/previewBeforeSend/threeImages.html.snap-1.png new file mode 100644 index 0000000000..b8a465a32d Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/threeImages.html.snap-1.png differ diff --git a/__tests__/html2/sendBox/previewBeforeSend/threeImages.html.snap-2.png b/__tests__/html2/sendBox/previewBeforeSend/threeImages.html.snap-2.png new file mode 100644 index 0000000000..7497718a27 Binary files /dev/null and b/__tests__/html2/sendBox/previewBeforeSend/threeImages.html.snap-2.png differ diff --git a/__tests__/html2/sendBox/sendAttachmentOn/focus-indicator.css b/__tests__/html2/sendBox/sendAttachmentOn/focus-indicator.css new file mode 100644 index 0000000000..2100b6da4c --- /dev/null +++ b/__tests__/html2/sendBox/sendAttachmentOn/focus-indicator.css @@ -0,0 +1,4 @@ +*:focus { + background-color: yellow !important; + outline: dashed 2px Red !important; +} diff --git a/__tests__/html/sendAttachmentOn/withMessage.html b/__tests__/html2/sendBox/sendAttachmentOn/withMessage.html similarity index 57% rename from __tests__/html/sendAttachmentOn/withMessage.html rename to __tests__/html2/sendBox/sendAttachmentOn/withMessage.html index 5c71c3360f..a3d6315478 100644 --- a/__tests__/html/sendAttachmentOn/withMessage.html +++ b/__tests__/html2/sendBox/sendAttachmentOn/withMessage.html @@ -10,11 +10,24 @@
diff --git a/__tests__/html2/sendBox/sendAttachmentOn/withMessage.html.snap-1.png b/__tests__/html2/sendBox/sendAttachmentOn/withMessage.html.snap-1.png new file mode 100644 index 0000000000..c67e4df211 Binary files /dev/null and b/__tests__/html2/sendBox/sendAttachmentOn/withMessage.html.snap-1.png differ diff --git a/__tests__/html2/sendBox/sendAttachmentOn/withMessage.html.snap-2.png b/__tests__/html2/sendBox/sendAttachmentOn/withMessage.html.snap-2.png new file mode 100644 index 0000000000..af48e12eae Binary files /dev/null and b/__tests__/html2/sendBox/sendAttachmentOn/withMessage.html.snap-2.png differ diff --git a/__tests__/html2/sendBox/sendAttachmentOn/withMessage.html.snap-3.png b/__tests__/html2/sendBox/sendAttachmentOn/withMessage.html.snap-3.png new file mode 100644 index 0000000000..cc40ab8855 Binary files /dev/null and b/__tests__/html2/sendBox/sendAttachmentOn/withMessage.html.snap-3.png differ diff --git a/__tests__/setup/elements/getSendBoxTextBox.js b/__tests__/setup/elements/getSendBoxTextBox.js index 9e7337354e..aa62a77fd7 100644 --- a/__tests__/setup/elements/getSendBoxTextBox.js +++ b/__tests__/setup/elements/getSendBoxTextBox.js @@ -1,6 +1,6 @@ import { By } from 'selenium-webdriver'; -const CSS_SELECTOR = '[role="form"] > * > form > input[type="text"]'; +const CSS_SELECTOR = `[data-testid="send box text area"]`; export default async function getSendBoxTextBox(driver) { return await driver.findElement(By.css(CSS_SELECTOR)); diff --git a/__tests__/setup/local/forzahorizon5.jpg b/__tests__/setup/local/forzahorizon5.jpg new file mode 100644 index 0000000000..8cae87a2ed Binary files /dev/null and b/__tests__/setup/local/forzahorizon5.jpg differ diff --git a/__tests__/setup/local/haloinfinite.jpg b/__tests__/setup/local/haloinfinite.jpg new file mode 100644 index 0000000000..dc890be74c Binary files /dev/null and b/__tests__/setup/local/haloinfinite.jpg differ diff --git a/__tests__/setup/local/minecraftdungeons.jpg b/__tests__/setup/local/minecraftdungeons.jpg new file mode 100644 index 0000000000..61116a2ea4 Binary files /dev/null and b/__tests__/setup/local/minecraftdungeons.jpg differ diff --git a/html2-test-transformer.js b/html2-test-transformer.js index 1fa24c2f73..81c76cb521 100644 --- a/html2-test-transformer.js +++ b/html2-test-transformer.js @@ -5,10 +5,11 @@ const testRoot = resolve(__dirname, './__tests__/html2/'); module.exports = { process: (_, sourcePath) => { const html = relative(testRoot, sourcePath); + const shouldSkip = sourcePath.endsWith('.skip.html'); return { code: ` - test(${JSON.stringify(html)}, () => + test${shouldSkip ? '.skip' : ''}(${JSON.stringify(html)}, () => runHTML(${JSON.stringify(`/__tests__/html2/${html}`)})); ` }; diff --git a/minecraftdungeons.jpg b/minecraftdungeons.jpg new file mode 100644 index 0000000000..61116a2ea4 Binary files /dev/null and b/minecraftdungeons.jpg differ diff --git a/packages/api/src/StyleOptions.ts b/packages/api/src/StyleOptions.ts index 8fee5fd7b0..1891eadd02 100644 --- a/packages/api/src/StyleOptions.ts +++ b/packages/api/src/StyleOptions.ts @@ -961,6 +961,21 @@ type StyleOptions = { * To add new groupings, configure `groupActivitiesMiddleware` to output extra groups. Then, add the group names to `styleOptions.groupActivitiesBy`. */ groupActivitiesBy?: readonly string[] | undefined; + + /** + * Send box: maximum number of attachment item to preview as thumbnail before showing as text-only. + * Send box: maximum height of the attachment bar. + * + * @default 114 + */ + sendBoxAttachmentBarMaxHeight?: number; + + /** + * Send box: maximum number of attachment item to preview as thumbnail before showing as list item. + * + * @default 3 + */ + sendBoxAttachmentBarMaxThumbnail?: number; }; // StrictStyleOptions is only used internally in Web Chat and for simplifying our code: diff --git a/packages/api/src/defaultStyleOptions.ts b/packages/api/src/defaultStyleOptions.ts index 5303f9d583..b8806d5c56 100644 --- a/packages/api/src/defaultStyleOptions.ts +++ b/packages/api/src/defaultStyleOptions.ts @@ -311,7 +311,11 @@ const DEFAULT_OPTIONS: Required = { // Speech recognition speechRecognitionContinuous: false, - groupActivitiesBy: ['sender', 'status'] + groupActivitiesBy: ['sender', 'status'], + + // Send box attachment bar + sendBoxAttachmentBarMaxHeight: 114, + sendBoxAttachmentBarMaxThumbnail: 3 }; export default DEFAULT_OPTIONS; diff --git a/packages/api/src/hooks/useSendBoxAttachments.ts b/packages/api/src/hooks/useSendBoxAttachments.ts index 5a41e1940c..82ee0cbd1f 100644 --- a/packages/api/src/hooks/useSendBoxAttachments.ts +++ b/packages/api/src/hooks/useSendBoxAttachments.ts @@ -6,6 +6,8 @@ import useWebChatAPIContext from './internal/useWebChatAPIContext'; export default function useSendBoxAttachments(): readonly [ readonly SendBoxAttachment[], + // TODO: This should be Dispatch>, however Redux doesn't support this signature. + // When we move out of Redux, we should change it. (attachments: readonly SendBoxAttachment[]) => void ] { // TODO: We should use the selector from "core" package. diff --git a/packages/api/src/localization/en-US.json b/packages/api/src/localization/en-US.json index a675514234..d52cbdc966 100644 --- a/packages/api/src/localization/en-US.json +++ b/packages/api/src/localization/en-US.json @@ -100,6 +100,14 @@ "_REFERENCE_LIST_HEADER_OTHER.comment": "Header of the expand/collapse of reference list. This is for plural rule of \"other\".", "REFERENCE_LIST_HEADER_TWO": "$1 references", "_REFERENCE_LIST_HEADER_TWO.comment": "Header of the expand/collapse of reference list. This is for plural rule of \"two\".", + "SEND_BOX_ATTACHMENT_BAR_DELETE_BUTTON_ALT": "Delete attachment $1", + "_SEND_BOX_ATTACHMENT_BAR_DELETE_BUTTON_ALT.comment": "This is for screen reader and is the label for the delete button which appear on top of the file attachment, when clicked, will delete the file attachment and prevent it from being uploaded when the message is sent. $1 is the name of the file.", + "SEND_BOX_ATTACHMENT_BAR_DELETE_BUTTON_TOOLTIP": "Delete", + "_SEND_BOX_ATTACHMENT_BAR_DELETE_BUTTON_TOOLTIP.comment": "This is the tooltip for the delete button which appear on top of the file attachment. See SEND_BOX_ATTACHMENT_BAR_DELETE_BUTTON_ALT for details.", + "SEND_BOX_ATTACHMENT_BAR_GENERIC_FILE_ALT": "A file", + "_SEND_BOX_ATTACHMENT_BAR_GENERIC_FILE_ALT.comment": "This is for screen reader and is the label for a file attached in the send box but it does not have a filename.", + "SEND_BOX_ATTACHMENT_BAR_GENERIC_IMAGE_ALT": "An image", + "_SEND_BOX_ATTACHMENT_BAR_GENERIC_IMAGE_ALT.comment": "This is for screen reader and is the label for an image attached in the send box but it does not have a filename.", "SEND_BOX_IS_EMPTY_TOOLTIP_ALT": "Cannot send empty message.", "_SEND_BOX_IS_EMPTY_TOOLTIP_ALT.comment": "This is for screen reader on the send button. When the send text box is empty, the send button is disabled and this text on the send button explain why the button is disabled.", "SPEECH_INPUT_LISTENING": "Listening…", diff --git a/packages/api/src/localization/yue.json b/packages/api/src/localization/yue.json index 425cd52e91..0e0971f530 100644 --- a/packages/api/src/localization/yue.json +++ b/packages/api/src/localization/yue.json @@ -56,6 +56,10 @@ "REFERENCE_LIST_HEADER_MANY": "$1 篇參考", "REFERENCE_LIST_HEADER_OTHER": "$1 篇參考", "REFERENCE_LIST_HEADER_TWO": "兩篇參考", + "SEND_BOX_ATTACHMENT_BAR_DELETE_BUTTON_ALT": "刪除附件 $1", + "SEND_BOX_ATTACHMENT_BAR_DELETE_BUTTON_TOOLTIP": "刪除", + "SEND_BOX_ATTACHMENT_BAR_GENERIC_FILE_ALT": "一個檔案", + "SEND_BOX_ATTACHMENT_BAR_GENERIC_IMAGE_ALT": "一幅圖片", "SEND_BOX_IS_EMPTY_TOOLTIP_ALT": "唔可以傳送空白嘅訊息。", "SPEECH_INPUT_LISTENING": "聽緊你講嘢…", "SPEECH_INPUT_MICROPHONE_BUTTON_CLOSE_ALT": "閂咪", diff --git a/packages/component/src/ModdableIcon/ModdableIcon.tsx b/packages/component/src/ModdableIcon/ModdableIcon.tsx new file mode 100644 index 0000000000..da7ebd9fcf --- /dev/null +++ b/packages/component/src/ModdableIcon/ModdableIcon.tsx @@ -0,0 +1,41 @@ +import { validateProps } from 'botframework-webchat-api/internal'; +import classNames from 'classnames'; +import React, { useMemo, type CSSProperties } from 'react'; +import { object, optional, pipe, readonly, string, type InferInput } from 'valibot'; + +import useStyleSet from '../hooks/useStyleSet'; + +const moddableIconPropsSchema = pipe( + object({ + className: optional(string()), + color: optional(string()), + imageURL: optional(string()), + maskURL: optional(string()), + size: optional(string()) + }), + readonly() +); + +type ModdableIconProps = InferInput; + +function ModdableIcon(props: ModdableIconProps) { + const { className, color, imageURL, maskURL, size } = validateProps(moddableIconPropsSchema, props); + + const [{ moddableIcon: moddableIconClassName }] = useStyleSet(); + + const style = useMemo( + () => + ({ + '--webchat__moddable-icon--color': color, + '--webchat__moddable-icon--mask': maskURL && `url(${maskURL})`, + '--webchat__moddable-icon--image': imageURL && `url(${imageURL})`, + '--webchat__moddable-icon--size': size + }) satisfies Record<`--${string}`, number | string | undefined> as any, // csstype.CSSProperties does not allow CSS custom variables yet. + [color, imageURL, maskURL, size] + ); + + return
; +} + +export default ModdableIcon; +export { moddableIconPropsSchema, type ModdableIconProps }; diff --git a/packages/component/src/ModdableIcon/ModdableIconStyle.ts b/packages/component/src/ModdableIcon/ModdableIconStyle.ts new file mode 100644 index 0000000000..8ca83d2d46 --- /dev/null +++ b/packages/component/src/ModdableIcon/ModdableIconStyle.ts @@ -0,0 +1,25 @@ +import { type StyleSet } from '../Styles/StyleSet/types/StyleSet'; + +export default function createModdableIconStyle() { + return { + '&.webchat__moddable-icon': { + height: 'var(--webchat__moddable-icon--size, 1em)', + width: 'var(--webchat__moddable-icon--size, 1em)', + + // 1. Use the image as texture. + backgroundImage: 'var(--webchat__moddable-icon--image, none)', + backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + backgroundSize: 'var(--webchat__moddable-icon--size, 1em)', + + // 2. If image is not set, fallback to solid color. + backgroundColor: 'var(--webchat__moddable-icon--color, transparent)', + + // 3. Set the mask if any. + maskImage: 'var(--webchat__moddable-icon--mask)', // TODO: Need to think about 3P customization story. + maskPosition: 'center', + maskRepeat: 'no-repeat', + maskSize: 'var(--webchat__moddable-icon--size, 1em)' + } + } satisfies StyleSet; +} diff --git a/packages/component/src/SendBox/AttachmentBar/AttachmentBar.tsx b/packages/component/src/SendBox/AttachmentBar/AttachmentBar.tsx new file mode 100644 index 0000000000..cb0fa22db4 --- /dev/null +++ b/packages/component/src/SendBox/AttachmentBar/AttachmentBar.tsx @@ -0,0 +1,71 @@ +import { hooks } from 'botframework-webchat-api'; +import { validateProps } from 'botframework-webchat-api/internal'; +import classNames from 'classnames'; +import React, { memo, useCallback, useMemo } from 'react'; +import { useRefFrom } from 'use-ref-from'; +import { type InferInput, object, optional, pipe, readonly, string } from 'valibot'; + +import { useStyleSet } from '../../hooks'; +import testIds from '../../testIds'; +import AttachmentBarItem from './AttachmentBarItem'; + +const { useSendBoxAttachments, useStyleOptions } = hooks; + +const sendBoxAttachmentBarPropsSchema = pipe( + object({ + className: optional(string()) + }), + readonly() +); + +type SendBoxAttachmentBarProps = InferInput; + +function SendBoxAttachmentBar(props: SendBoxAttachmentBarProps) { + const { className } = validateProps(sendBoxAttachmentBarPropsSchema, props); + + const [sendBoxAttachments, setSendBoxAttachments] = useSendBoxAttachments(); + const [{ sendBoxAttachmentBar: sendBoxAttachmentBarClassName }] = useStyleSet(); + const [{ sendBoxAttachmentBarMaxThumbnail }] = useStyleOptions(); + + const mode = useMemo( + () => (sendBoxAttachments.length > sendBoxAttachmentBarMaxThumbnail ? 'list item' : 'thumbnail'), + [sendBoxAttachmentBarMaxThumbnail, sendBoxAttachments] + ); + + const sendBoxAttachmentsRef = useRefFrom(sendBoxAttachments); + + const handleAttachmentDelete = useCallback( + ({ attachment }) => + setSendBoxAttachments( + sendBoxAttachmentsRef.current.filter(sendBoxAttachment => sendBoxAttachment !== attachment) + ), + [setSendBoxAttachments, sendBoxAttachmentsRef] + ); + + return ( + sendBoxAttachments.length > 0 && ( +
+
+ {sendBoxAttachments.map((attachment, index) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} +
+
+ ) + ); +} + +export default memo(SendBoxAttachmentBar); +export { sendBoxAttachmentBarPropsSchema, type SendBoxAttachmentBarProps }; diff --git a/packages/component/src/SendBox/AttachmentBar/AttachmentBarItem.tsx b/packages/component/src/SendBox/AttachmentBar/AttachmentBarItem.tsx new file mode 100644 index 0000000000..ecad889396 --- /dev/null +++ b/packages/component/src/SendBox/AttachmentBar/AttachmentBarItem.tsx @@ -0,0 +1,107 @@ +import { hooks } from 'botframework-webchat-api'; +import { validateProps } from 'botframework-webchat-api/internal'; +import { type SendBoxAttachment } from 'botframework-webchat-core'; +import classNames from 'classnames'; +import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'; +import { useRefFrom } from 'use-ref-from'; +import { + custom, + function_, + instance, + object, + optional, + picklist, + pipe, + readonly, + safeParse, + union, + type InferInput +} from 'valibot'; + +import { useFocus, useStyleSet } from '../../hooks'; +import testIds from '../../testIds'; +import DeleteButton from './ItemDeleteButton'; +import Preview from './ItemPreview'; + +const { useLocalizer } = hooks; + +const sendBoxAttachmentBarItemPropsSchema = pipe( + object({ + attachment: pipe( + object({ + blob: union([instance(Blob), instance(File)]), + thumbnailURL: optional(instance(URL)) + }), + readonly() + ), + mode: picklist(['list item', 'thumbnail']), + onDelete: optional( + custom<(event: Readonly<{ attachment: SendBoxAttachment }>) => void>( + value => safeParse(function_(), value).success + ) + ) + }), + readonly() +); + +type SendBoxAttachmentBarItemProps = InferInput; + +function SendBoxAttachmentBarItem(props: SendBoxAttachmentBarItemProps) { + const { attachment, mode, onDelete } = validateProps(sendBoxAttachmentBarItemPropsSchema, props); + + const [{ sendBoxAttachmentBarItem: sendBoxAttachmentBarItemClassName }] = useStyleSet(); + const attachmentRef = useRefFrom(attachment); + const elementRef = useRef(null); + const focus = useFocus(); + const localize = useLocalizer(); + const onDeleteRef = useRefFrom(onDelete); + const shownRef = useRef(false); + + const attachmentName = useMemo( + () => + attachment.blob instanceof File + ? attachment.blob.name + : attachment.thumbnailURL + ? localize('SEND_BOX_ATTACHMENT_BAR_GENERIC_IMAGE_ALT') + : localize('SEND_BOX_ATTACHMENT_BAR_GENERIC_FILE_ALT'), + [attachment, localize] + ); + + const handleDeleteButtonClick = useCallback(() => { + onDeleteRef.current?.({ attachment: attachmentRef.current }); + + // After delete, focus back to the send box. + focus('sendBox'); + }, [attachmentRef, focus, onDeleteRef]); + + // If the item is newly added, scroll it into view. + useEffect(() => { + if (!shownRef.current) { + shownRef.current = true; + + elementRef.current?.scrollIntoView(); + } + }, [elementRef, shownRef]); + + return ( +
+ + +
+ ); +} + +export default memo(SendBoxAttachmentBarItem); +export { sendBoxAttachmentBarItemPropsSchema, type SendBoxAttachmentBarItemProps }; diff --git a/packages/component/src/SendBox/AttachmentBar/AttachmentBarItemStyle.ts b/packages/component/src/SendBox/AttachmentBar/AttachmentBarItemStyle.ts new file mode 100644 index 0000000000..a0c3dac1e2 --- /dev/null +++ b/packages/component/src/SendBox/AttachmentBar/AttachmentBarItemStyle.ts @@ -0,0 +1,143 @@ +import { StrictStyleOptions } from 'botframework-webchat-api'; +import { type StyleSet } from '../../Styles/StyleSet/types/StyleSet'; + +export default function createSendBoxAttachmentBarItemStyle(_: StrictStyleOptions) { + return { + /* #region List item */ + '&.webchat__send-box-attachment-bar-item': { + display: 'grid', + flexShrink: '0', + gridTemplateRows: 'auto', + + '&.webchat__send-box-attachment-bar-item--as-list-item': { + borderRadius: '4px', + boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12)', + gridTemplateAreas: '"body auxiliary"', + gridTemplateColumns: '1fr auto', + padding: '2px' + }, + + '&.webchat__send-box-attachment-bar-item--as-thumbnail': { + aspectRatio: '1/1', + border: 'solid 1px rgba(0, 0, 0, .25)', // Figma has border-width of 0.96px. + borderRadius: '8px', // Figma is 7.68px. + gridTemplateAreas: '"body"', + gridTemplateColumns: 'auto', + height: '80px', // <= 87px would fit white-label design with 3 thumbnails. + overflow: 'hidden' + } + }, + /* #endregion */ + + /* #region Delete button */ + '& .webchat__send-box-attachment-bar-item__delete-button': { + appearance: 'none', + borderRadius: '4px', // BorderRadiusXS is not defined in Fluent UI, guessing it is 4px. + display: 'grid', // Center the image + alignItems: 'center', + justifyContent: 'center', + gridArea: 'body', + justifySelf: 'end', + opacity: '1', + padding: '0', + transition: 'opacity 50ms', // Assume ultra-fast. + + '--webchat__moddable-icon--size': '19px', + + '&.webchat__send-box-attachment-bar-item__delete-button--large': { + '--webchat__moddable-icon--mask': `url(data:image/svg+xml;utf8,${encodeURIComponent('')})` + }, + + '&.webchat__send-box-attachment-bar-item__delete-button--small': { + '--webchat__moddable-icon--mask': `url(data:image/svg+xml;utf8,${encodeURIComponent('')})` + }, + + // https://react.fluentui.dev/?path=/docs/theme-colors--docs + '@media not (prefers-color-scheme: dark)': { + '--webchat__moddable-icon--color': '#242424', // Background/colorNeutralForeground1 + backgroundColor: 'White', // Background/colorNeutralBackground1 + borderColor: '#D1D1D1', // Stroke/colorNeutralStroke1 + + '&:hover': { + backgroundColor: '#F5F5F5', // Background/colorNeutralBackground1Hover + borderColor: '#C7C7C7' // Stroke/colorNeutralStroke1Hover + }, + + '&:active': { + backgroundColor: '#E0E0E0', // Background/colorNeutralBackground1Pressed + borderColor: '#C7C7C7' // Stroke/colorNeutralStroke1Pressed + }, + + '&:disabled, &[aria-diabled]': { + '--webchat__moddable-icon--color': '#BDBDBD', // Stroke/colorNeutralForegroundDisabled + backgroundColor: '#F0F0F0', // Background/colorNeutralBackgroundDisabled + borderColor: '#E0E0E0', // Stroke/colorNeutralStrokeDisabled + color: '#BDBDBD' // Stroke/colorNeutralForegroundDisabled + } + }, + + '@media (prefers-color-scheme: dark)': { + '--webchat__moddable-icon--color': '#FFFFFF', // Background/colorNeutralBackground1 + backgroundColor: '#292929', // Background/colorNeutralBackground1 + borderColor: '#666666', // Stroke/colorNeutralStroke1 + + '&:hover': { + backgroundColor: '#3D3D3D', // Background/colorNeutralBackground1Hover + borderColor: '#757575' // Stroke/colorNeutralStroke1Hover + }, + + '&:active': { + backgroundColor: '#1F1F1F', // Background/colorNeutralBackground1Pressed + borderColor: '#6B6B6B' // Stroke/colorNeutralStroke1Pressed + }, + + '&:disabled, &[aria-diabled]': { + '--webchat__moddable-icon--color': '#5C5C5C', // Stroke/colorNeutralForegroundDisabled + backgroundColor: '#141414', // Background/colorNeutralBackgroundDisabled + borderColor: '#424242', // Stroke/colorNeutralStrokeDisabled + color: '#5C5C5C' // Stroke/colorNeutralForegroundDisabled + } + } + }, + + '&.webchat__send-box-attachment-bar-item.webchat__send-box-attachment-bar-item--as-list-item .webchat__send-box-attachment-bar-item__delete-button': + { + border: '0', + gridArea: 'auxiliary', + height: '24px', + width: '24px' + }, + + '&.webchat__send-box-attachment-bar-item.webchat__send-box-attachment-bar-item--as-thumbnail .webchat__send-box-attachment-bar-item__delete-button': + { + borderStyle: 'solid', // Border color will be set elsewhere. + borderWidth: '1px', // Figma has border-width of 0.96px. + gridArea: 'body', + height: '23px', // Figma is 23.04px. + margin: '8px', // Figma is 7.68px. + width: '23px' // Figma is 23.04px. + }, + + '@media not (prefers-reduced-motion: reduce)': { + '&.webchat__send-box-attachment-bar-item.webchat__send-box-attachment-bar-item--as-thumbnail:not(:hover):not(:focus-within) .webchat__send-box-attachment-bar-item__delete-button': + { + opacity: '0' + } + }, + /* #endregion */ + + /* #region Preview */ + '& .webchat__send-box-attachment-bar-item__preview': { + alignItems: 'center', + display: 'grid', + gridArea: 'body', + overflow: 'hidden' + }, + + '&.webchat__send-box-attachment-bar-item.webchat__send-box-attachment-bar-item--as-list-item .webchat__send-box-attachment-bar-item__preview': + { + paddingInline: '8px' + } + /* #endregion */ + } satisfies StyleSet; +} diff --git a/packages/component/src/SendBox/AttachmentBar/AttachmentBarStyle.ts b/packages/component/src/SendBox/AttachmentBar/AttachmentBarStyle.ts new file mode 100644 index 0000000000..247f9fe316 --- /dev/null +++ b/packages/component/src/SendBox/AttachmentBar/AttachmentBarStyle.ts @@ -0,0 +1,39 @@ +import { StrictStyleOptions } from 'botframework-webchat-api'; +import { type StyleSet } from '../../Styles/StyleSet/types/StyleSet'; + +export default function createSendBoxAttachmentBarStyle({ sendBoxAttachmentBarMaxHeight }: StrictStyleOptions) { + return { + '&.webchat__send-box-attachment-bar': { + gridArea: 'attachment-bar', + + '&.webchat__send-box-attachment-bar--as-list-item': { + maxHeight: `${sendBoxAttachmentBarMaxHeight}px`, + overflowY: 'auto', + scrollbarGutter: 'stable', + scrollbarWidth: 'thin' + }, + + '&.webchat__send-box-attachment-bar--as-thumbnail': { + overflowX: 'auto' + }, + + '& .webchat__send-box-attachment-bar__box': { + gap: '4px' + }, + + '&.webchat__send-box-attachment-bar--as-list-item .webchat__send-box-attachment-bar__box': { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + + '&:not(:empty)': { + padding: '4px' + } + }, + + '&.webchat__send-box-attachment-bar--as-thumbnail .webchat__send-box-attachment-bar__box': { + display: 'flex', + scrollbarWidth: 'thin' + } + } + } satisfies StyleSet; +} diff --git a/packages/component/src/SendBox/AttachmentBar/ItemDeleteButton.tsx b/packages/component/src/SendBox/AttachmentBar/ItemDeleteButton.tsx new file mode 100644 index 0000000000..0c74ebb53a --- /dev/null +++ b/packages/component/src/SendBox/AttachmentBar/ItemDeleteButton.tsx @@ -0,0 +1,60 @@ +import { hooks } from 'botframework-webchat-api'; +import { validateProps } from 'botframework-webchat-api/internal'; +import classNames from 'classnames'; +import React, { KeyboardEventHandler, useCallback } from 'react'; +import { function_, object, optional, picklist, pipe, readonly, string, type InferInput } from 'valibot'; + +import { useFocus } from '../../hooks'; +import ModdableIcon from '../../ModdableIcon/ModdableIcon'; +import testIds from '../../testIds'; + +const { useLocalizer } = hooks; + +const attachmentDeleteButtonPropsSchema = pipe( + object({ + attachmentName: string(), + onClick: optional(function_()), + size: optional(picklist(['large', 'small'])) + }), + readonly() +); + +type AttachmentDeleteButtonProps = InferInput; + +function AttachmentDeleteButton(props: AttachmentDeleteButtonProps) { + const { attachmentName, onClick, size } = validateProps(attachmentDeleteButtonPropsSchema, props); + + const focus = useFocus(); + const localize = useLocalizer(); + + const handleKeyDown = useCallback>( + event => { + if (event.key === 'Escape') { + event.preventDefault(); + + focus('sendBox'); + } + }, + [focus] + ); + + return ( + + ); +} + +export default AttachmentDeleteButton; +export { attachmentDeleteButtonPropsSchema, type AttachmentDeleteButtonProps }; diff --git a/packages/component/src/SendBox/AttachmentBar/ItemPreview.tsx b/packages/component/src/SendBox/AttachmentBar/ItemPreview.tsx new file mode 100644 index 0000000000..e2cf6c9c6c --- /dev/null +++ b/packages/component/src/SendBox/AttachmentBar/ItemPreview.tsx @@ -0,0 +1,49 @@ +import { validateProps } from 'botframework-webchat-api/internal'; +import { type SendBoxAttachment } from 'botframework-webchat-core'; +import React, { memo } from 'react'; +import { object, picklist, pipe, readonly, string, type InferInput } from 'valibot'; + +import FilePreview from './Preview/FilePreview'; +import ImagePreview from './Preview/ImagePreview'; +import { sendBoxAttachmentSchema } from './Preview/sendBoxAttachment'; + +const sendBoxAttachmentBarItemPreviewPropsSchema = pipe( + object({ + attachment: sendBoxAttachmentSchema, + attachmentName: string(), + mode: picklist(['list item', 'thumbnail']) + }), + readonly() +); + +type SendBoxAttachmentBarItemPreviewProps = InferInput; + +// TODO: Turn this into middleware. +function SendBoxAttachmentBarItemPreview(props: SendBoxAttachmentBarItemPreviewProps) { + const { attachment, attachmentName, mode } = validateProps(sendBoxAttachmentBarItemPreviewPropsSchema, props); + + let element: React.ReactNode; + + if (attachment.thumbnailURL) { + element = ( + + ); + } else { + element = ( + + ); + } + + return
{element}
; +} + +export default memo(SendBoxAttachmentBarItemPreview); +export { sendBoxAttachmentBarItemPreviewPropsSchema, type SendBoxAttachmentBarItemPreviewProps }; diff --git a/packages/component/src/SendBox/AttachmentBar/Preview/FilePreview.tsx b/packages/component/src/SendBox/AttachmentBar/Preview/FilePreview.tsx new file mode 100644 index 0000000000..f42f221665 --- /dev/null +++ b/packages/component/src/SendBox/AttachmentBar/Preview/FilePreview.tsx @@ -0,0 +1,64 @@ +import { validateProps } from 'botframework-webchat-api/internal'; +import classNames from 'classnames'; +import React, { memo } from 'react'; +import { object, picklist, pipe, readonly, string, type InferInput } from 'valibot'; + +import { useStyleSet } from '../../../hooks'; +import ModdableIcon from '../../../ModdableIcon/ModdableIcon'; +import { sendBoxAttachmentSchema } from './sendBoxAttachment'; + +const sendBoxAttachmentBarItemFileAttachmentPreviewPropsSchema = pipe( + object({ + attachment: sendBoxAttachmentSchema, + attachmentName: string(), + mode: picklist(['list item', 'thumbnail']) + }), + readonly() +); + +type SendBoxAttachmentBarItemFileAttachmentPreviewProps = InferInput< + typeof sendBoxAttachmentBarItemFileAttachmentPreviewPropsSchema +>; + +function SendBoxAttachmentBarItemFileAttachmentPreview(props: SendBoxAttachmentBarItemFileAttachmentPreviewProps) { + const { attachment, mode, attachmentName } = validateProps( + sendBoxAttachmentBarItemFileAttachmentPreviewPropsSchema, + props + ); + + const [{ sendBoxAttachmentBarItemFilePreview: sendBoxAttachmentBarItemFilePreviewClassName }] = useStyleSet(); + + return mode === 'list item' ? ( +
+ +
{attachmentName}
+
+ ) : ( +
+ +
+ ); +} + +export default memo(SendBoxAttachmentBarItemFileAttachmentPreview); +export { + sendBoxAttachmentBarItemFileAttachmentPreviewPropsSchema, + type SendBoxAttachmentBarItemFileAttachmentPreviewProps +}; diff --git a/packages/component/src/SendBox/AttachmentBar/Preview/FilePreviewStyle.ts b/packages/component/src/SendBox/AttachmentBar/Preview/FilePreviewStyle.ts new file mode 100644 index 0000000000..cefe374c8e --- /dev/null +++ b/packages/component/src/SendBox/AttachmentBar/Preview/FilePreviewStyle.ts @@ -0,0 +1,100 @@ +import { StrictStyleOptions } from 'botframework-webchat-api'; +import { type StyleSet } from '../../../Styles/StyleSet/types/StyleSet'; + +export default function createSendBoxAttachmentBarItemFilePreviewStyle({ primaryFont }: StrictStyleOptions) { + return { + '&.webchat__send-box-attachment-bar-item-file-preview': { + alignItems: 'center', + display: 'grid', + + '&.webchat__send-box-attachment-bar-item-file-preview--as-list-item': { + fontFamily: primaryFont, + gap: '8px', + gridTemplateColumns: 'auto 1fr' + }, + + '&.webchat__send-box-attachment-bar-item-file-preview--as-thumbnail': { + height: '100%', + justifyContent: 'center', + width: '100%' + }, + + '&.webchat__send-box-attachment-bar-item-file-preview--is-file.webchat__send-box-attachment-bar-item-file-preview--as-thumbnail': + { + '--webchat__moddable-icon--image': `url(data:image/svg+xml;utf8,${encodeURIComponent(` + + + + `)})`, + '--webchat__moddable-icon--size': '36px' + }, + + '&.webchat__send-box-attachment-bar-item-file-preview--is-file.webchat__send-box-attachment-bar-item-file-preview--as-list-item': + { + '--webchat__moddable-icon--image': `url(data:image/svg+xml;utf8,${encodeURIComponent(` + + + + `)})`, + '--webchat__moddable-icon--size': '16px' + }, + + '&.webchat__send-box-attachment-bar-item-file-preview--is-image': { + '--webchat__moddable-icon--image': `url(data:image/svg+xml;utf8,${encodeURIComponent(` + + + + + `)})`, + '--webchat__moddable-icon--size': '16px' + }, + + '& .webchat__send-box-attachment-bar-item-file-preview__text': { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' + } + } + } satisfies StyleSet; +} diff --git a/packages/component/src/SendBox/AttachmentBar/Preview/ImagePreview.tsx b/packages/component/src/SendBox/AttachmentBar/Preview/ImagePreview.tsx new file mode 100644 index 0000000000..28ee63045c --- /dev/null +++ b/packages/component/src/SendBox/AttachmentBar/Preview/ImagePreview.tsx @@ -0,0 +1,49 @@ +import { validateProps } from 'botframework-webchat-api/internal'; +import classNames from 'classnames'; +import React, { memo } from 'react'; +import { object, picklist, pipe, readonly, string, type InferInput } from 'valibot'; + +import { useStyleSet } from '../../../hooks'; +import FilePreview from './FilePreview'; +import { sendBoxAttachmentSchema } from './sendBoxAttachment'; + +const sendBoxAttachmentBarItemImageAttachmentPreviewPropsSchema = pipe( + object({ + attachment: sendBoxAttachmentSchema, + attachmentName: string(), + mode: picklist(['list item', 'thumbnail']) + }), + readonly() +); + +type SendBoxAttachmentBarItemImageAttachmentPreviewProps = InferInput< + typeof sendBoxAttachmentBarItemImageAttachmentPreviewPropsSchema +>; + +function SendBoxAttachmentBarItemImageAttachmentPreview(props: SendBoxAttachmentBarItemImageAttachmentPreviewProps) { + const { attachment, mode, attachmentName } = validateProps( + sendBoxAttachmentBarItemImageAttachmentPreviewPropsSchema, + props + ); + + const [{ sendBoxAttachmentBarItemImagePreview: sendBoxAttachmentBarItemImagePreviewClassName }] = useStyleSet(); + + return mode === 'list item' ? ( + + ) : ( + {attachmentName} + ); +} + +export default memo(SendBoxAttachmentBarItemImageAttachmentPreview); +export { + sendBoxAttachmentBarItemImageAttachmentPreviewPropsSchema, + type SendBoxAttachmentBarItemImageAttachmentPreviewProps +}; diff --git a/packages/component/src/SendBox/AttachmentBar/Preview/ImagePreviewStyle.ts b/packages/component/src/SendBox/AttachmentBar/Preview/ImagePreviewStyle.ts new file mode 100644 index 0000000000..e8335160a7 --- /dev/null +++ b/packages/component/src/SendBox/AttachmentBar/Preview/ImagePreviewStyle.ts @@ -0,0 +1,12 @@ +import { StrictStyleOptions } from 'botframework-webchat-api'; +import { type StyleSet } from '../../../Styles/StyleSet/types/StyleSet'; + +export default function createSendBoxAttachmentBarItemImagePreviewStyle(_: StrictStyleOptions) { + return { + '&.webchat__send-box-attachment-bar-item-image-preview': { + height: '100%', + objectFit: 'cover', + width: '100%' + } + } satisfies StyleSet; +} diff --git a/packages/component/src/SendBox/AttachmentBar/Preview/sendBoxAttachment.ts b/packages/component/src/SendBox/AttachmentBar/Preview/sendBoxAttachment.ts new file mode 100644 index 0000000000..f8211443da --- /dev/null +++ b/packages/component/src/SendBox/AttachmentBar/Preview/sendBoxAttachment.ts @@ -0,0 +1,13 @@ +import { type InferInput, instance, object, optional, pipe, readonly, union } from 'valibot'; + +const sendBoxAttachmentSchema = pipe( + object({ + blob: union([instance(Blob), instance(File)]), + thumbnailURL: optional(instance(URL)) + }), + readonly() +); + +type SendBoxAttachment = InferInput; + +export { sendBoxAttachmentSchema, type SendBoxAttachment }; diff --git a/packages/component/src/SendBox/AttachmentBar/index.ts b/packages/component/src/SendBox/AttachmentBar/index.ts new file mode 100644 index 0000000000..4c44ffaea3 --- /dev/null +++ b/packages/component/src/SendBox/AttachmentBar/index.ts @@ -0,0 +1,3 @@ +import AttachmentBar from './AttachmentBar'; + +export { AttachmentBar }; diff --git a/packages/component/src/SendBox/BasicSendBox.tsx b/packages/component/src/SendBox/BasicSendBox.tsx index b4621fcee5..3353a2f22f 100644 --- a/packages/component/src/SendBox/BasicSendBox.tsx +++ b/packages/component/src/SendBox/BasicSendBox.tsx @@ -9,6 +9,7 @@ import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject' import useStyleSet from '../hooks/useStyleSet'; import useWebSpeechPonyfill from '../hooks/useWebSpeechPonyfill'; import useErrorMessageId from '../providers/internal/SendBox/useErrorMessageId'; +import { AttachmentBar } from './AttachmentBar/index'; import DictationInterims from './DictationInterims'; import MicrophoneButton from './MicrophoneButton'; import SendButton from './SendButton'; @@ -25,7 +26,6 @@ const ROOT_STYLE = { '&.webchat__send-box': { '& .webchat__send-box__button': { flexShrink: 0 }, '& .webchat__send-box__dictation-interims': { flex: 10000 }, - '& .webchat__send-box__main': { display: 'flex' }, '& .webchat__send-box__microphone-button': { flex: 1 }, '& .webchat__send-box__text-box': { flex: 10000 } } @@ -78,12 +78,15 @@ function BasicSendBox(props: BasicSendBoxProps) { >
+ - {speechInterimsVisible ? ( - - ) : ( - - )} +
+ {speechInterimsVisible ? ( + + ) : ( + + )} +
{supportSpeechRecognition ? ( ) : ( diff --git a/packages/component/src/SendBoxToolbar/UploadButton.tsx b/packages/component/src/SendBoxToolbar/UploadButton.tsx index ed7ce4434f..c2dcff2dcf 100644 --- a/packages/component/src/SendBoxToolbar/UploadButton.tsx +++ b/packages/component/src/SendBoxToolbar/UploadButton.tsx @@ -1,7 +1,8 @@ import { hooks } from 'botframework-webchat-api'; import { validateProps } from 'botframework-webchat-api/internal'; import classNames from 'classnames'; -import React, { memo, useCallback, useRef, type FormEventHandler, type MouseEventHandler } from 'react'; +import random from 'math-random'; +import React, { memo, useCallback, useRef, useState, type FormEventHandler, type MouseEventHandler } from 'react'; import { useRefFrom } from 'use-ref-from'; import { object, pipe, readonly, string, type InferInput } from 'valibot'; @@ -48,6 +49,7 @@ function UploadButton(props: UploadButtonProps) { const [{ sendAttachmentOn, uploadAccept, uploadMultiple }] = useStyleOptions(); const [{ uploadButton: uploadButtonStyleSet }] = useStyleSet(); + const [inputKey, setInputKey] = useState(0); const [sendBoxAttachments, setSendBoxAttachments] = useSendBoxAttachments(); const [uiState] = useUIState(); const focus = useFocus(); @@ -59,6 +61,7 @@ function UploadButton(props: UploadButtonProps) { const disabled = uiState === 'disabled'; const sendAttachmentOnRef = useRefFrom(sendAttachmentOn); + const sendBoxAttachmentsRef = useRefFrom(sendBoxAttachments); const uploadFileString = localize('TEXT_INPUT_UPLOAD_BUTTON_ALT'); const handleClick = useCallback>(() => inputRef.current?.click(), [inputRef]); @@ -72,17 +75,30 @@ function UploadButton(props: UploadButtonProps) { // Otherwise, if the user click the send button too quickly, it will not attach any files. (async function () { setSendBoxAttachments( - Object.freeze( - await Promise.all( - [...currentTarget.files].map(blob => makeThumbnail(blob).then(thumbnailURL => ({ blob, thumbnailURL }))) - ) - ) + Object.freeze([ + ...sendBoxAttachmentsRef.current, + ...(await Promise.all( + Array.from(currentTarget.files).map(async (blob: File) => { + const entry = sendBoxAttachmentsRef.current.find(entry => entry.blob === blob); + + if (entry) { + return entry; + } + + const thumbnailURL = await makeThumbnail(blob); + + return { blob, thumbnailURL }; + }) + )) + ]) ); + setInputKey(random()); + sendAttachmentOnRef.current === 'attach' && submit(); })(); }, - [focus, makeThumbnail, sendAttachmentOnRef, setSendBoxAttachments, submit] + [focus, makeThumbnail, sendBoxAttachmentsRef, sendAttachmentOnRef, setInputKey, setSendBoxAttachments, submit] ); return ( @@ -92,6 +108,9 @@ function UploadButton(props: UploadButtonProps) { aria-disabled={disabled} aria-hidden="true" className="webchat__upload-button--file-input" + // Recreates the element after every upload to prevent issues in WebDriver. + // Otherwise, on second upload, WebDriver will resend files from first upload as new Blob/File instance and it will cause duplicates. + key={inputKey} multiple={uploadMultiple} onChange={disabled ? undefined : handleFileChange} onClick={disabled ? PREVENT_DEFAULT_HANDLER : undefined} diff --git a/packages/component/src/Styles/StyleSet/SendBox.ts b/packages/component/src/Styles/StyleSet/SendBox.ts index 76ea01c0fc..6ee37fe271 100644 --- a/packages/component/src/Styles/StyleSet/SendBox.ts +++ b/packages/component/src/Styles/StyleSet/SendBox.ts @@ -1,6 +1,16 @@ -import { StrictStyleOptions } from 'botframework-webchat-api'; +import { type StrictStyleOptions } from 'botframework-webchat-api'; +import { type StyleSet } from './types/StyleSet'; + +function stringifyNumericPixel(value: number | string): string { + if (typeof value === 'number') { + return `${value}px`; + } + + return value; +} export default function createSendBoxStyle({ + paddingRegular, sendBoxBackground, sendBoxBorderBottom, sendBoxBorderLeft, @@ -17,12 +27,40 @@ export default function createSendBoxStyle({ '& .webchat__send-box__main': { alignItems: 'stretch', backgroundColor: sendBoxBackground, - borderBottom: sendBoxBorderBottom, - borderLeft: sendBoxBorderLeft, - borderRight: sendBoxBorderRight, - borderTop: sendBoxBorderTop, - minHeight: sendBoxHeight + borderBottom: stringifyNumericPixel(sendBoxBorderBottom), + borderLeft: stringifyNumericPixel(sendBoxBorderLeft), + borderRight: stringifyNumericPixel(sendBoxBorderRight), + borderTop: stringifyNumericPixel(sendBoxBorderTop), + display: 'grid', + gridTemplateAreas: '"upload-button text-box send-button"', + gridTemplateColumns: 'auto 1fr auto', + gridTemplateRows: 'auto', + minHeight: stringifyNumericPixel(sendBoxHeight), + + // For unknown reason, if the attachment bar does not exist, the second row is still occupying about 1px, we need to hide it. + '&:has(.webchat__send-box__attachment-bar)': { + gridTemplateAreas: '"attachment-bar attachment-bar attachment-bar" "upload-button text-box send-button"', + gridTemplateRows: 'auto auto' + } + }, + + '& .webchat__send-box__editable': { + display: 'flex', + flexDirection: 'column', + // eslint-disable-next-line no-magic-numbers + gap: `${paddingRegular / 2}px`, + // eslint-disable-next-line no-magic-numbers + paddingBottom: `${paddingRegular / 2}px`, + // eslint-disable-next-line no-magic-numbers + paddingTop: `${paddingRegular / 2}px`, + overflowX: 'hidden', + width: '100%' + }, + + '& .webchat__send-box__attachment-bar': { + padding: `${paddingRegular}px`, + paddingBlockEnd: `0px` } } - }; + } satisfies StyleSet; } diff --git a/packages/component/src/Styles/StyleSet/SendBoxTextBox.ts b/packages/component/src/Styles/StyleSet/SendBoxTextBox.ts index f30df4734b..1a8998a20f 100644 --- a/packages/component/src/Styles/StyleSet/SendBoxTextBox.ts +++ b/packages/component/src/Styles/StyleSet/SendBoxTextBox.ts @@ -13,7 +13,12 @@ export default function createSendBoxTextBoxStyle({ '&.webchat__send-box-text-box': { alignItems: 'center', fontFamily: primaryFont, - padding: paddingRegular, + // eslint-disable-next-line no-magic-numbers + paddingBottom: paddingRegular / 2, + paddingLeft: paddingRegular, + paddingRight: paddingRegular, + // eslint-disable-next-line no-magic-numbers + paddingTop: paddingRegular / 2, position: 'relative', '& .webchat__send-box-text-box__input': { diff --git a/packages/component/src/Styles/StyleSet/types/StyleSet.ts b/packages/component/src/Styles/StyleSet/types/StyleSet.ts new file mode 100644 index 0000000000..12765263e4 --- /dev/null +++ b/packages/component/src/Styles/StyleSet/types/StyleSet.ts @@ -0,0 +1,11 @@ +type ContainerQuery = `@container ${string}`; +type MediaQuery = `@media ${string}`; +type SelfQuery = `&${string}`; +type CustomVariable = `--${string}`; + +type StyleSet = { + [key: ContainerQuery | MediaQuery | SelfQuery]: StyleSet; + [key: CustomVariable]: string; +} & Partial; + +export { type ContainerQuery, type CustomVariable, type MediaQuery, type SelfQuery, type StyleSet }; diff --git a/packages/component/src/Styles/createStyleSet.ts b/packages/component/src/Styles/createStyleSet.ts index 1304749fa7..8bbf960d6f 100644 --- a/packages/component/src/Styles/createStyleSet.ts +++ b/packages/component/src/Styles/createStyleSet.ts @@ -1,5 +1,9 @@ import { normalizeStyleOptions, StyleOptions } from 'botframework-webchat-api'; +import createSendBoxAttachmentBarItemStyle from '../SendBox/AttachmentBar/AttachmentBarItemStyle'; +import createSendBoxAttachmentBarStyle from '../SendBox/AttachmentBar/AttachmentBarStyle'; +import createSendBoxAttachmentBarItemFilePreviewStyle from '../SendBox/AttachmentBar/Preview/FilePreviewStyle'; +import createSendBoxAttachmentBarItemImagePreviewStyle from '../SendBox/AttachmentBar/Preview/ImagePreviewStyle'; import createActivitiesStyle from './StyleSet/Activities'; import createActivityButtonStyle from './StyleSet/ActivityButton'; import createActivityCopyButtonStyle from './StyleSet/ActivityCopyButton'; @@ -14,8 +18,8 @@ import createCarouselFilmStripAttachment from './StyleSet/CarouselFilmStripAttac import createCarouselFlipper from './StyleSet/CarouselFlipper'; import createChatHistoryBoxStyleSet from './StyleSet/ChatHistoryBox'; import createCitationModalDialogStyle from './StyleSet/CitationModalDialog'; -import createCodeBlockCopyButtonStyle from './StyleSet/CodeBlockCopyButton'; import createCodeBlockStyle from './StyleSet/CodeBlock'; +import createCodeBlockCopyButtonStyle from './StyleSet/CodeBlockCopyButton'; import createConnectivityNotification from './StyleSet/ConnectivityNotification'; import createDictationInterimsStyle from './StyleSet/DictationInterims'; import createErrorBoxStyle from './StyleSet/ErrorBox'; @@ -27,22 +31,23 @@ import createInitialsAvatarStyle from './StyleSet/InitialsAvatar'; import createLinkDefinitionsStyle from './StyleSet/LinkDefinitions'; import createMicrophoneButtonStyle from './StyleSet/MicrophoneButton'; import createModalDialogStyle from './StyleSet/ModalDialog'; +import createModdableIconStyle from '../ModdableIcon/ModdableIconStyle'; import createMonochromeImageMaskerStyleSet from './StyleSet/MonochromeImageMasker'; import createRenderMarkdownStyle from './StyleSet/RenderMarkdown'; import createRootStyle from './StyleSet/Root'; import createScrollToEndButtonStyle from './StyleSet/ScrollToEndButton'; -import createSendBoxButtonStyle from './StyleSet/SendBoxButton'; import createSendBoxStyle from './StyleSet/SendBox'; +import createSendBoxButtonStyle from './StyleSet/SendBoxButton'; import createSendBoxTextBoxStyle from './StyleSet/SendBoxTextBox'; import createSendStatusStyle from './StyleSet/SendStatus'; import createSpinnerAnimationStyle from './StyleSet/SpinnerAnimation'; import createStackedLayoutStyle from './StyleSet/StackedLayout'; -import createSuggestedActionsStyle from './StyleSet/SuggestedActions'; import createSuggestedActionStyle from './StyleSet/SuggestedAction'; +import createSuggestedActionsStyle from './StyleSet/SuggestedActions'; import createTextContentStyle from './StyleSet/TextContent'; import createThumbButtonStyle from './StyleSet/ThumbButton'; -import createToasterStyle from './StyleSet/Toaster'; import createToastStyle from './StyleSet/Toast'; +import createToasterStyle from './StyleSet/Toaster'; import createTooltipStyle from './StyleSet/Tooltip'; import createTypingAnimationStyle from './StyleSet/TypingAnimation'; import createTypingIndicatorStyle from './StyleSet/TypingIndicator'; @@ -85,11 +90,16 @@ export default function createStyleSet(styleOptions: StyleOptions) { imageAvatar: createImageAvatarStyle(), initialsAvatar: createInitialsAvatarStyle(strictStyleOptions), microphoneButton: createMicrophoneButtonStyle(strictStyleOptions), + moddableIcon: createModdableIconStyle(), monochromeImageMasker: createMonochromeImageMaskerStyleSet(), options: { ...strictStyleOptions }, // Cloned to make sure no additional modifications will propagate up. root: createRootStyle(strictStyleOptions), scrollToEndButton: createScrollToEndButtonStyle(strictStyleOptions), sendBox: createSendBoxStyle(strictStyleOptions), + sendBoxAttachmentBar: createSendBoxAttachmentBarStyle(strictStyleOptions), + sendBoxAttachmentBarItem: createSendBoxAttachmentBarItemStyle(strictStyleOptions), + sendBoxAttachmentBarItemFilePreview: createSendBoxAttachmentBarItemFilePreviewStyle(strictStyleOptions), + sendBoxAttachmentBarItemImagePreview: createSendBoxAttachmentBarItemImagePreviewStyle(strictStyleOptions), sendBoxButton: createSendBoxButtonStyle(strictStyleOptions), sendBoxTextBox: createSendBoxTextBoxStyle(strictStyleOptions), spinnerAnimation: createSpinnerAnimationStyle(strictStyleOptions), diff --git a/packages/component/src/Utils/MonochromeImageMasker.tsx b/packages/component/src/Utils/MonochromeImageMasker.tsx index eac4c64902..2f89565571 100644 --- a/packages/component/src/Utils/MonochromeImageMasker.tsx +++ b/packages/component/src/Utils/MonochromeImageMasker.tsx @@ -1,10 +1,23 @@ +import { validateProps } from 'botframework-webchat-api/internal'; import classNames from 'classnames'; import React, { memo, useMemo, type CSSProperties } from 'react'; +import { object, optional, pipe, readonly, string, type InferInput } from 'valibot'; + import { useStyleSet } from '../hooks'; -type Props = Readonly<{ className?: string | undefined; src: string }>; +const monochromeImageMaskerPropsSchema = pipe( + object({ + className: optional(string()), + src: string() + }), + readonly() +); + +type MonochromeImageMaskerProps = InferInput; + +function MonochromeImageMasker(props: MonochromeImageMaskerProps) { + const { className, src } = validateProps(monochromeImageMaskerPropsSchema, props); -const MonochromeImageMasker = ({ className, src }: Props) => { const [{ monochromeImageMasker }] = useStyleSet(); const style = useMemo( () => ({ '--webchat__monochrome-image-masker__mask-image': `url(${src})` }) as CSSProperties, @@ -14,8 +27,7 @@ const MonochromeImageMasker = ({ className, src }: Props) => { return (
); -}; - -MonochromeImageMasker.displayName = 'MonochromeImageMasker'; +} export default memo(MonochromeImageMasker); +export { monochromeImageMaskerPropsSchema, type MonochromeImageMaskerProps }; diff --git a/packages/component/src/testIds.ts b/packages/component/src/testIds.ts index c79d4f23e6..2fb1f84595 100644 --- a/packages/component/src/testIds.ts +++ b/packages/component/src/testIds.ts @@ -1,6 +1,9 @@ const testIds = { codeBlockCopyButton: 'code block copy button', copyButton: 'copy button', + sendBoxAttachmentBar: 'send box attachment bar', + sendBoxAttachmentBarItem: 'send box attachment bar item', + sendBoxAttachmentBarItemDeleteButton: 'send box attachment bar item delete button', sendBoxSpeechBox: 'send box speech box', sendBoxTextBox: 'send box text area', typingIndicator: 'typing indicator', diff --git a/packages/test/page-object/src/globals/pageElements/sendBoxTextBox.js b/packages/test/page-object/src/globals/pageElements/sendBoxTextBox.js index d439d02a12..0508f17a79 100644 --- a/packages/test/page-object/src/globals/pageElements/sendBoxTextBox.js +++ b/packages/test/page-object/src/globals/pageElements/sendBoxTextBox.js @@ -1,6 +1,8 @@ +import { testIds } from 'botframework-webchat'; + import root from './root'; -const CSS_SELECTOR = '[role="form"] > * > form > input[type="text"], [role="form"] > * > form textarea'; +const CSS_SELECTOR = `[data-testid="${testIds.sendBoxTextBox}"]`; export default function sendBoxTextBox() { return root().querySelector(CSS_SELECTOR);