diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6b6f54327..8c635abdff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -74,6 +74,10 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
- Added support for math blocks using `$$` delimiter alongside existing `\[...\]` and `\(...\)` notations, in PR [#5381](https://github.com/microsoft/BotFramework-WebChat/pull/5381), by [@OEvgeny](https://github.com/OEvgeny)
- Added support for speech recognition initial silence timeout when using Azure Speech, in PR [#5400](https://github.com/microsoft/BotFramework/WebChat/pull/5400), by [@compulim](https://github.com/compulim)
- Introduced syntax highlighting for markdown code blocks, in PR [#5389](https://github.com/microsoft/BotFramework-WebChat/pull/5389), by [@OEvgeny](https://github.com/OEvgeny)
+- (Experimental) Added `feedbackActionsPlacement` style option to control feedback button placement, in PR [#5407](https://github.com/microsoft/BotFramework-WebChat/pull/5407), by [@OEvgeny](https://github.com/OEvgeny)
+ - New style option supports two values: `'activity-actions'` and `'activity-status'` (default)
+ - When set to `'activity-actions'`, feedback buttons are displayed in the activity actions toolbar
+ - When set to `'activity-status'`, feedback buttons appear in the activity status area (default behavior)
### Changed
diff --git a/__tests__/__image_snapshots__/html/feedback-activity-status-click-js-vote-button-should-send-event-on-click-2-snap.png b/__tests__/__image_snapshots__/html/feedback-activity-status-click-js-vote-button-should-send-event-on-click-2-snap.png
index 64711e99cc..1c8de0f423 100644
Binary files a/__tests__/__image_snapshots__/html/feedback-activity-status-click-js-vote-button-should-send-event-on-click-2-snap.png and b/__tests__/__image_snapshots__/html/feedback-activity-status-click-js-vote-button-should-send-event-on-click-2-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/feedback-activity-status-click-js-vote-button-should-send-event-on-click-3-snap.png b/__tests__/__image_snapshots__/html/feedback-activity-status-click-js-vote-button-should-send-event-on-click-3-snap.png
index eb6591e071..c5724814f6 100644
Binary files a/__tests__/__image_snapshots__/html/feedback-activity-status-click-js-vote-button-should-send-event-on-click-3-snap.png and b/__tests__/__image_snapshots__/html/feedback-activity-status-click-js-vote-button-should-send-event-on-click-3-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/feedback-activity-status-click-js-vote-button-should-send-event-on-click-4-snap.png b/__tests__/__image_snapshots__/html/feedback-activity-status-click-js-vote-button-should-send-event-on-click-4-snap.png
index eb6591e071..c5724814f6 100644
Binary files a/__tests__/__image_snapshots__/html/feedback-activity-status-click-js-vote-button-should-send-event-on-click-4-snap.png and b/__tests__/__image_snapshots__/html/feedback-activity-status-click-js-vote-button-should-send-event-on-click-4-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-1-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-1-snap.png
new file mode 100644
index 0000000000..918da9396e
Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-1-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-2-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-2-snap.png
new file mode 100644
index 0000000000..e88fe9dc37
Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-2-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-3-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-3-snap.png
new file mode 100644
index 0000000000..f17e1e3b2f
Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-3-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-4-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-4-snap.png
new file mode 100644
index 0000000000..b8447f2486
Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-4-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-5-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-5-snap.png
new file mode 100644
index 0000000000..d6c51cfbfc
Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-5-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-6-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-6-snap.png
new file mode 100644
index 0000000000..98124c8a77
Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-6-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-7-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-7-snap.png
new file mode 100644
index 0000000000..9a9ac2906a
Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-feedback-7-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-1-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-1-snap.png
new file mode 100644
index 0000000000..cceaa5a996
Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-1-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-2-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-2-snap.png
new file mode 100644
index 0000000000..1bc5f7ce9d
Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-2-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-3-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-3-snap.png
new file mode 100644
index 0000000000..3aae5669d6
Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-3-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-4-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-4-snap.png
new file mode 100644
index 0000000000..705453c4a8
Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-4-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-5-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-5-snap.png
new file mode 100644
index 0000000000..0f51225a2d
Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-5-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-6-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-6-snap.png
new file mode 100644
index 0000000000..3594bb2fd6
Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-6-snap.png differ
diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-7-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-7-snap.png
new file mode 100644
index 0000000000..31b8ab3386
Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-feedback-7-snap.png differ
diff --git a/__tests__/html/assets/index.css b/__tests__/html/assets/index.css
index 34f79e85c7..732b5b4f76 100644
--- a/__tests__/html/assets/index.css
+++ b/__tests__/html/assets/index.css
@@ -21,6 +21,10 @@ body {
width: 360px;
}
+.webchat__tooltip {
+ transition-duration: 0.01ms !important;
+}
+
body.jest input,
body.jest textarea,
body.jest [contenteditable] {
diff --git a/__tests__/html/fluentTheme/side-by-side.wide.dark.html b/__tests__/html/fluentTheme/side-by-side.wide.dark.html
index 77e8176886..a703fe383c 100644
--- a/__tests__/html/fluentTheme/side-by-side.wide.dark.html
+++ b/__tests__/html/fluentTheme/side-by-side.wide.dark.html
@@ -612,6 +612,91 @@
$ pip install numpy matplotlib`
}
+ ], [
+ {
+ from:{
+ role: "bot"
+ },
+ id: "a-00000",
+ type: "message",
+ text: "This is compleded feedback action example.",
+ timestamp: timestamp(),
+ entities: [
+ {
+ ...aiMessageEntity,
+ keywords: ['AIGeneratedContent', 'AllowCopy'],
+ potentialAction: [
+ {
+ "@type": "LikeAction",
+ actionStatus: "CompletedActionStatus",
+ target: {
+ "@type": "EntryPoint",
+ urlTemplate: "ms-directline://postback?interaction=like"
+ }
+ },
+ {
+ "@type": "DislikeAction",
+ actionStatus: "PotentialActionStatus",
+ result: {
+ "@type": "Review",
+ reviewBody: "I don't like it.",
+ "reviewBody-input": {
+ "@type": "PropertyValueSpecification",
+ valueMinLength: 3,
+ valueName: "reason"
+ }
+ },
+ target: {
+ "@type": "EntryPoint",
+ urlTemplate: "ms-directline://postback?interaction=dislike{&reason}"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ from:{
+ role: "bot"
+ },
+ id: "a-00001",
+ type: "message",
+ text: "Hi! I'm Cody, the devbot. How can I help?",
+ timestamp: timestamp(),
+ entities: [
+ {
+ ...aiMessageEntity,
+ keywords: ['AIGeneratedContent', 'AllowCopy'],
+ potentialAction: [
+ {
+ "@type": "LikeAction",
+ actionStatus: "PotentialActionStatus",
+ target: {
+ "@type": "EntryPoint",
+ urlTemplate: "ms-directline://postback?interaction=like"
+ }
+ },
+ {
+ "@type": "DislikeAction",
+ actionStatus: "PotentialActionStatus",
+ result: {
+ "@type": "Review",
+ reviewBody: "I don't like it.",
+ "reviewBody-input": {
+ "@type": "PropertyValueSpecification",
+ valueMinLength: 3,
+ valueName: "reason"
+ }
+ },
+ target: {
+ "@type": "EntryPoint",
+ urlTemplate: "ms-directline://postback?interaction=dislike{&reason}"
+ }
+ }
+ ]
+ }
+ ]
+ }
]];
const leftStore = testHelpers.createStore();
@@ -726,6 +811,24 @@
await host.snapshot();
await host.sendKeys('ENTER');
await host.snapshot();
+ },
+ likeDislike: async sendbox => {
+ sendbox.focus();
+ await host.sendShiftTab();
+ await host.sendKeys('ARROW_UP');
+ await host.sendKeys('ENTER');
+ await host.snapshot();
+ await host.sendKeys('TAB');
+ await host.snapshot();
+ await host.sendKeys('ENTER');
+ await host.snapshot();
+ await host.sendKeys('TAB');
+ await host.snapshot();
+ await host.sendKeys('ENTER');
+ await host.snapshot();
+ await host.sendKeys('ESCAPE');
+ await host.sendKeys('ESCAPE');
+ await host.snapshot();
}
}));
diff --git a/__tests__/html/fluentTheme/side-by-side.wide.dark.js b/__tests__/html/fluentTheme/side-by-side.wide.dark.js
index 464c432f78..ef3cf60870 100644
--- a/__tests__/html/fluentTheme/side-by-side.wide.dark.js
+++ b/__tests__/html/fluentTheme/side-by-side.wide.dark.js
@@ -17,6 +17,10 @@ describe('Fluent theme applied', () => {
test('side by side left - transcript, right - codeblock', () =>
runHTML('fluentTheme/side-by-side.wide.dark?transcript=0&transcript=5&focus=1&focus-preset=viewCode'));
test('side by side left - transcript, right - codeblock dark', () =>
- runHTML('fluentTheme/side-by-side.wide.dark?transcript=0&transcript=5&focus=1&focus-preset=viewCode&codeBlockTheme=github-dark-default'));
+ runHTML(
+ 'fluentTheme/side-by-side.wide.dark?transcript=0&transcript=5&focus=1&focus-preset=viewCode&codeBlockTheme=github-dark-default'
+ ));
+ test('side by side left - transcript, right - feedback', () =>
+ runHTML('fluentTheme/side-by-side.wide.dark?transcript=0&transcript=6&focus=1&focus-preset=likeDislike'));
});
});
diff --git a/__tests__/html/fluentTheme/side-by-side.wide.html b/__tests__/html/fluentTheme/side-by-side.wide.html
index 9b907e4f81..827578df0c 100644
--- a/__tests__/html/fluentTheme/side-by-side.wide.html
+++ b/__tests__/html/fluentTheme/side-by-side.wide.html
@@ -622,6 +622,91 @@
$ pip install numpy matplotlib`
}
+ ], [
+ {
+ from:{
+ role: "bot"
+ },
+ id: "a-00000",
+ type: "message",
+ text: "This is compleded feedback action example.",
+ timestamp: timestamp(),
+ entities: [
+ {
+ ...aiMessageEntity,
+ keywords: ['AIGeneratedContent', 'AllowCopy'],
+ potentialAction: [
+ {
+ "@type": "LikeAction",
+ actionStatus: "CompletedActionStatus",
+ target: {
+ "@type": "EntryPoint",
+ urlTemplate: "ms-directline://postback?interaction=like"
+ }
+ },
+ {
+ "@type": "DislikeAction",
+ actionStatus: "PotentialActionStatus",
+ result: {
+ "@type": "Review",
+ reviewBody: "I don't like it.",
+ "reviewBody-input": {
+ "@type": "PropertyValueSpecification",
+ valueMinLength: 3,
+ valueName: "reason"
+ }
+ },
+ target: {
+ "@type": "EntryPoint",
+ urlTemplate: "ms-directline://postback?interaction=dislike{&reason}"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ from:{
+ role: "bot"
+ },
+ id: "a-00001",
+ type: "message",
+ text: "Hi! I'm Cody, the devbot. How can I help?",
+ timestamp: timestamp(),
+ entities: [
+ {
+ ...aiMessageEntity,
+ keywords: ['AIGeneratedContent', 'AllowCopy'],
+ potentialAction: [
+ {
+ "@type": "LikeAction",
+ actionStatus: "PotentialActionStatus",
+ target: {
+ "@type": "EntryPoint",
+ urlTemplate: "ms-directline://postback?interaction=like"
+ }
+ },
+ {
+ "@type": "DislikeAction",
+ actionStatus: "PotentialActionStatus",
+ result: {
+ "@type": "Review",
+ reviewBody: "I don't like it.",
+ "reviewBody-input": {
+ "@type": "PropertyValueSpecification",
+ valueMinLength: 3,
+ valueName: "reason"
+ }
+ },
+ target: {
+ "@type": "EntryPoint",
+ urlTemplate: "ms-directline://postback?interaction=dislike{&reason}"
+ }
+ }
+ ]
+ }
+ ]
+ }
]];
const leftStore = testHelpers.createStore();
@@ -709,6 +794,24 @@
await host.snapshot();
await host.sendKeys('ENTER');
await host.snapshot();
+ },
+ likeDislike: async sendbox => {
+ sendbox.focus();
+ await host.sendShiftTab();
+ await host.sendKeys('ARROW_UP');
+ await host.sendKeys('ENTER');
+ await host.snapshot();
+ await host.sendKeys('TAB');
+ await host.snapshot();
+ await host.sendKeys('ENTER');
+ await host.snapshot();
+ await host.sendKeys('TAB');
+ await host.snapshot();
+ await host.sendKeys('ENTER');
+ await host.snapshot();
+ await host.sendKeys('ESCAPE');
+ await host.sendKeys('ESCAPE');
+ await host.snapshot();
}
}));
diff --git a/__tests__/html/fluentTheme/side-by-side.wide.js b/__tests__/html/fluentTheme/side-by-side.wide.js
index 06f0a39000..544d83ecb1 100644
--- a/__tests__/html/fluentTheme/side-by-side.wide.js
+++ b/__tests__/html/fluentTheme/side-by-side.wide.js
@@ -16,5 +16,9 @@ describe('Fluent theme applied', () => {
test('side by side left - transcript, right - codeblock', () =>
runHTML('fluentTheme/side-by-side.wide?transcript=0&transcript=5&focus=1&focus-preset=viewCode'));
test('side by side left - transcript, right - codeblock dark', () =>
- runHTML('fluentTheme/side-by-side.wide?transcript=0&transcript=5&focus=1&focus-preset=viewCode&codeBlockTheme=github-dark-default'));
+ runHTML(
+ 'fluentTheme/side-by-side.wide?transcript=0&transcript=5&focus=1&focus-preset=viewCode&codeBlockTheme=github-dark-default'
+ ));
+ test('side by side left - transcript, right - feedback', () =>
+ runHTML('fluentTheme/side-by-side.wide?transcript=0&transcript=6&focus=1&focus-preset=likeDislike'));
});
diff --git a/__tests__/html2/activity/feedback.activity.html b/__tests__/html2/activity/feedback.activity.html
new file mode 100644
index 0000000000..f930e1baaa
--- /dev/null
+++ b/__tests__/html2/activity/feedback.activity.html
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__tests__/html2/activity/feedback.activity.html.snap-1.png b/__tests__/html2/activity/feedback.activity.html.snap-1.png
new file mode 100644
index 0000000000..fe389e042e
Binary files /dev/null and b/__tests__/html2/activity/feedback.activity.html.snap-1.png differ
diff --git a/__tests__/html2/activity/feedback.activity.html.snap-2.png b/__tests__/html2/activity/feedback.activity.html.snap-2.png
new file mode 100644
index 0000000000..5f8ab84a05
Binary files /dev/null and b/__tests__/html2/activity/feedback.activity.html.snap-2.png differ
diff --git a/__tests__/html2/activity/feedback.activity.html.snap-3.png b/__tests__/html2/activity/feedback.activity.html.snap-3.png
new file mode 100644
index 0000000000..1b4a6f5974
Binary files /dev/null and b/__tests__/html2/activity/feedback.activity.html.snap-3.png differ
diff --git a/__tests__/html2/activity/feedback.activity.html.snap-4.png b/__tests__/html2/activity/feedback.activity.html.snap-4.png
new file mode 100644
index 0000000000..cba6d2efdd
Binary files /dev/null and b/__tests__/html2/activity/feedback.activity.html.snap-4.png differ
diff --git a/__tests__/html2/activity/feedback.activity.html.snap-5.png b/__tests__/html2/activity/feedback.activity.html.snap-5.png
new file mode 100644
index 0000000000..1a330e4f64
Binary files /dev/null and b/__tests__/html2/activity/feedback.activity.html.snap-5.png differ
diff --git a/__tests__/html2/activity/feedback.activity.html.snap-6.png b/__tests__/html2/activity/feedback.activity.html.snap-6.png
new file mode 100644
index 0000000000..166082fd9c
Binary files /dev/null and b/__tests__/html2/activity/feedback.activity.html.snap-6.png differ
diff --git a/__tests__/html2/activity/feedback.status.html b/__tests__/html2/activity/feedback.status.html
new file mode 100644
index 0000000000..b56f8ffaa1
--- /dev/null
+++ b/__tests__/html2/activity/feedback.status.html
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__tests__/html2/activity/feedback.status.html.snap-1.png b/__tests__/html2/activity/feedback.status.html.snap-1.png
new file mode 100644
index 0000000000..cd365c6e45
Binary files /dev/null and b/__tests__/html2/activity/feedback.status.html.snap-1.png differ
diff --git a/__tests__/html2/activity/feedback.status.html.snap-2.png b/__tests__/html2/activity/feedback.status.html.snap-2.png
new file mode 100644
index 0000000000..55b11b5922
Binary files /dev/null and b/__tests__/html2/activity/feedback.status.html.snap-2.png differ
diff --git a/__tests__/html2/activity/feedback.status.html.snap-3.png b/__tests__/html2/activity/feedback.status.html.snap-3.png
new file mode 100644
index 0000000000..26fcc14fc6
Binary files /dev/null and b/__tests__/html2/activity/feedback.status.html.snap-3.png differ
diff --git a/__tests__/html2/activity/feedback.status.html.snap-4.png b/__tests__/html2/activity/feedback.status.html.snap-4.png
new file mode 100644
index 0000000000..f173014f25
Binary files /dev/null and b/__tests__/html2/activity/feedback.status.html.snap-4.png differ
diff --git a/__tests__/html2/activity/feedback.status.html.snap-5.png b/__tests__/html2/activity/feedback.status.html.snap-5.png
new file mode 100644
index 0000000000..8f798755e1
Binary files /dev/null and b/__tests__/html2/activity/feedback.status.html.snap-5.png differ
diff --git a/packages/api/src/StyleOptions.ts b/packages/api/src/StyleOptions.ts
index 77a7c41687..70dfb01051 100644
--- a/packages/api/src/StyleOptions.ts
+++ b/packages/api/src/StyleOptions.ts
@@ -928,6 +928,20 @@ type StyleOptions = {
* New in 4.19.0.
*/
codeBlockTheme?: 'github-light-default' | 'github-dark-default';
+
+ /**
+ * (EXPERIMENTAL) Feedback buttons placement
+ *
+ * - `'activity-actions'` - place feedback buttons inside activity actions
+ * - `'activity-status'` - place feedback buttons inside activity status
+ *
+ * @default 'activity-status'
+ *
+ * @deprecated This is an experimental style options and should not be used without understanding its risk.
+ *
+ * New in 4.19.0.
+ */
+ feedbackActionsPlacement?: 'activity-actions' | 'activity-status';
};
// 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 ec3147fcbd..80d4d11649 100644
--- a/packages/api/src/defaultStyleOptions.ts
+++ b/packages/api/src/defaultStyleOptions.ts
@@ -305,7 +305,9 @@ const DEFAULT_OPTIONS: Required = {
borderAnimationColor2: '#4DD3FF',
borderAnimationColor3: '#2B8DD8',
- codeBlockTheme: 'github-light-default' as const
+ codeBlockTheme: 'github-light-default' as const,
+
+ feedbackActionsPlacement: 'activity-status' as const
};
export default DEFAULT_OPTIONS;
diff --git a/packages/api/src/localization/en-US.json b/packages/api/src/localization/en-US.json
index 8b5fd1ffd5..396e9a6f3d 100644
--- a/packages/api/src/localization/en-US.json
+++ b/packages/api/src/localization/en-US.json
@@ -172,8 +172,10 @@
"_TYPING_INDICATOR_MULTIPLE_TEXT.comment": "This shows when two or more users are typing simultaneously.",
"TYPING_INDICATOR_MULTIPLE_TEXT": "$1 and others are typing.",
"VIEW_CODE_BUTTON_TEXT": "Code",
+ "VOTE_COMPLETE_ALT": "Feedback recorded",
+ "VOTE_COMPLETE_ALT.comment": "This is for screen reader. The label of a button with a thumb up/down icons. The label is used when feedback was recorded.",
"VOTE_DISLIKE_ALT": "Dislike",
- "_VOTE_DISLIKE_ALT.comment": "This is for screen reader. The label of a button with a thumb up icon and is placed next to the timestamp. The button is for giving feedback that the end-user don't think the response is useful.",
+ "_VOTE_DISLIKE_ALT.comment": "This is for screen reader. The label of a button with a thumb up icon. The button is for giving feedback that the end-user don't think the response is useful.",
"VOTE_LIKE_ALT": "Like",
- "_VOTE_LIKE_ALT.comment": "This is for screen reader. The label of a button with a thumb up icon and is placed next to the timestamp. The button is for giving feedback that the end-user think the response is useful."
+ "_VOTE_LIKE_ALT.comment": "This is for screen reader. The label of a button with a thumb up icon. The button is for giving feedback that the end-user think the response is useful."
}
diff --git a/packages/component/src/Activity/ActivityFeedback.tsx b/packages/component/src/Activity/ActivityFeedback.tsx
new file mode 100644
index 0000000000..2cb7ad93a0
--- /dev/null
+++ b/packages/component/src/Activity/ActivityFeedback.tsx
@@ -0,0 +1,53 @@
+import { hooks } from 'botframework-webchat-api';
+import { getOrgSchemaMessage, OrgSchemaAction, parseAction, WebChatActivity } from 'botframework-webchat-core';
+import cx from 'classnames';
+import React, { memo, useMemo } from 'react';
+
+import Feedback from './private/Feedback';
+import dereferenceBlankNodes from '../Utils/JSONLinkedData/dereferenceBlankNodes';
+
+const { useStyleOptions } = hooks;
+
+type ActivityFeedbackProps = Readonly<{
+ activity: WebChatActivity;
+}>;
+
+function ActivityFeedback({ activity }: ActivityFeedbackProps) {
+ const [{ feedbackActionsPlacement }] = useStyleOptions();
+
+ const graph = useMemo(() => dereferenceBlankNodes(activity.entities || []), [activity.entities]);
+
+ const messageThing = useMemo(() => getOrgSchemaMessage(graph), [graph]);
+
+ const feedbackActions = useMemo>(() => {
+ try {
+ const reactActions = (messageThing?.potentialAction || []).filter(
+ ({ '@type': type }) => type === 'LikeAction' || type === 'DislikeAction'
+ );
+
+ if (reactActions.length) {
+ return Object.freeze(new Set(reactActions));
+ }
+
+ const voteActions = graph.filter(({ type }) => type === 'https://schema.org/VoteAction').map(parseAction);
+
+ if (voteActions.length) {
+ return Object.freeze(new Set(voteActions));
+ }
+ } catch {
+ // Intentionally left blank.
+ }
+ return Object.freeze(new Set([] as OrgSchemaAction[]));
+ }, [graph, messageThing?.potentialAction]);
+
+ return (
+
+ );
+}
+
+export default memo(ActivityFeedback);
diff --git a/packages/component/src/ActivityStatus/private/Feedback/Feedback.tsx b/packages/component/src/Activity/private/Feedback.tsx
similarity index 58%
rename from packages/component/src/ActivityStatus/private/Feedback/Feedback.tsx
rename to packages/component/src/Activity/private/Feedback.tsx
index b5686dc31f..feb4427e5f 100644
--- a/packages/component/src/ActivityStatus/private/Feedback/Feedback.tsx
+++ b/packages/component/src/Activity/private/Feedback.tsx
@@ -1,24 +1,26 @@
import { hooks } from 'botframework-webchat-api';
import { type OrgSchemaAction } from 'botframework-webchat-core';
-import React, { Fragment, memo, useEffect, useState, type PropsWithChildren } from 'react';
+import React, { Fragment, memo, useEffect, useMemo, useState, type PropsWithChildren } from 'react';
import { useRefFrom } from 'use-ref-from';
-import FeedbackVoteButton from './private/VoteButton';
+import FeedbackVoteButton from './VoteButton';
-const { usePonyfill, usePostActivity } = hooks;
+const { usePonyfill, usePostActivity, useLocalizer } = hooks;
type Props = Readonly<
PropsWithChildren<{
actions: ReadonlySet;
+ className?: string | undefined;
}>
>;
const DEBOUNCE_TIMEOUT = 500;
-const Feedback = memo(({ actions }: Props) => {
+const Feedback = memo(({ actions, className }: Props) => {
const [{ clearTimeout, setTimeout }] = usePonyfill();
const [selectedAction, setSelectedAction] = useState();
const postActivity = usePostActivity();
+ const localize = useLocalizer();
const postActivityRef = useRefFrom(postActivity);
@@ -41,14 +43,31 @@ const Feedback = memo(({ actions }: Props) => {
return () => clearTimeout(timeout);
}, [clearTimeout, postActivityRef, selectedAction, setTimeout]);
+ const actionProps = useMemo(
+ () =>
+ [...actions].some(action => action.actionStatus === 'CompletedActionStatus')
+ ? {
+ disabled: true,
+ title: localize('VOTE_COMPLETE_ALT')
+ }
+ : undefined,
+ [actions, localize]
+ );
+
return (
- {Array.from(actions).map((action, index) => (
+ {[...actions].map((action, index) => (
))}
diff --git a/packages/component/src/ActivityStatus/private/Feedback/private/ThumbButton.Image.tsx b/packages/component/src/Activity/private/ThumbButton.Image.tsx
similarity index 70%
rename from packages/component/src/ActivityStatus/private/Feedback/private/ThumbButton.Image.tsx
rename to packages/component/src/Activity/private/ThumbButton.Image.tsx
index 8ddd5a1a61..4ebf55e3de 100644
--- a/packages/component/src/ActivityStatus/private/Feedback/private/ThumbButton.Image.tsx
+++ b/packages/component/src/Activity/private/ThumbButton.Image.tsx
@@ -1,9 +1,9 @@
import React, { memo } from 'react';
-import ThumbDislike16Filled from './icons/ThumbDislike16Filled';
-import ThumbDislike16Regular from './icons/ThumbDislike16Regular';
-import ThumbLike16Filled from './icons/ThumbLike16Filled';
-import ThumbLike16Regular from './icons/ThumbLike16Regular';
+import ThumbDislike16Filled from './ThumbDislike16Filled';
+import ThumbDislike16Regular from './ThumbDislike16Regular';
+import ThumbLike16Filled from './ThumbLike16Filled';
+import ThumbLike16Regular from './ThumbLike16Regular';
type Props = Readonly<{
className?: string;
diff --git a/packages/component/src/ActivityStatus/private/Feedback/private/ThumbButton.tsx b/packages/component/src/Activity/private/ThumbButton.tsx
similarity index 53%
rename from packages/component/src/ActivityStatus/private/Feedback/private/ThumbButton.tsx
rename to packages/component/src/Activity/private/ThumbButton.tsx
index 229385adaa..ac5ce993c4 100644
--- a/packages/component/src/ActivityStatus/private/Feedback/private/ThumbButton.tsx
+++ b/packages/component/src/Activity/private/ThumbButton.tsx
@@ -1,39 +1,51 @@
import { hooks } from 'botframework-webchat-api';
import classNames from 'classnames';
-import React, { memo } from 'react';
+import React, { memo, useCallback, useMemo } from 'react';
+import { useRefFrom } from 'use-ref-from';
import ThumbButtonImage from './ThumbButton.Image';
-import useStyleSet from '../../../../hooks/useStyleSet';
+import useStyleSet from '../../hooks/useStyleSet';
+import { Tooltip } from '../../Tooltip';
const { useLocalizer } = hooks;
type Props = Readonly<{
+ className?: string | undefined;
direction: 'down' | 'up';
+ disabled?: boolean | undefined;
onClick?: () => void;
pressed?: boolean;
+ title?: string | undefined;
}>;
-const ThumbButton = memo(({ direction, onClick, pressed }: Props) => {
+const ThumbButton = memo(({ className, direction, disabled, onClick, pressed, title }: Props) => {
const [{ thumbButton }] = useStyleSet();
const localize = useLocalizer();
+ const onClickRef = useRefFrom(onClick);
- const title = localize(direction === 'down' ? 'VOTE_DISLIKE_ALT' : 'VOTE_LIKE_ALT');
+ const buttonTitle = useMemo(
+ () => title ?? localize(direction === 'down' ? 'VOTE_DISLIKE_ALT' : 'VOTE_LIKE_ALT'),
+ [direction, localize, title]
+ );
+
+ const handleClick = useCallback(() => !disabled && onClickRef.current?.(), [disabled, onClickRef]);
return (
);
});
diff --git a/packages/component/src/ActivityStatus/private/Feedback/private/icons/ThumbDislike16Filled.tsx b/packages/component/src/Activity/private/ThumbDislike16Filled.tsx
similarity index 100%
rename from packages/component/src/ActivityStatus/private/Feedback/private/icons/ThumbDislike16Filled.tsx
rename to packages/component/src/Activity/private/ThumbDislike16Filled.tsx
diff --git a/packages/component/src/ActivityStatus/private/Feedback/private/icons/ThumbDislike16Regular.tsx b/packages/component/src/Activity/private/ThumbDislike16Regular.tsx
similarity index 100%
rename from packages/component/src/ActivityStatus/private/Feedback/private/icons/ThumbDislike16Regular.tsx
rename to packages/component/src/Activity/private/ThumbDislike16Regular.tsx
diff --git a/packages/component/src/ActivityStatus/private/Feedback/private/icons/ThumbLike16Filled.tsx b/packages/component/src/Activity/private/ThumbLike16Filled.tsx
similarity index 100%
rename from packages/component/src/ActivityStatus/private/Feedback/private/icons/ThumbLike16Filled.tsx
rename to packages/component/src/Activity/private/ThumbLike16Filled.tsx
diff --git a/packages/component/src/ActivityStatus/private/Feedback/private/icons/ThumbLike16Regular.tsx b/packages/component/src/Activity/private/ThumbLike16Regular.tsx
similarity index 100%
rename from packages/component/src/ActivityStatus/private/Feedback/private/icons/ThumbLike16Regular.tsx
rename to packages/component/src/Activity/private/ThumbLike16Regular.tsx
diff --git a/packages/component/src/ActivityStatus/private/Feedback/private/VoteButton.tsx b/packages/component/src/Activity/private/VoteButton.tsx
similarity index 67%
rename from packages/component/src/ActivityStatus/private/Feedback/private/VoteButton.tsx
rename to packages/component/src/Activity/private/VoteButton.tsx
index c3bf888a54..e67c6d5e60 100644
--- a/packages/component/src/ActivityStatus/private/Feedback/private/VoteButton.tsx
+++ b/packages/component/src/Activity/private/VoteButton.tsx
@@ -2,15 +2,18 @@ import { onErrorResumeNext, parseVoteAction, type OrgSchemaAction } from 'botfra
import React, { memo, useCallback, useMemo } from 'react';
import { useRefFrom } from 'use-ref-from';
-import ThumbsButton from './ThumbButton';
+import ThumbButton from './ThumbButton';
type Props = Readonly<{
+ className?: string | undefined;
action: OrgSchemaAction;
+ disabled?: boolean | undefined;
onClick?: (action: OrgSchemaAction) => void;
pressed: boolean;
+ title?: string | undefined;
}>;
-const FeedbackVoteButton = memo(({ action, onClick, pressed }: Props) => {
+const FeedbackVoteButton = memo(({ action, className, disabled, onClick, pressed, title }: Props) => {
const onClickRef = useRefFrom(onClick);
const voteActionRef = useRefFrom(action);
@@ -28,7 +31,16 @@ const FeedbackVoteButton = memo(({ action, onClick, pressed }: Props) => {
const handleClick = useCallback(() => onClickRef.current?.(voteActionRef.current), [onClickRef, voteActionRef]);
- return ;
+ return (
+
+ );
});
FeedbackVoteButton.displayName = 'FeedbackVoteButton';
diff --git a/packages/component/src/ActivityStatus/OthersActivityStatus.tsx b/packages/component/src/ActivityStatus/OthersActivityStatus.tsx
index 6ab15ed222..8b03b17991 100644
--- a/packages/component/src/ActivityStatus/OthersActivityStatus.tsx
+++ b/packages/component/src/ActivityStatus/OthersActivityStatus.tsx
@@ -1,6 +1,6 @@
+import { hooks } from 'botframework-webchat-api';
import {
getOrgSchemaMessage,
- OrgSchemaAction,
OrgSchemaProject,
parseAction,
parseClaim,
@@ -8,22 +8,25 @@ import {
type WebChatActivity
} from 'botframework-webchat-core';
import classNames from 'classnames';
-import React, { memo, useMemo, type ReactNode } from 'react';
+import React, { memo, useMemo } from 'react';
import useStyleSet from '../hooks/useStyleSet';
import dereferenceBlankNodes from '../Utils/JSONLinkedData/dereferenceBlankNodes';
-import Feedback from './private/Feedback/Feedback';
import Originator from './private/Originator';
-import Slotted from './Slotted';
import Timestamp from './Timestamp';
+import ActivityFeedback from '../Activity/ActivityFeedback';
+import StatusSlot from './StatusSlot';
-type Props = Readonly<{ activity: WebChatActivity }>;
+const { useStyleOptions } = hooks;
+
+type Props = Readonly<{ activity: WebChatActivity; className?: string | undefined }>;
const warnRootLevelThings = warnOnce(
'Root-level things are being deprecated, please relate all things to `entities[@id=""]` instead. This feature will be removed in 2025-03-06.'
);
-const OthersActivityStatus = memo(({ activity }: Props) => {
+const OthersActivityStatus = memo(({ activity, className }: Props) => {
+ const [{ feedbackActionsPlacement }] = useStyleOptions();
const [{ sendStatus }] = useStyleSet();
const { timestamp } = activity;
const graph = useMemo(() => dereferenceBlankNodes(activity.entities || []), [activity.entities]);
@@ -56,38 +59,24 @@ const OthersActivityStatus = memo(({ activity }: Props) => {
}
}, [graph, messageThing]);
- const feedbackActions = useMemo | undefined>(() => {
- try {
- const reactActions = (messageThing?.potentialAction || []).filter(
- ({ '@type': type }) => type === 'LikeAction' || type === 'DislikeAction'
- );
-
- if (reactActions.length) {
- return Object.freeze(new Set(reactActions));
- }
-
- const voteActions = graph.filter(({ type }) => type === 'https://schema.org/VoteAction').map(parseAction);
-
- if (voteActions.length) {
- return Object.freeze(new Set(voteActions));
- }
- } catch {
- // Intentionally left blank.
- }
- }, [graph, messageThing]);
-
return (
-
- {useMemo(
- () =>
- [
- timestamp && ,
- claimInterpreter && ,
- feedbackActions?.size &&
- ].filter(Boolean),
- [claimInterpreter, timestamp, feedbackActions]
+
+ {timestamp && (
+
+
+
+ )}
+ {claimInterpreter && (
+
+
+
+ )}
+ {feedbackActionsPlacement === 'activity-status' && (
+
+
+
)}
-
+
);
});
diff --git a/packages/component/src/ActivityStatus/SelfActivityStatus.tsx b/packages/component/src/ActivityStatus/SelfActivityStatus.tsx
index 4b2b51b8d2..b4a2936509 100644
--- a/packages/component/src/ActivityStatus/SelfActivityStatus.tsx
+++ b/packages/component/src/ActivityStatus/SelfActivityStatus.tsx
@@ -2,20 +2,21 @@ import { type WebChatActivity } from 'botframework-webchat-core';
import classNames from 'classnames';
import React, { memo } from 'react';
-import Slotted from './Slotted';
import Timestamp from './Timestamp';
import useStyleSet from '../hooks/useStyleSet';
-type Props = Readonly<{ activity: WebChatActivity }>;
+type Props = Readonly<{ activity: WebChatActivity; className?: string | undefined }>;
-const SelftActivityStatus = memo(({ activity }: Props) => {
+const SelftActivityStatus = memo(({ activity, className }: Props) => {
const [{ sendStatus }] = useStyleSet();
const { timestamp } = activity;
return timestamp ? (
-
+
-
+
) : null;
});
diff --git a/packages/component/src/ActivityStatus/Slotted.tsx b/packages/component/src/ActivityStatus/Slotted.tsx
deleted file mode 100644
index 1c0506eca0..0000000000
--- a/packages/component/src/ActivityStatus/Slotted.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React, { Children, Fragment, memo, type PropsWithChildren } from 'react';
-import classNames from 'classnames';
-
-type Props = Readonly>;
-
-const Slotted = memo(({ children, className }: Props) => (
-
- {Children.map(children, (child, index) =>
- // TODO: We may be able to do this in pure CSS, say, :not(:first-child)::before { content: '|' }.
- index ? (
-
-
- {'|'}
-
- {child}
-
- ) : (
- child
- )
- )}
-
-));
-
-Slotted.displayName = 'SlottedActivityStatus';
-
-export default Slotted;
diff --git a/packages/component/src/ActivityStatus/StatusSlot.tsx b/packages/component/src/ActivityStatus/StatusSlot.tsx
new file mode 100644
index 0000000000..c9a1fe4b54
--- /dev/null
+++ b/packages/component/src/ActivityStatus/StatusSlot.tsx
@@ -0,0 +1,12 @@
+import React, { memo, ReactNode } from 'react';
+import classNames from 'classnames';
+
+type Props = Readonly<{ className?: string; children?: ReactNode | undefined }>;
+
+const StatusSlot = ({ children, className }: Props) => (
+ {children}
+);
+
+StatusSlot.displayName = 'StatusSlot';
+
+export default memo(StatusSlot);
diff --git a/packages/component/src/Attachment/Text/private/MarkdownTextContent.tsx b/packages/component/src/Attachment/Text/private/MarkdownTextContent.tsx
index b5bdbc2d73..07f4b57a8a 100644
--- a/packages/component/src/Attachment/Text/private/MarkdownTextContent.tsx
+++ b/packages/component/src/Attachment/Text/private/MarkdownTextContent.tsx
@@ -25,8 +25,9 @@ import MessageSensitivityLabel, { type MessageSensitivityLabelProps } from './Me
import isAIGeneratedActivity from './isAIGeneratedActivity';
import isBasedOnSoftwareSourceCode from './isBasedOnSoftwareSourceCode';
import isHTMLButtonElement from './isHTMLButtonElement';
+import ActivityFeedback from '../../../Activity/ActivityFeedback';
-const { useLocalizer } = hooks;
+const { useLocalizer, useStyleOptions } = hooks;
type Entry = {
claim?: OrgSchemaClaim | undefined;
@@ -47,6 +48,7 @@ function isCitationURL(url: string): boolean {
}
const MarkdownTextContent = memo(({ activity, children, markdown }: Props) => {
+ const [{ feedbackActionsPlacement }] = useStyleOptions();
const [
{
citationModalDialog: citationModalDialogStyleSet,
@@ -244,6 +246,9 @@ const MarkdownTextContent = memo(({ activity, children, markdown }: Props) => {
{activity.type === 'message' && activity.text && messageThing?.keywords?.includes('AllowCopy') ? (
) : null}
+ {activity.type === 'message' && feedbackActionsPlacement === 'activity-actions' && (
+
+ )}
);
diff --git a/packages/component/src/Middleware/ActivityStatus/createTimestampMiddleware.tsx b/packages/component/src/Middleware/ActivityStatus/createTimestampMiddleware.tsx
index ea8896988f..cd34e75a08 100644
--- a/packages/component/src/Middleware/ActivityStatus/createTimestampMiddleware.tsx
+++ b/packages/component/src/Middleware/ActivityStatus/createTimestampMiddleware.tsx
@@ -20,9 +20,9 @@ export default function createTimestampMiddleware(): ActivityStatusMiddleware {
// If "hideTimestamp" is set, we will not render the visual timestamp. But continue to render the screen reader only version.
return ;
} else if (activity.from.role === 'bot') {
- return ;
+ return ;
}
- return ;
+ return ;
};
}
diff --git a/packages/component/src/Styles/StyleSet/SendStatus.ts b/packages/component/src/Styles/StyleSet/SendStatus.ts
index 7d585747a9..b851d6fb3d 100644
--- a/packages/component/src/Styles/StyleSet/SendStatus.ts
+++ b/packages/component/src/Styles/StyleSet/SendStatus.ts
@@ -14,6 +14,18 @@ export default function createSendStatusStyle() {
gap: 4
},
+ '& .webchat__activity-status-slot': {
+ display: 'contents',
+
+ '&:not(:first-child)::before': {
+ content: `"|"`
+ },
+
+ '&:empty': {
+ display: 'none'
+ }
+ },
+
'& .webchat__activity-status__originator': {
alignItems: 'center',
diff --git a/packages/component/src/Styles/StyleSet/SlottedActivityStatus.ts b/packages/component/src/Styles/StyleSet/SlottedActivityStatus.ts
deleted file mode 100644
index df5f000e59..0000000000
--- a/packages/component/src/Styles/StyleSet/SlottedActivityStatus.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import CSSTokens from '../CSSTokens';
-
-export default function createSlottedActivityStatus() {
- return {
- '&.webchat__slotted-activity-status': {
- alignItems: 'center',
- display: 'inline-flex',
- gap: 4,
- marginTop: `calc(${CSSTokens.PaddingRegular} / 2)`,
-
- '& .webchat__slotted-activity-status__pipe': {
- fontSize: CSSTokens.FontSizeSmall
- }
- }
- };
-}
diff --git a/packages/component/src/Styles/StyleSet/ThumbButton.ts b/packages/component/src/Styles/StyleSet/ThumbButton.ts
index 7f5eba727e..85613c1d53 100644
--- a/packages/component/src/Styles/StyleSet/ThumbButton.ts
+++ b/packages/component/src/Styles/StyleSet/ThumbButton.ts
@@ -3,43 +3,73 @@ import CSSTokens from '../CSSTokens';
export default function () {
return {
'&.webchat__thumb-button': {
+ alignItems: 'center',
appearance: 'none',
- background: 'Transparent',
+ background: 'transparent',
border: 0,
borderRadius: 2,
+ boxSizing: 'content-box',
+ display: 'grid',
+ gridTemplateAreas: `"icon"`,
height: 16,
- /* The Fluent icon is larger than the button. We need to clip it.
- Without clipping, hover effect will appear on the edge of the button but not possible to click. */
- overflow: 'hidden',
+ justifyContent: 'center',
padding: 0,
width: 16,
+ '&.webchat__thumb-button--large': {
+ border: '1px solid transparent',
+ borderRadius: '4px',
+ height: '20px',
+ padding: '5px',
+ width: '20px',
+
+ '& .webchat__thumb-button__image': {
+ color: 'currentColor',
+ fontSize: '20px',
+ height: '1em',
+ width: '1em'
+ },
+
+ '&:hover, &:active, &.webchat__thumb-button--is-pressed': {
+ background: 'transparent',
+ color: CSSTokens.ColorAccent
+ },
+
+ '&[aria-disabled="true"]': {
+ color: CSSTokens.ColorSubtle
+ }
+ },
+
'&:active': {
background: '#EDEBE9'
},
- '&:focus': {
+ '&:focus-visible': {
outline: 'solid 1px #605E5C'
},
'& .webchat__thumb-button__image': {
color: CSSTokens.ColorAccent,
- width: 14
- },
+ gridArea: 'icon',
+ visibility: 'hidden',
+ width: 14,
- '&:hover .webchat__thumb-button__image:not(.webchat__thumb-button__image--is-filled)': {
- display: 'none'
+ '&.webchat__thumb-button__image--is-stroked': {
+ visibility: 'visible'
+ }
},
- '&.webchat__thumb-button--is-pressed .webchat__thumb-button__image:not(.webchat__thumb-button__image--is-filled)':
- {
- display: 'none'
- },
+ '&:not([aria-disabled="true"]):hover, &.webchat__thumb-button--is-pressed': {
+ '& .webchat__thumb-button__image': {
+ '&.webchat__thumb-button__image--is-stroked': {
+ visibility: 'hidden'
+ },
- '&.webchat__thumb-button:not(:hover):not(.webchat__thumb-button--is-pressed) .webchat__thumb-button__image--is-filled':
- {
- display: 'none'
+ '&.webchat__thumb-button__image--is-filled': {
+ visibility: 'visible'
+ }
}
+ }
}
};
}
diff --git a/packages/component/src/Styles/StyleSet/Tooltip.ts b/packages/component/src/Styles/StyleSet/Tooltip.ts
new file mode 100644
index 0000000000..238cf4b142
--- /dev/null
+++ b/packages/component/src/Styles/StyleSet/Tooltip.ts
@@ -0,0 +1,62 @@
+export default () => ({
+ '*:has(> &.webchat__tooltip)': {
+ position: 'relative',
+
+ '&:is(:hover, :focus-visible, :active) > .webchat__tooltip': {
+ opacity: 1,
+ transitionDelay: '400ms'
+ }
+ },
+ '&.webchat__tooltip': {
+ '--webchat__tooltip-tip-size': '8.484px',
+ '--webchat__tooltip-background': '#fff',
+
+ background: 'var(--webchat__tooltip-background)',
+ borderRadius: '4px',
+ color: '#242424',
+ filter: `drop-shadow(0px 4px 8px rgba(0, 0, 0, 0.14)) drop-shadow(0px 0px 2px rgba(0, 0, 0, 0.12))`,
+ fontSize: '12px',
+ inlineSize: 'max-content',
+ isolation: 'isolate',
+ lineHeight: '16px',
+ margin: 0,
+ maxInlineSize: '30ch',
+ opacity: 0,
+ padding: '6px 12px',
+ pointerEvents: 'none',
+ position: 'absolute',
+ textAlign: 'start',
+ transition: 'opacity 0.2s ease',
+ userSelect: 'none',
+ willChange: 'filter',
+
+ '&::after': {
+ background: 'inherit',
+ color: 'currentColor',
+ content: `""`,
+ inset: 0,
+ position: 'absolute'
+ },
+
+ '&.webchat__tooltip--block-start, &.webchat__tooltip--block-end': {
+ textAlign: 'center'
+ },
+
+ '&.webchat__tooltip--block-start': {
+ insetBlockEnd: 'calc(100% + 7px)',
+ insetInlineStart: '50%',
+ transform: 'translate(-50%, 0)',
+
+ '&::after': {
+ border: '1px solid var(--webchat__tooltip-background)',
+ borderBottomLeftRadius: '2px',
+ clipPath: 'polygon(0% 0%, 100% 100%, 0% 100%)',
+ height: 'var(--webchat__tooltip-tip-size)',
+ insetBlockStart: 'calc(100% - 6px)',
+ insetInlineStart: 'calc(50% - var(--webchat__tooltip-tip-size) / 2)',
+ transform: 'rotate(-45deg)',
+ width: 'var(--webchat__tooltip-tip-size)'
+ }
+ }
+ }
+});
diff --git a/packages/component/src/Styles/createStyleSet.ts b/packages/component/src/Styles/createStyleSet.ts
index ecf70b65e1..d8e334eee6 100644
--- a/packages/component/src/Styles/createStyleSet.ts
+++ b/packages/component/src/Styles/createStyleSet.ts
@@ -33,7 +33,6 @@ import createSendBoxButtonStyle from './StyleSet/SendBoxButton';
import createSendBoxStyle from './StyleSet/SendBox';
import createSendBoxTextBoxStyle from './StyleSet/SendBoxTextBox';
import createSendStatusStyle from './StyleSet/SendStatus';
-import createSlottedActivityStatusStyle from './StyleSet/SlottedActivityStatus';
import createSpinnerAnimationStyle from './StyleSet/SpinnerAnimation';
import createStackedLayoutStyle from './StyleSet/StackedLayout';
import createSuggestedActionsStyle from './StyleSet/SuggestedActions';
@@ -41,6 +40,7 @@ import createSuggestedActionStyle from './StyleSet/SuggestedAction';
import createTextContentStyle from './StyleSet/TextContent';
import createThumbButtonStyle from './StyleSet/ThumbButton';
import createToasterStyle from './StyleSet/Toaster';
+import createTooltipStyle from './StyleSet/Tooltip';
import createToastStyle from './StyleSet/Toast';
import createTypingAnimationStyle from './StyleSet/TypingAnimation';
import createTypingIndicatorStyle from './StyleSet/TypingIndicator';
@@ -111,9 +111,9 @@ export default function createStyleSet(styleOptions: StyleOptions) {
modalDialog: createModalDialogStyle(),
renderMarkdown: createRenderMarkdownStyle(),
sendStatus: createSendStatusStyle(),
- slottedActivityStatus: createSlottedActivityStatusStyle(),
textContent: createTextContentStyle(),
thumbButton: createThumbButtonStyle(),
+ tooltip: createTooltipStyle(),
viewCodeDialog: createViewCodeDialogStyle()
} as const);
}
diff --git a/packages/component/src/Tooltip/index.ts b/packages/component/src/Tooltip/index.ts
new file mode 100644
index 0000000000..5e2cd55af9
--- /dev/null
+++ b/packages/component/src/Tooltip/index.ts
@@ -0,0 +1 @@
+export { default as Tooltip } from './private/Tooltip';
diff --git a/packages/component/src/Tooltip/private/Tooltip.tsx b/packages/component/src/Tooltip/private/Tooltip.tsx
new file mode 100644
index 0000000000..6cdc2bb3b4
--- /dev/null
+++ b/packages/component/src/Tooltip/private/Tooltip.tsx
@@ -0,0 +1,26 @@
+import cx from 'classnames';
+import React, { memo, type ReactNode } from 'react';
+import { useStyleSet } from '../../hooks';
+
+type TooltipProps = Readonly<{
+ children: ReactNode;
+ className?: string | undefined;
+ position?: 'block-start';
+}>;
+
+function Tooltip({ children, className, position = 'block-start' }: TooltipProps) {
+ const [{ tooltip: tooltipClassName }] = useStyleSet();
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default memo(Tooltip);
diff --git a/packages/core/src/types/external/OrgSchema/Action.ts b/packages/core/src/types/external/OrgSchema/Action.ts
index d69047f849..374ee63ee6 100644
--- a/packages/core/src/types/external/OrgSchema/Action.ts
+++ b/packages/core/src/types/external/OrgSchema/Action.ts
@@ -21,7 +21,7 @@ export type Action = Thing & {
*/
actionStatus?:
| 'ActiveActionStatus'
- | 'CompletedActionStatu'
+ | 'CompletedActionStatus'
| 'FailedActionStatus'
| 'PotentialActionStatus'
| undefined;
diff --git a/packages/fluent-theme/src/components/theme/Theme.module.css b/packages/fluent-theme/src/components/theme/Theme.module.css
index 351edb8c2a..8a1b9811d4 100644
--- a/packages/fluent-theme/src/components/theme/Theme.module.css
+++ b/packages/fluent-theme/src/components/theme/Theme.module.css
@@ -33,6 +33,9 @@
--webchat-colorNeutralStencil1: var(--colorNeutralStencil1, #e6e6e6); /* #575757 for dark mode */
--webchat-colorNeutralStencil2: var(--colorNeutralStencil2, #fafafa); /* #333333 for dark mode */
+ --webchat-colorNeutralShadowAmbient: var(--colorNeutralShadowAmbient, rgba(0, 0, 0, 0.12));
+ --webchat-colorNeutralShadowKey: var(--colorNeutralShadowKey, rgba(0, 0, 0, 0.14));
+
--webchat-colorTransparentBackground: var(--colorTransparentBackground, rgba(0, 0, 0, 0.4));
--webchat-colorNeutralStrokeDisabled: var(--colorNeutralStrokeDisabled, #e0e0e0);
@@ -590,3 +593,31 @@
:global(.webchat-fluent).theme :global(.webchat__monochrome-image-masker) {
background-color: var(--webchat-colorNeutralForeground4);
}
+
+/* Feedback button */
+:global(.webchat-fluent).theme :global(.webchat__thumb-button) {
+ color: var(--webchat-colorNeutralForeground1);
+
+ &:focus-visible {
+ outline: var(--webchat-strokeWidthThick) solid var(--webchat-colorStrokeFocus2);
+ }
+
+ &[aria-disabled='true'] {
+ color: var(--webchat-colorNeutralForegroundDisabled);
+ }
+}
+
+/* Tooltip */
+:global(.webchat-fluent).theme :global(.webchat__tooltip) {
+ --webchat__tooltip-background: var(--tooltip-background, var(--webchat-colorNeutralBackground1));
+
+ color: var(--webchat-colorNeutralForeground1);
+ filter: drop-shadow(0 0 2px var(--webchat-colorNeutralShadowAmbient))
+ drop-shadow(0 4px 8px var(--webchat-colorNeutralShadowKey));
+ font-family: var(--webchat-fontFamilyBase);
+ font-size: var(--webchat-fontSizeBase200);
+ font-weight: var(--webchat-fontWeightRegular);
+ line-height: var(--webchat-lineHeightBase200);
+ padding: var(--webchat-spacingVerticalSNudge) var(--webchat-spacingHorizontalM);
+ transition: opacity var(--webchat-durationNormal) var(--webchat-curveDecelerateMid);
+}
diff --git a/packages/fluent-theme/src/private/FluentThemeProvider.tsx b/packages/fluent-theme/src/private/FluentThemeProvider.tsx
index 7c1fcdfa6d..ee886f4d52 100644
--- a/packages/fluent-theme/src/private/FluentThemeProvider.tsx
+++ b/packages/fluent-theme/src/private/FluentThemeProvider.tsx
@@ -1,4 +1,4 @@
-import type { ActivityMiddleware } from 'botframework-webchat-api';
+import { type ActivityMiddleware, type StyleOptions } from 'botframework-webchat-api';
import { Components } from 'botframework-webchat-component';
import { WebChatDecorator } from 'botframework-webchat-component/decorator';
import React, { memo, type ReactNode } from 'react';
@@ -43,11 +43,20 @@ const sendBoxMiddleware = [() => () => () => PrimarySendBox];
const styles = createStyles();
+const fluentStyleOptions: StyleOptions = Object.freeze({
+ feedbackActionsPlacement: 'activity-actions'
+});
+
const FluentThemeProvider = ({ children, variant = 'fluent' }: Props) => (
-
+
{children}