From 17b9981afdf93089e6498396b631c40cafe18a44 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 9 Jan 2025 20:51:46 +0000 Subject: [PATCH] Fluent: allow displaying feedback inside activity actions toolbar --- .../fluentTheme/side-by-side.wide.dark.html | 61 +++++++++++++++++++ .../fluentTheme/side-by-side.wide.dark.js | 6 +- .../html/fluentTheme/side-by-side.wide.html | 61 +++++++++++++++++++ .../html/fluentTheme/side-by-side.wide.js | 6 +- packages/api/src/StyleOptions.ts | 12 ++++ packages/api/src/defaultStyleOptions.ts | 4 +- .../src/Activity/ActivityFeedback.tsx | 53 ++++++++++++++++ .../private}/Feedback.tsx | 6 +- .../private/ThumbButton.Image.tsx | 8 +-- .../private/ThumbButton.tsx | 6 +- .../private}/ThumbDislike16Filled.tsx | 0 .../private}/ThumbDislike16Regular.tsx | 0 .../private}/ThumbLike16Filled.tsx | 0 .../private}/ThumbLike16Regular.tsx | 0 .../private/VoteButton.tsx | 5 +- .../ActivityStatus/OthersActivityStatus.tsx | 45 +++----------- .../src/ActivityStatus/SelfActivityStatus.tsx | 5 +- .../component/src/ActivityStatus/Slotted.tsx | 26 -------- .../src/ActivityStatus/StatusSlot.tsx | 12 ++++ .../Text/private/MarkdownTextContent.tsx | 2 + .../src/Styles/StyleSet/SendStatus.ts | 11 ++++ .../src/Styles/StyleSet/ThumbButton.ts | 30 ++++++++- .../src/components/theme/Theme.module.css | 9 +++ .../src/private/FluentThemeProvider.tsx | 13 +++- 24 files changed, 299 insertions(+), 82 deletions(-) create mode 100644 packages/component/src/Activity/ActivityFeedback.tsx rename packages/component/src/{ActivityStatus/private/Feedback => Activity/private}/Feedback.tsx (89%) rename packages/component/src/{ActivityStatus/private/Feedback => Activity}/private/ThumbButton.Image.tsx (70%) rename packages/component/src/{ActivityStatus/private/Feedback => Activity}/private/ThumbButton.tsx (87%) rename packages/component/src/{ActivityStatus/private/Feedback/private/icons => Activity/private}/ThumbDislike16Filled.tsx (100%) rename packages/component/src/{ActivityStatus/private/Feedback/private/icons => Activity/private}/ThumbDislike16Regular.tsx (100%) rename packages/component/src/{ActivityStatus/private/Feedback/private/icons => Activity/private}/ThumbLike16Filled.tsx (100%) rename packages/component/src/{ActivityStatus/private/Feedback/private/icons => Activity/private}/ThumbLike16Regular.tsx (100%) rename packages/component/src/{ActivityStatus/private/Feedback => Activity}/private/VoteButton.tsx (81%) delete mode 100644 packages/component/src/ActivityStatus/Slotted.tsx create mode 100644 packages/component/src/ActivityStatus/StatusSlot.tsx diff --git a/__tests__/html/fluentTheme/side-by-side.wide.dark.html b/__tests__/html/fluentTheme/side-by-side.wide.dark.html index 77e8176886..0b1a32e037 100644 --- a/__tests__/html/fluentTheme/side-by-side.wide.dark.html +++ b/__tests__/html/fluentTheme/side-by-side.wide.dark.html @@ -612,6 +612,49 @@ $ pip install numpy matplotlib` } + ], [ + { + 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 +769,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..8ca2ea848a 100644 --- a/__tests__/html/fluentTheme/side-by-side.wide.html +++ b/__tests__/html/fluentTheme/side-by-side.wide.html @@ -622,6 +622,49 @@ $ pip install numpy matplotlib` } + ], [ + { + 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 +752,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/packages/api/src/StyleOptions.ts b/packages/api/src/StyleOptions.ts index 77a7c41687..0486772305 100644 --- a/packages/api/src/StyleOptions.ts +++ b/packages/api/src/StyleOptions.ts @@ -928,6 +928,18 @@ type StyleOptions = { * New in 4.19.0. */ codeBlockTheme?: 'github-light-default' | 'github-dark-default'; + + /** + * Feedback actions placement + * + * - `'activity-actions'` - place feedback buttons inside activity actions + * - `'activity-status'` - place feedback buttons inside activity status + * + * @default 'activity-status' + * + * 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/component/src/Activity/ActivityFeedback.tsx b/packages/component/src/Activity/ActivityFeedback.tsx new file mode 100644 index 0000000000..e405c7a036 --- /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; + placement: 'activity-status' | 'activity-actions'; +}>; + +function ActivityFeedback({ activity, placement }: ActivityFeedbackProps) { + const [{ feedbackActionsPlacement }] = useStyleOptions(); + + const graph = useMemo(() => dereferenceBlankNodes(activity.entities || []), [activity.entities]); + + const messageThing = useMemo(() => getOrgSchemaMessage(graph), [graph]); + + 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?.potentialAction]); + + return feedbackActions?.size && placement === feedbackActionsPlacement ? ( + + ) : null; +} + +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 89% rename from packages/component/src/ActivityStatus/private/Feedback/Feedback.tsx rename to packages/component/src/Activity/private/Feedback.tsx index b5686dc31f..5ef6d82a30 100644 --- a/packages/component/src/ActivityStatus/private/Feedback/Feedback.tsx +++ b/packages/component/src/Activity/private/Feedback.tsx @@ -3,19 +3,20 @@ import { type OrgSchemaAction } from 'botframework-webchat-core'; import React, { Fragment, memo, useEffect, useState, type PropsWithChildren } from 'react'; import { useRefFrom } from 'use-ref-from'; -import FeedbackVoteButton from './private/VoteButton'; +import FeedbackVoteButton from './VoteButton'; const { usePonyfill, usePostActivity } = 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(); @@ -46,6 +47,7 @@ const Feedback = memo(({ actions }: Props) => { {Array.from(actions).map((action, index) => ( void; pressed?: boolean; }>; -const ThumbButton = memo(({ direction, onClick, pressed }: Props) => { +const ThumbButton = memo(({ className, direction, onClick, pressed }: Props) => { const [{ thumbButton }] = useStyleSet(); const localize = useLocalizer(); @@ -26,6 +27,7 @@ const ThumbButton = memo(({ direction, onClick, pressed }: Props) => { className={classNames( 'webchat__thumb-button', { 'webchat__thumb-button--is-pressed': pressed }, + className, thumbButton + '' )} onClick={onClick} 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 81% rename from packages/component/src/ActivityStatus/private/Feedback/private/VoteButton.tsx rename to packages/component/src/Activity/private/VoteButton.tsx index c3bf888a54..d6e0ace2ef 100644 --- a/packages/component/src/ActivityStatus/private/Feedback/private/VoteButton.tsx +++ b/packages/component/src/Activity/private/VoteButton.tsx @@ -5,12 +5,13 @@ import { useRefFrom } from 'use-ref-from'; import ThumbsButton from './ThumbButton'; type Props = Readonly<{ + className?: string; action: OrgSchemaAction; onClick?: (action: OrgSchemaAction) => void; pressed: boolean; }>; -const FeedbackVoteButton = memo(({ action, onClick, pressed }: Props) => { +const FeedbackVoteButton = memo(({ action, className, onClick, pressed }: Props) => { const onClickRef = useRefFrom(onClick); const voteActionRef = useRefFrom(action); @@ -28,7 +29,7 @@ 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..cdc9ef181f 100644 --- a/packages/component/src/ActivityStatus/OthersActivityStatus.tsx +++ b/packages/component/src/ActivityStatus/OthersActivityStatus.tsx @@ -1,6 +1,5 @@ import { getOrgSchemaMessage, - OrgSchemaAction, OrgSchemaProject, parseAction, parseClaim, @@ -8,14 +7,14 @@ 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 }>; @@ -56,38 +55,14 @@ 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 && } + + + +
); }); diff --git a/packages/component/src/ActivityStatus/SelfActivityStatus.tsx b/packages/component/src/ActivityStatus/SelfActivityStatus.tsx index 4b2b51b8d2..632fdb6134 100644 --- a/packages/component/src/ActivityStatus/SelfActivityStatus.tsx +++ b/packages/component/src/ActivityStatus/SelfActivityStatus.tsx @@ -2,7 +2,6 @@ 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'; @@ -13,9 +12,9 @@ const SelftActivityStatus = memo(({ activity }: Props) => { 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..8252c6e28e --- /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..7676cadf43 100644 --- a/packages/component/src/Attachment/Text/private/MarkdownTextContent.tsx +++ b/packages/component/src/Attachment/Text/private/MarkdownTextContent.tsx @@ -25,6 +25,7 @@ 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; @@ -244,6 +245,7 @@ const MarkdownTextContent = memo(({ activity, children, markdown }: Props) => { {activity.type === 'message' && activity.text && messageThing?.keywords?.includes('AllowCopy') ? ( ) : null} + ); diff --git a/packages/component/src/Styles/StyleSet/SendStatus.ts b/packages/component/src/Styles/StyleSet/SendStatus.ts index 7d585747a9..4373f54c64 100644 --- a/packages/component/src/Styles/StyleSet/SendStatus.ts +++ b/packages/component/src/Styles/StyleSet/SendStatus.ts @@ -14,6 +14,17 @@ export default function createSendStatusStyle() { gap: 4 }, + '& .webchat__activity-status-slot': { + '&:not(:first-child)::before': { + content: `"|"`, + marginRight: '4px', + display: 'inline-block' + }, + '&:empty': { + display: 'none' + } + }, + '& .webchat__activity-status__originator': { alignItems: 'center', diff --git a/packages/component/src/Styles/StyleSet/ThumbButton.ts b/packages/component/src/Styles/StyleSet/ThumbButton.ts index 7f5eba727e..d90722bc76 100644 --- a/packages/component/src/Styles/StyleSet/ThumbButton.ts +++ b/packages/component/src/Styles/StyleSet/ThumbButton.ts @@ -7,24 +7,48 @@ export default function () { background: 'Transparent', border: 0, borderRadius: 2, + boxSizing: 'content-box', height: 16, + fontSize: '14px', /* 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', + overflow: ['hidden', 'clip'], padding: 0, width: 16, + '&.webchat__thumb-button--large': { + alignItems: 'center', + border: '1px solid transparent', + borderRadius: '4px', + display: 'flex', + fontSize: '20px', + height: '20px', + justifyContent: 'center', + padding: '5px', + width: '20px', + + '& .webchat__thumb-button__image': { + color: 'currentColor' + }, + + '&:hover, &:active, &.webchat__thumb-button--is-pressed': { + background: 'transparent', + color: CSSTokens.ColorAccent + } + }, + '&:active': { background: '#EDEBE9' }, - '&:focus': { + '&:focus-visible': { outline: 'solid 1px #605E5C' }, '& .webchat__thumb-button__image': { color: CSSTokens.ColorAccent, - width: 14 + width: '1em', + height: '1em' }, '&:hover .webchat__thumb-button__image:not(.webchat__thumb-button__image--is-filled)': { diff --git a/packages/fluent-theme/src/components/theme/Theme.module.css b/packages/fluent-theme/src/components/theme/Theme.module.css index 351edb8c2a..86878f3963 100644 --- a/packages/fluent-theme/src/components/theme/Theme.module.css +++ b/packages/fluent-theme/src/components/theme/Theme.module.css @@ -590,3 +590,12 @@ :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); + } +} diff --git a/packages/fluent-theme/src/private/FluentThemeProvider.tsx b/packages/fluent-theme/src/private/FluentThemeProvider.tsx index 7c1fcdfa6d..591926dffd 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 = { + feedbackActionsPlacement: 'activity-actions' +}; + const FluentThemeProvider = ({ children, variant = 'fluent' }: Props) => ( - + {children}