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}