From bc3c00abb00c623482eedb6cb87f84dd846adbb7 Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Fri, 30 Oct 2026 09:50:51 -0700 Subject: [PATCH 01/16] Convert the tooltip to CSS Modules and style it in Fluent through the CSS module theme The tooltip keeps its webchat__tooltip class names through webChatPrefixedClassName. --- .../src/Tooltip/private/Tooltip.module.css | 67 +++++++++++++++++++ .../component/src/Tooltip/private/Tooltip.tsx | 33 ++++++--- .../src/components/theme/Theme.module.css | 2 +- 3 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 packages/component/src/Tooltip/private/Tooltip.module.css diff --git a/packages/component/src/Tooltip/private/Tooltip.module.css b/packages/component/src/Tooltip/private/Tooltip.module.css new file mode 100644 index 0000000000..402fedb3fc --- /dev/null +++ b/packages/component/src/Tooltip/private/Tooltip.module.css @@ -0,0 +1,67 @@ +/* The tooltip positions itself against its parent, which can be any element. The hashed class name scopes this rule. */ +:has(> .tooltip) { + position: relative; + + /* For - -
{children}
+
+
- - ); - } -); +
{children}
+ + + ); +} ModalDialog.displayName = 'ModalDialog'; -export default ModalDialog; +export default memo(ModalDialog); +export { modalDialogPropsSchema, type ModalDialogProps }; diff --git a/packages/fluent-theme/src/components/theme/Theme.module.css b/packages/fluent-theme/src/components/theme/Theme.module.css index d21e6f474b..c0e9344c1a 100644 --- a/packages/fluent-theme/src/components/theme/Theme.module.css +++ b/packages/fluent-theme/src/components/theme/Theme.module.css @@ -187,129 +187,13 @@ --webchat-curveDecelerateMid: var(--curveDecelerateMid, cubic-bezier(0, 0, 0, 1)); } +.variant-copilot { + --webchat-preserve-reason: target-class; +} + @media (prefers-reduced-motion) { :global(.webchat-fluent).theme { --webchat-durationUltraFast: 0.01ms; --webchat-durationNormal: 0.01ms; } } - -/* Style Options override */ -:global(.webchat-fluent).theme :global(.webchat__css-custom-properties) { - --webchat__color--accent: var(--webchat-colorBrandForeground1); - --webchat__color--subtle: var(--webchat-colorNeutralForeground2); - --webchat__color--timestamp: var(--webchat-colorNeutralForeground3); - --webchat__font--primary: var(--webchat-fontFamilyBase); - --webchat__font-size--small: var(--webchat-fontSizeBase200); - --webchat__font-size--medium: var(--webchat-fontSizeBase300); - --webchat__line-height--small: var(--webchat-lineHeightBase200); - --webchat__line-height--medium: var(--webchat-lineHeightBase300); - --webchat__max-width--bubble: var(--webchat-bubble-maxWidth); - --webchat__max-width--attachment-bubble: var(--webchat-bubble-maxWidth); - --webchat__min-height--bubble: var(--webchat-bubble-minHeight); - --webchat__padding--regular: var(--webchat-spacingVerticalS); - --webchat__padding--sendbox: var(--webchat-spacingVerticalNone) var(--webchat-spacingHorizontalMNudge) - var(--webchat-spacingHorizontalMNudge); - --webchat__transition-duration: var(--webchat-durationNormal); - --webchat__transition-easing: var(--webchat-curveDecelerateMid); - --webchat__border-animation--color-1: #464feb; - --webchat__border-animation--color-2: #47cffa; - --webchat__border-animation--color-3: #b47cf8; -} - -:global(.webchat-fluent).theme.variant-copilot :global(.webchat__css-custom-properties) { - --webchat__padding--sendbox: var(--webchat-spacingVerticalNone) var(--webchat-spacingHorizontalL) - var(--webchat-spacingVerticalL) var(--webchat-spacingHorizontalSNudge); - --webchat__max-height--image-bubble: max(480px, 90cqb); -} - -/* Transcript surface */ -:global(.webchat-fluent).theme :global(.webchat__surface) { - background-color: var(--webchat__color--surface); -} - -:global(.webchat-fluent).theme.variant-copilot :global(.webchat__surface) { - box-sizing: border-box; - padding-inline-start: var(--webchat-spacingHorizontalMNudge); -} - -:global(.webchat-fluent).theme.variant-copilot - :global(.webchat__basic-transcript .webchat__basic-transcript__scrollable) { - scrollbar-gutter: stable; -} - -/* Transcript filer in copilot variant */ -:global(.webchat-fluent).theme.variant-copilot :global(.webchat__basic-transcript .webchat__basic-transcript__filler) { - /* No filler unless pre-chat activity, see next rule */ - flex-grow: 0; -} - -/* Transcript with a single pre-chat activity */ -:global(.webchat-fluent).theme - :global( - .webchat__basic-transcript:has(.part-grouping-decorator:only-child):has(.pre-chat-message-activity) - .webchat__basic-transcript__filler - ) { - /* limit grow to half of the size to center the activity */ - flex-grow: 0.5; -} - -/* Modal */ -:global(.webchat-fluent).theme :global(.webchat__modal-dialog) { - height: 100%; - display: grid; - place-items: center; - - &::backdrop { - background-color: var(--webchat-colorBackgroundOverlay); - } - - :global(.webchat__modal-dialog__box) { - background-color: var(--webchat-colorNeutralBackground1); - border-radius: var(--webchat-borderRadiusXLarge); - position: relative; - color: var(--webchat-colorNeutralForeground1); - box-shadow: var(--webchat-shadow64); - } - - :global(.webchat__modal-dialog__close-button) { - color: var(--webchat-colorNeutralForeground1); - transition: background-color var(--webchat-durationNormal) var(--webchat-curveDecelerateMid); - } - - /* TODO: align with original fluent modal styles in WebChat as they don't match v9 anymore */ - :global(.webchat__modal-dialog__close-button):not(:active):hover { - background-color: var(--webchat-colorNeutralBackground3); - } - - :global(.webchat__modal-dialog__close-button):focus:active { - background-color: var(--webchat-colorNeutralBackground4); - } - - :global(.webchat__modal-dialog__close-button):not(:focus-visible) { - border: none; - } - - :global(.webchat__modal-dialog__close-button):focus { - border-color: var(--webchat-colorStrokeFocus2); - } - - :global(.webchat__modal-dialog__close-button-icon) { - color: currentColor; - } -} - -/* Tooltip */ -:global(.webchat-fluent).theme .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/fluentCSSModuleTheme.module.css b/packages/fluent-theme/src/private/fluentCSSModuleTheme.module.css index f7b4e3775d..1d8eeda5f0 100644 --- a/packages/fluent-theme/src/private/fluentCSSModuleTheme.module.css +++ b/packages/fluent-theme/src/private/fluentCSSModuleTheme.module.css @@ -29,23 +29,23 @@ } } - .stacked-layout .bubble :global(.webchat__fileContent__badge) { + .stacked-layout .bubble .fileContent__badge { cursor: default; font-size: var(--webchat-fontSizeBase300); line-height: var(--webchat-lineHeightBase300); - :global(.webchat__fileContent__fileName) { + .fileContent__fileName { color: var(--webchat-colorBrandForegroundLink); font-family: inherit; } - :global(.webchat__fileContent__size) { + .fileContent__size { color: var(--webchat-colorNeutralForeground2); font-family: inherit; } } - .stacked-layout .bubble :global(.webchat__fileContent__downloadIcon) { + .stacked-layout .bubble .fileContent__downloadIcon { color: var(--webchat-colorBrandForegroundLink); } /* #endregion */ @@ -973,11 +973,50 @@ /* #region Theme */ +/* Style Options override */ +:global(.webchat-fluent).theme .css-custom-properties { + --webchat__color--accent: var(--webchat-colorBrandForeground1); + --webchat__color--subtle: var(--webchat-colorNeutralForeground2); + --webchat__color--timestamp: var(--webchat-colorNeutralForeground3); + --webchat__font--primary: var(--webchat-fontFamilyBase); + --webchat__font-size--small: var(--webchat-fontSizeBase200); + --webchat__font-size--medium: var(--webchat-fontSizeBase300); + --webchat__line-height--small: var(--webchat-lineHeightBase200); + --webchat__line-height--medium: var(--webchat-lineHeightBase300); + --webchat__max-width--bubble: var(--webchat-bubble-maxWidth); + --webchat__max-width--attachment-bubble: var(--webchat-bubble-maxWidth); + --webchat__min-height--bubble: var(--webchat-bubble-minHeight); + --webchat__padding--regular: var(--webchat-spacingVerticalS); + --webchat__padding--sendbox: var(--webchat-spacingVerticalNone) var(--webchat-spacingHorizontalMNudge) + var(--webchat-spacingHorizontalMNudge); + --webchat__transition-duration: var(--webchat-durationNormal); + --webchat__transition-easing: var(--webchat-curveDecelerateMid); + --webchat__border-animation--color-1: #464feb; + --webchat__border-animation--color-2: #47cffa; + --webchat__border-animation--color-3: #b47cf8; +} + +:global(.webchat-fluent).theme.variant-copilot .css-custom-properties { + --webchat__padding--sendbox: var(--webchat-spacingVerticalNone) var(--webchat-spacingHorizontalL) + var(--webchat-spacingVerticalL) var(--webchat-spacingHorizontalSNudge); + --webchat__max-height--image-bubble: max(480px, 90cqb); +} + +/* Transcript surface */ +:global(.webchat-fluent).theme .surface { + background-color: var(--webchat__color--surface); +} + +:global(.webchat-fluent).theme.variant-copilot .surface { + box-sizing: border-box; + padding-inline-start: var(--webchat-spacingHorizontalMNudge); +} + /* Scrollbars */ :global(.webchat-fluent).theme .code-block-content .code-block-content__code-block, :global(.webchat-fluent).theme .send-box-attachment-bar.send-box-attachment-bar--as-list-item, :global(.webchat-fluent).theme .text-area.text-area--scroll, -:global(.webchat-fluent).theme :global(.webchat__basic-transcript .webchat__basic-transcript__scrollable), +:global(.webchat-fluent).theme .basic-transcript .basic-transcript__scrollable, :global(.webchat-fluent).theme .render-markdown [data-math-type='block'], :global(.webchat-fluent).theme .view-code-dialog__body, :global(.webchat-fluent).theme :global(.view-code-dialog__code-body) { @@ -1029,6 +1068,10 @@ --webchat__transcript--spacing: var(--webchat-spacingVerticalM); } +:global(.webchat-fluent).theme.variant-copilot .basic-transcript .basic-transcript__scrollable { + scrollbar-gutter: stable; +} + /* Transcript focus indicator color */ :global(.webchat-fluent).theme .transcript-focus-area__transcript-indicator { --webchat__color--transcript-visual-keyboard-indicator: var(--webchat-colorStrokeFocus2); @@ -1056,7 +1099,7 @@ } /* Transcript activity focused directly */ - > :global(.webchat__focus-trap) > :global(.webchat__basic-transcript__activity-body) .bubble { + > .focus-trap > .basic-transcript__activity-body .bubble { display: grid; grid-template-areas: 'focused-content'; position: static; @@ -1124,6 +1167,65 @@ } } +/* Transcript filer in copilot variant */ +:global(.webchat-fluent).theme.variant-copilot .basic-transcript .basic-transcript__filler { + /* No filler unless pre-chat activity, see next rule */ + flex-grow: 0; +} + +/* Transcript with a single pre-chat activity */ +:global(.webchat-fluent).theme + .basic-transcript:has(:global(.part-grouping-decorator):only-child):has(:global(.pre-chat-message-activity)) + .basic-transcript__filler { + /* limit grow to half of the size to center the activity */ + flex-grow: 0.5; +} + +/* Modal */ +:global(.webchat-fluent).theme .modal-dialog { + height: 100%; + display: grid; + place-items: center; + + &::backdrop { + background-color: var(--webchat-colorBackgroundOverlay); + } + + .modal-dialog__box { + background-color: var(--webchat-colorNeutralBackground1); + border-radius: var(--webchat-borderRadiusXLarge); + position: relative; + color: var(--webchat-colorNeutralForeground1); + box-shadow: var(--webchat-shadow64); + } + + .modal-dialog__close-button { + color: var(--webchat-colorNeutralForeground1); + transition: background-color var(--webchat-durationNormal) var(--webchat-curveDecelerateMid); + } + + /* TODO: align with original fluent modal styles in WebChat as they don't match v9 anymore */ + .modal-dialog__close-button:not(:active):hover { + background-color: var(--webchat-colorNeutralBackground3); + } + + .modal-dialog__close-button:focus:active { + background-color: var(--webchat-colorNeutralBackground4); + } + + .modal-dialog__close-button:not(:focus-visible) { + border: none; + } + + .modal-dialog__close-button:focus { + border-color: var(--webchat-colorStrokeFocus2); + } + + .modal-dialog__close-button-icon { + color: currentColor; + } +} + /* Code block */ :global(.webchat-fluent).theme .code-block { border: none; @@ -1205,13 +1307,13 @@ } /* View Code dialog */ -:global(.webchat-fluent).theme :global(.webchat__modal-dialog).view-code-dialog { - :global(.webchat__modal-dialog__close-button-layout) { +:global(.webchat-fluent).theme .modal-dialog.view-code-dialog { + .modal-dialog__close-button-layout { display: flex; padding: var(--webchat-spacingVerticalS) var(--webchat-spacingHorizontalS); } - :global(.webchat__modal-dialog__close-button) { + .modal-dialog__close-button { align-items: center; aspect-ratio: 1; display: flex; @@ -1231,11 +1333,11 @@ top: 0; } - :global(.webchat__modal-dialog__box) { + .modal-dialog__box { width: fit-content; } - :global(.webchat__modal-dialog__body) { + .modal-dialog__body { margin: 0; padding: var(--webchat-spacingVerticalS) var(--webchat-spacingHorizontalM); } @@ -1355,6 +1457,21 @@ } } +/* Tooltip */ +:global(.webchat-fluent).theme .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); +} + /* Feedback Form */ :global(.webchat-fluent).theme .feedback-form { .feedback-form__form-header { From ddf6762761bda90704009bca62d6469ab70ca0fc Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Fri, 30 Oct 2026 09:54:49 -0700 Subject: [PATCH 03/16] Convert the file attachment classes to CSS Modules and style them in Fluent through the CSS module theme FileContent's local styles moved to FileContent.module.css; its class names keep the webchat__ prefix, so the option-driven "fileContent" style set entry still applies the style options and nothing renders differently. --- .../src/Attachment/FileContent.module.css | 23 +++++++++ .../component/src/Attachment/FileContent.tsx | 47 ++++++++----------- 2 files changed, 42 insertions(+), 28 deletions(-) create mode 100644 packages/component/src/Attachment/FileContent.module.css diff --git a/packages/component/src/Attachment/FileContent.module.css b/packages/component/src/Attachment/FileContent.module.css new file mode 100644 index 0000000000..305e66aff3 --- /dev/null +++ b/packages/component/src/Attachment/FileContent.module.css @@ -0,0 +1,23 @@ +/* Style options apply through the "fileContent" style set entry, which selects the "webchat__fileContent" class names. */ +:global(.webchat) .fileContent { + display: flex; + + .fileContent__buttonLink { + display: flex; + flex: 1; + } + + .fileContent__badge { + display: flex; + flex: 1; + flex-direction: column; + } + + .fileContent__downloadIcon:not(*), + .fileContent__fileName:not(*), + .fileContent__size:not(*) { + /* No styles of their own here: the rule declares the local names, so useStyles() returns them and themes can style them. */ + /* Lightning CSS does not export local names of empty rules, and ":not(*)" never matches. */ + display: none; + } +} diff --git a/packages/component/src/Attachment/FileContent.tsx b/packages/component/src/Attachment/FileContent.tsx index 9fefdb737a..a9443c9dc8 100644 --- a/packages/component/src/Attachment/FileContent.tsx +++ b/packages/component/src/Attachment/FileContent.tsx @@ -1,29 +1,16 @@ import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; +import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react'; import { hooks } from 'botframework-webchat-api'; -import classNames from 'classnames'; -import React, { memo } from 'react'; +import cx from 'classnames'; +import React, { Fragment, memo } from 'react'; import { boolean, number, object, optional, pipe, readonly, string, type InferInput } from 'valibot'; -import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject'; import useStyleSet from '../hooks/useStyleSet'; import { ComponentIcon } from '../Icon'; -const { useByteFormatter, useDirection, useLocalizer } = hooks; - -const ROOT_STYLE = { - display: 'flex', - - '& .webchat__fileContent__buttonLink': { - display: 'flex', - flex: 1 - }, +import styles from './FileContent.module.css'; - '& .webchat__fileContent__badge': { - display: 'flex', - flex: 1, - flexDirection: 'column' - } -}; +const { useByteFormatter, useDirection, useLocalizer } = hooks; // TODO: Consider using `useSanitizeHrefCallback`, which underlying use `sanitize-html` or whatever in HTML content transformer. const ALLOWED_PROTOCOLS = ['blob:', 'data:', 'http:', 'https:']; @@ -50,28 +37,31 @@ type FileContentBadgeProps = InferInput; const FileContentBadge = (props: FileContentBadgeProps) => { const { downloadIcon = false, fileName, size } = validateProps(fileContentBadgePropsSchema, props); + // Keeps the "webchat__fileContent" class names. + const classNames = useStyles(styles, webChatPrefixedClassName); const [direction] = useDirection(); const formatByte = useByteFormatter(); const localizedSize = typeof size === 'number' && formatByte(size); return ( - -
-
{fileName}
- {!!localizedSize &&
{localizedSize}
} + +
+
{fileName}
+ {!!localizedSize &&
{localizedSize}
}
{downloadIcon && ( )} - +
); }; @@ -90,10 +80,11 @@ const fileContentPropsSchema = pipe( function FileContent(props: FileContentProps) { const { className, href, fileName, size } = validateProps(fileContentPropsSchema, props); + // Keeps the "webchat__fileContent" class names. + const classNames = useStyles(styles, webChatPrefixedClassName); const [{ fileContent: fileContentStyleSet }] = useStyleSet(); const localize = useLocalizer(); const localizeBytes = useByteFormatter(); - const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + ''; const localizedSize = typeof size === 'number' && localizeBytes(size); @@ -112,13 +103,13 @@ function FileContent(props: FileContentProps) { ); return ( -
+
{sanitizedHref ? ( // URL is sanitized. // eslint-disable-next-line react/forbid-elements Date: Fri, 30 Oct 2026 09:57:48 -0700 Subject: [PATCH 04/16] Convert the transcript classes that Fluent styles to CSS Modules and style them through the CSS module theme basic-transcript, __scrollable, __filler (BasicTranscript local styles), __activity-body (ActivityRow) and focus-trap (FocusTrap) come from CSS modules and keep their webchat__ class names, which code and tests still query. --- .../component/src/BasicTranscript.module.css | 27 +++++++++++ packages/component/src/BasicTranscript.tsx | 46 ++++--------------- .../src/Transcript/ActivityRow.module.css | 5 ++ .../component/src/Transcript/ActivityRow.tsx | 9 +++- .../src/Transcript/FocusTrap.module.css | 5 ++ .../component/src/Transcript/FocusTrap.tsx | 7 ++- 6 files changed, 60 insertions(+), 39 deletions(-) create mode 100644 packages/component/src/BasicTranscript.module.css create mode 100644 packages/component/src/Transcript/ActivityRow.module.css create mode 100644 packages/component/src/Transcript/FocusTrap.module.css diff --git a/packages/component/src/BasicTranscript.module.css b/packages/component/src/BasicTranscript.module.css new file mode 100644 index 0000000000..1ae315438d --- /dev/null +++ b/packages/component/src/BasicTranscript.module.css @@ -0,0 +1,27 @@ +:global(.webchat) .basic-transcript { + /* The element is also the transcript focus area, which TranscriptFocus.module.css lays out as a grid. */ + overflow: hidden; + /* Make sure to set "position: relative" here to form another stacking context for the scroll-to-end button. */ + /* Stacking context help isolating elements that use "z-index" from global pollution. */ + /* https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context */ + position: relative; + + .basic-transcript__filler { + flex: 1; + } + + .basic-transcript__scrollable { + display: flex; + flex-direction: column; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + } + + :global(.webchat__basic-transcript__transcript) { + list-style-type: none; + } + + :global(.webchat__basic-transcript__activity-focus-target) { + pointer-events: none; + } +} diff --git a/packages/component/src/BasicTranscript.tsx b/packages/component/src/BasicTranscript.tsx index 763cc6ab73..126b0e2360 100644 --- a/packages/component/src/BasicTranscript.tsx +++ b/packages/component/src/BasicTranscript.tsx @@ -1,6 +1,7 @@ // TODO: [P2] Fix ESLint error `no-use-before-define` /* eslint-disable @typescript-eslint/no-use-before-define */ +import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react'; import { hooks } from 'botframework-webchat-api'; import classNames from 'classnames'; import React, { @@ -37,7 +38,6 @@ import { type ActivityElementMap } from './Transcript/types'; import FocusRedirector from './Utils/FocusRedirector'; import inputtableKey from './Utils/TypeFocusSink/inputtableKey'; import { android } from './Utils/detectBrowser'; -import { useStyleToEmotionObject } from './hooks/internal/styleToEmotionObject'; import useDispatchScrollPosition from './hooks/internal/useDispatchScrollPosition'; import useDispatchTranscriptFocusByActivityKey from './hooks/internal/useDispatchTranscriptFocusByActivityKey'; import useNonce from './hooks/internal/useNonce'; @@ -65,6 +65,8 @@ import useFocusRelativeActivity from './providers/TranscriptFocus/useFocusRelati import useFocusedExplicitly from './providers/TranscriptFocus/useFocusedExplicitly'; import useFocusedKey from './providers/TranscriptFocus/useFocusedKey'; +import styles from './BasicTranscript.module.css'; + const { useActivityKeys, useDirection, @@ -77,37 +79,6 @@ const { useStyleOptions } = hooks; -const ROOT_STYLE = { - '&.webchat__basic-transcript': { - display: 'flex', - flexDirection: 'column', - overflow: 'hidden', - // Make sure to set "position: relative" here to form another stacking context for the scroll-to-end button. - // Stacking context help isolating elements that use "z-index" from global pollution. - // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context - position: 'relative', - - '& .webchat__basic-transcript__filler': { - flex: 1 - }, - - '& .webchat__basic-transcript__scrollable': { - display: 'flex', - flexDirection: 'column', - overflowX: 'hidden', - WebkitOverflowScrolling: 'touch' - }, - - '& .webchat__basic-transcript__transcript': { - listStyleType: 'none' - }, - - '& .webchat__basic-transcript__activity-focus-target': { - pointerEvents: 'none' - } - } -}; - type ScrollBehavior = 'auto' | 'smooth'; type ScrollToOptions = { behavior?: ScrollBehavior }; type ScrollToPosition = { activityID?: string; scrollTop?: number }; @@ -135,8 +106,9 @@ const InternalTranscript = memo( const getActivityByKey = useGetActivityByKey(); const getKeyByActivityId = useGetKeyByActivityId(); const localize = useLocalizer(); - const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + ''; const rootElementRef = useRef(null); + // Keeps the "webchat__basic-transcript" class names. + const transcriptClassNames = useStyles(styles, webChatPrefixedClassName); const focusedKeyRef = useValueRef(focusedKey); const transcriptAriaLabel = localize('TRANSCRIPT_ARIA_LABEL_ALT'); @@ -444,7 +416,7 @@ const InternalTranscript = memo( // We are disabling `aria-activedescendant` for Android. See for details. aria-activedescendant={android ? undefined : activeDescendantId} aria-label={transcriptAriaLabel} - className={classNames('webchat__basic-transcript', rootClassName, (className || '') + '')} + className={classNames(transcriptClassNames['basic-transcript'], (className || '') + '')} dir={direction} onFocus={handleFocus} onKeyDown={handleTranscriptKeyDown} @@ -488,6 +460,8 @@ const InternalTranscriptScrollable = ({ children, onFocusFiller }: InternalTrans const [sticky]: [boolean] = useSticky(); const localize = useLocalizer(); const markAllAsAcknowledged = useMarkAllAsAcknowledged(); + // Keeps the "webchat__basic-transcript" class names. + const transcriptClassNames = useStyles(styles, webChatPrefixedClassName); const prevSticky = usePrevious(sticky); const transcriptRoleDescription = localize('TRANSCRIPT_ARIA_ROLE_ALT'); @@ -505,8 +479,8 @@ const InternalTranscriptScrollable = ({ children, onFocusFiller }: InternalTrans return ( - -
+ +
{hasAnyChild && ( (({ activity, child const [readActivityKeys] = useActivityKeysByRead(); const activityRef = useRefFrom(activity); const bodyRef = useRef(); + // Keeps the "webchat__basic-transcript__activity-body" class name. + const activityRowClassNames = useStyles(styles, webChatPrefixedClassName); const focusByActivityKey = useFocusByActivityKey(); const getKeyByActivity = useGetKeyByActivity(); // TODO: [P2] #2858 We should use core/definitions/speakingActivity for this predicate instead @@ -66,11 +71,11 @@ const ActivityRow = forwardRef(({ activity, child const focusTrapChildren = useMemo( () => ( -
+
{children}
), - [bodyRef, children] + [activityRowClassNames, bodyRef, children] ); const activityIdRef = useRefFrom(activity.id); diff --git a/packages/component/src/Transcript/FocusTrap.module.css b/packages/component/src/Transcript/FocusTrap.module.css new file mode 100644 index 0000000000..3395fc0583 --- /dev/null +++ b/packages/component/src/Transcript/FocusTrap.module.css @@ -0,0 +1,5 @@ +/* No styles of its own: the rule declares the local name, so useStyles() returns it and themes can style it. */ +/* Lightning CSS does not export local names of empty rules, and ":not(*)" never matches. */ +:global(.webchat) .focus-trap:not(*) { + display: none; +} diff --git a/packages/component/src/Transcript/FocusTrap.tsx b/packages/component/src/Transcript/FocusTrap.tsx index 2740af2158..3d4d4abcc2 100644 --- a/packages/component/src/Transcript/FocusTrap.tsx +++ b/packages/component/src/Transcript/FocusTrap.tsx @@ -1,4 +1,5 @@ /* eslint-disable no-magic-numbers */ +import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react'; import React, { Fragment, memo, @@ -12,6 +13,8 @@ import React, { import tabbableElements from '../Utils/tabbableElements'; import { useRefFrom } from 'use-ref-from'; +import styles from './FocusTrap.module.css'; + // Media elements with native controls (`