Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/5997-fix-css-fluent-context-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Changed

- Converted the tooltip, modal dialog, file attachment and transcript root classes to CSS Modules, keeping their `webchat__` class names, so Fluent styles them through the CSS module theme, in PR [#5997](https://github.com/microsoft/BotFramework-WebChat/pull/5997), by [@OEvgeny](https://github.com/OEvgeny)

### Removed

- 💥 Deprecated the `modalDialog` and `tooltip` style set entries, which no longer affect rendering, in PR [#5997](https://github.com/microsoft/BotFramework-WebChat/pull/5997), by [@OEvgeny](https://github.com/OEvgeny)
21 changes: 21 additions & 0 deletions packages/component/src/Attachment/FileContent.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* 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,
.fileContent__fileName,
.fileContent__size {
--webchat-preserve-reason: target-class;
}
}
47 changes: 19 additions & 28 deletions packages/component/src/Attachment/FileContent.tsx
Original file line number Diff line number Diff line change
@@ -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:'];
Expand All @@ -50,28 +37,31 @@ type FileContentBadgeProps = InferInput<typeof fileContentBadgePropsSchema>;
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 (
<React.Fragment>
<div aria-hidden={true} className="webchat__fileContent__badge">
<div className="webchat__fileContent__fileName">{fileName}</div>
{!!localizedSize && <div className="webchat__fileContent__size">{localizedSize}</div>}
<Fragment>
<div aria-hidden={true} className={classNames['fileContent__badge']}>
<div className={classNames['fileContent__fileName']}>{fileName}</div>
{!!localizedSize && <div className={classNames['fileContent__size']}>{localizedSize}</div>}
</div>
{downloadIcon && (
<ComponentIcon
appearance="text"
className={classNames(
'webchat__fileContent__downloadIcon',
className={cx(
classNames['fileContent__downloadIcon'],
// The "fileContent" style set entry, which applies the style options, selects this modifier.
direction === 'rtl' && 'webchat__fileContent__downloadIcon--rtl'
)}
icon="download"
/>
)}
</React.Fragment>
</Fragment>
);
};

Expand All @@ -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);

Expand All @@ -112,13 +103,13 @@ function FileContent(props: FileContentProps) {
);

return (
<div className={classNames('webchat__fileContent', rootClassName, fileContentStyleSet + '', className)}>
<div className={cx(classNames['fileContent'], fileContentStyleSet + '', className)}>
{sanitizedHref ? (
// URL is sanitized.
// eslint-disable-next-line react/forbid-elements
<a
aria-label={alt}
className="webchat__fileContent__buttonLink"
className={classNames['fileContent__buttonLink']}
download={fileName}
href={sanitizedHref}
rel="noopener noreferrer"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ActivityButton from './ActivityButton';
import CodeContent from './CodeContent';

import styles from './ViewCodeDialog.module.css';
import themeStyles from './ViewCodeDialogTheme.module.css';

const { useLocalizer } = hooks;

Expand Down Expand Up @@ -45,8 +46,9 @@ const ActivityViewCodeButton = (props: ActivityViewCodeButtonProps) => {
</CodeContent>
),
{
'aria-label': localize('ACTIVITY_CODE_ALT', title ?? ''),
className: classNames['view-code-dialog'],
'aria-label': localize('ACTIVITY_CODE_ALT', title ?? '')
cssModuleTheme: themeStyles
}
);
}, [code, isAIGenerated, language, localize, showModal, title, classNames]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,3 @@
:global(.webchat) .view-code-dialog {
box-sizing: border-box;
display: grid;
height: 100%;
max-height: 100vh;
overflow: hidden;
padding: 1rem;
place-items: center;
}

:global(.webchat) .view-code-dialog :global(.webchat__modal-dialog__box) {
display: flex;
flex-direction: column;
max-height: 100%;
max-width: 100%;
position: relative;
}

:global(.webchat) .view-code-dialog :global(.webchat__modal-dialog__close-button-layout) {
position: absolute;
right: 0;
}

:global(.webchat) .view-code-dialog :global(.webchat__modal-dialog__body) {
display: flex;
flex-direction: column;
gap: var(--webchat__padding--regular);
overflow: hidden;
}

:global(.webchat) .view-code-dialog__header {
align-items: center;
display: flex;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* The CSS module theme of the view code dialog: ActivityViewCodeButton passes it whole to showModal(), so the modal
dialog's parts get these class names, and only in this dialog. */
/* The modal dialog rules (Popover.module.css) can come later in the stylesheet: ":is(dialog)" out-specifies them. */
:global(.webchat) .modal-dialog:is(dialog) {
box-sizing: border-box;
display: grid;
height: 100%;
max-height: 100vh;
overflow: hidden;
padding: 1rem;
place-items: center;

.modal-dialog__box {
display: flex;
flex-direction: column;
max-height: 100%;
max-width: 100%;
position: relative;
}

.modal-dialog__close-button-layout {
position: absolute;
right: 0;
}

.modal-dialog__body {
display: flex;
flex-direction: column;
gap: var(--webchat__padding--regular);
overflow: hidden;
}
}
32 changes: 32 additions & 0 deletions packages/component/src/BasicTranscript.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
:global(.webchat) .basic-transcript {
/* The element is also the transcript focus area, which TranscriptFocus.module.css lays out as a grid. */

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe fold either of the module into another if it simplifies things

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5114be9: ActivityRow.module.css is folded into BasicTranscript.module.css (the class keeps its webchat__basic-transcript__activity-body name); FocusTrap.module.css stays separate because PartGroupingActivity also renders a focus trap.

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;
}

/* ActivityRow: no styles of its own, themes style it. */
.basic-transcript__activity-body {
--webchat-preserve-reason: target-class;
}

.basic-transcript__scrollable {
display: flex;
flex-direction: column;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}

.basic-transcript__transcript {
list-style-type: none;
}

.basic-transcript__activity-focus-target {
pointer-events: none;
}
}
48 changes: 11 additions & 37 deletions packages/component/src/BasicTranscript.tsx
Original file line number Diff line number Diff line change
@@ -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, {
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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 };
Expand Down Expand Up @@ -135,8 +106,9 @@ const InternalTranscript = memo(
const getActivityByKey = useGetActivityByKey();
const getKeyByActivityId = useGetKeyByActivityId();
const localize = useLocalizer();
const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + '';
const rootElementRef = useRef<HTMLDivElement>(null);
// Keeps the "webchat__basic-transcript" class names.
const transcriptClassNames = useStyles(styles, webChatPrefixedClassName);

const focusedKeyRef = useValueRef(focusedKey);
const transcriptAriaLabel = localize('TRANSCRIPT_ARIA_LABEL_ALT');
Expand Down Expand Up @@ -444,7 +416,7 @@ const InternalTranscript = memo(
// We are disabling `aria-activedescendant` for Android. See <ActivityRow> 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}
Expand Down Expand Up @@ -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');
Expand All @@ -505,12 +479,12 @@ const InternalTranscriptScrollable = ({ children, onFocusFiller }: InternalTrans

return (
<React.Fragment>
<ReactScrollToBottomPanel className="webchat__basic-transcript__scrollable">
<div aria-hidden={true} className="webchat__basic-transcript__filler" onFocus={onFocusFiller} />
<ReactScrollToBottomPanel className={transcriptClassNames['basic-transcript__scrollable']}>
<div aria-hidden={true} className={transcriptClassNames['basic-transcript__filler']} onFocus={onFocusFiller} />
{hasAnyChild && (
<TranscriptActivityList
aria-roledescription={transcriptRoleDescription}
className={classNames(activitiesStyleSet + '', 'webchat__basic-transcript__transcript')}
className={classNames(activitiesStyleSet + '', transcriptClassNames['basic-transcript__transcript'])}
role="feed"
>
{children}
Expand Down
4 changes: 4 additions & 0 deletions packages/component/src/BasicWebChat.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:global(.webchat) .surface {
display: flex;
flex-direction: column;
}
Loading
Loading