Skip to content
Open
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/6024-fix-css-modules-option-free-styles-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Changed

- Converted the chat history box, image avatar, error box, transcript list and media attachments to CSS Modules with `webchat__` class names, and `videoHeight` to `--webchat__height--video`, in PR [#6024](https://github.com/microsoft/BotFramework-WebChat/pull/6024), by [@OEvgeny](https://github.com/OEvgeny)

### Removed

- 💥 Deprecated the `activities`, `audioAttachment`, `audioContent`, `chatHistoryBox`, `errorBox`, `imageAvatar`, `videoAttachment`, `videoContent`, `vimeoContent` and `youTubeContent` style set entries, which no longer affect rendering, and removed the unused `webchat__chat-history-box__toolbar--rtl` class, in PR [#6024](https://github.com/microsoft/BotFramework-WebChat/pull/6024), by [@OEvgeny](https://github.com/OEvgeny)
61 changes: 61 additions & 0 deletions __tests__/html2/rtl/scrollToEndButton.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
</head>
<body>
<main id="webchat"></main>
<script type="importmap">
{
"imports": {
"botframework-webchat": "/__dist__/packages/bundle/static/botframework-webchat.js",
"react": "/__dist__/packages/bundle/static/react.js",
"react-dom": "/__dist__/packages/bundle/static/react-dom.js"
}
}
</script>
<script type="module">
import '/test-harness.mjs';
import '/test-page-object.mjs';

import { createStoreWithOptions, renderWebChat } from 'botframework-webchat';

// TODO: This is for `createDirectLineEmulator` only, should find ways to eliminate this line.
window.WebChat = { createStoreWithOptions };

run(async function () {
const { directLine, store } = testHelpers.createDirectLineEmulator();

// GIVEN: Web Chat in Hebrew (right-to-left) on a left-to-right page.
renderWebChat(
{ directLine, locale: 'he-IL', store, styleOptions: { hideSendBox: true } },
document.getElementById('webchat')
);

await pageConditions.uiConnected();

// GIVEN: Enough messages to scroll the transcript.
for (let index = 1; index <= 20; index++) {
// eslint-disable-next-line no-await-in-loop
await directLine.emulateIncomingActivity({ from: { role: 'bot' }, text: `הודעה ${index}`, type: 'message' });
}

await pageConditions.numActivitiesShown(20);
await pageConditions.scrollToBottomCompleted();

// WHEN: The user scrolls to the top and a new message arrives.
await pageObjects.scrollToTop();
await directLine.emulateIncomingActivity({ from: { role: 'bot' }, text: 'הודעה חדשה', type: 'message' });

// THEN: The "New messages" button should show, laid out right-to-left.
await pageConditions.scrollToEndButtonShown();
await pageConditions.scrollStabilized(0);

expect(pageElements.scrollToEndButton().matches(':dir(rtl)')).toBe(true);

// THEN: The button should be at the bottom-left corner of the transcript, the end side in right-to-left.
await host.snapshot('local');
});
</script>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 59 additions & 0 deletions __tests__/html2/styleOptions/videoHeight.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
</head>
<body>
<main id="webchat"></main>
<script type="importmap">
{
"imports": {
"botframework-webchat": "/__dist__/packages/bundle/static/botframework-webchat.js",
"react": "/__dist__/packages/bundle/static/react.js",
"react-dom": "/__dist__/packages/bundle/static/react-dom.js"
}
}
</script>
<script type="module">
import '/test-harness.mjs';
import '/test-page-object.mjs';

import { createStoreWithOptions, renderWebChat } from 'botframework-webchat';

// TODO: This is for `createDirectLineEmulator` only, should find ways to eliminate this line.
window.WebChat = { createStoreWithOptions };

run(async function () {
const { directLine, store } = testHelpers.createDirectLineEmulator();

// GIVEN: Web Chat with a video height of 135 pixels, half of the default 270 pixels.
renderWebChat(
{ directLine, store, styleOptions: { hideSendBox: true, videoHeight: 135 } },
document.getElementById('webchat')
);

await pageConditions.uiConnected();

// WHEN: The bot sends a video attachment.
await directLine.emulateIncomingActivity({
attachments: [
{
contentType: 'video/webm',
// `VideoContent` parses the URL with `new URL()`, so it must be absolute.
contentUrl: new URL('/assets/dark-gray-video.webm', location.href).href,
name: 'Dark gray video'
}
],
from: { role: 'bot' },
type: 'message'
});

await pageConditions.numActivitiesShown(1);
await pageConditions.mediaBufferStabilized(document.querySelector('video'));

// THEN: The video player should be 135 pixels tall.
await host.snapshot('local');
});
</script>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
display: block;
}

/* Same two-tone focus indicator as video attachments, see `Styles/StyleSet/VideoAttachment.ts` in the component package. */
/* Same two-tone focus indicator as video attachments, see `Attachment/MediaAttachment.module.css` in the component package. */
/* It follows the clipped top corners. When the bubble does not clip the video, the video is rounded like the indicator. */
> video:focus-visible {
border-radius: inherit;
Expand Down
8 changes: 5 additions & 3 deletions packages/component/src/Attachment/AudioAttachment.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react';
import React from 'react';
import { object, optional, pipe, readonly, string } from 'valibot';

import AudioContent from './AudioContent';
import useStyleSet from '../hooks/useStyleSet';

import styles from './MediaAttachment.module.css';

const audioAttachmentPropsSchema = pipe(
object({
Expand All @@ -21,10 +23,10 @@ const audioAttachmentPropsSchema = pipe(
const AudioAttachment = props => {
const { attachment } = validateProps(audioAttachmentPropsSchema, props);

const [{ audioAttachment: audioAttachmentStyleSet }] = useStyleSet();
const classNames = useStyles(styles, webChatPrefixedClassName);

return (
<div className={audioAttachmentStyleSet}>
<div className={classNames['audio-attachment']}>
<AudioContent alt={attachment.name} src={attachment.contentUrl} />
</div>
);
Expand Down
14 changes: 8 additions & 6 deletions packages/component/src/Attachment/AudioContent.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot';
import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react';
import React, { memo } from 'react';
import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot';
import { boolean, object, optional, pipe, readonly, string } from 'valibot';

import useStyleSet from '../hooks/useStyleSet';
import styles from './MediaAttachment.module.css';

const audioContentPropsSchema = pipe(
object({
Expand All @@ -15,17 +16,18 @@ const audioContentPropsSchema = pipe(
readonly()
);

type AudioContentProps = InferInput<typeof audioContentPropsSchema>;
type AudioContentProps = InferReactProps<typeof audioContentPropsSchema>;

function AudioContent(props: AudioContentProps) {
const { alt, autoPlay = false, loop = false, src } = validateProps(audioContentPropsSchema, props);
const [{ audioContent: audioContentStyleSet }] = useStyleSet();

const classNames = useStyles(styles, webChatPrefixedClassName);

return (
<audio
aria-label={alt}
autoPlay={autoPlay}
className={audioContentStyleSet}
className={classNames['audio-content']}
controls={true}
loop={loop}
src={src}
Expand Down
13 changes: 7 additions & 6 deletions packages/component/src/Attachment/HTMLVideoContent.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot';
import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react';
import React, { memo } from 'react';
import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot';
import { boolean, object, optional, pipe, readonly, string } from 'valibot';

import useStyleSet from '../hooks/useStyleSet';
import styles from './MediaAttachment.module.css';

const htmlVideoContentPropsSchema = pipe(
object({
Expand All @@ -15,18 +16,18 @@ const htmlVideoContentPropsSchema = pipe(
readonly()
);

type HTMLVideoContentProps = InferInput<typeof htmlVideoContentPropsSchema>;
type HTMLVideoContentProps = InferReactProps<typeof htmlVideoContentPropsSchema>;

function HTMLVideoContent(props: HTMLVideoContentProps) {
const { alt, autoPlay = false, loop = false, poster, src } = validateProps(htmlVideoContentPropsSchema, props);

const [{ videoContent: videoContentStyleSet }] = useStyleSet();
const classNames = useStyles(styles, webChatPrefixedClassName);

return (
<video
aria-label={alt}
autoPlay={autoPlay}
className={videoContentStyleSet}
className={classNames['video-content']}
controls={true}
loop={loop}
poster={poster}
Expand Down
74 changes: 74 additions & 0 deletions packages/component/src/Attachment/MediaAttachment.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/* #region Attachments */

:global(.webchat) .audio-attachment {
display: flex;
min-height: var(--webchat__size--avatar);
position: relative;

/* The browser draws the focus ring of <audio> outside of the player, where the bubble clips it, and under the player controls. */
/* Draw the focus indicator on top of the player instead, in the same pill shape as the player (Chromium uses `border-radius: 100px`). */
> audio:focus-visible {
outline: 0;
}

&:has(> audio:focus-visible)::after {
border-color: var(--webchat__color--transcript-visual-keyboard-indicator);
border-radius: 100px;
border-style: var(--webchat__border-style--transcript-visual-keyboard-indicator);
border-width: var(--webchat__border-width--transcript-visual-keyboard-indicator);
content: '';
inset: 0;
pointer-events: none;
position: absolute;
}
}

:global(.webchat) .video-attachment {
/* Same corners as the bubble, which clips the player. */
border-radius: inherit;
display: flex;
flex-direction: column;
position: relative;

/* The browser draws the focus ring of <video> outside of the player, where the bubble clips it. The rest is not visible over a dark video. */
/* Draw a two-tone focus indicator on top of the player instead: the ring, and a band in the bubble background color to separate it from the video. */
/* The indicator follows the bubble corners. When the bubble has padding and does not clip the player, the player is rounded like the indicator. */
> video:focus-visible {
border-radius: inherit;
outline: 0;
}

&:has(> video:focus-visible)::after {
border-color: var(--webchat__color--transcript-visual-keyboard-indicator);
border-radius: inherit;
border-style: var(--webchat__border-style--transcript-visual-keyboard-indicator);
border-width: var(--webchat__border-width--transcript-visual-keyboard-indicator);
box-shadow: inset 0 0 0 2px var(--webchat__background--bubble);
content: '';
inset: 0;
pointer-events: none;
position: absolute;
}
}

/* #endregion */

/* #region Players */

:global(.webchat) .audio-content {
width: 100%;
}

:global(.webchat) .video-content {
height: var(--webchat__height--video);
width: 100%;
}

:global(.webchat) .vimeo-content,
:global(.webchat) .youtube-content {
border: 0;
height: var(--webchat__height--video);
width: 100%;
}

/* #endregion */
15 changes: 5 additions & 10 deletions packages/component/src/Attachment/VideoAttachment.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
import classNames from 'classnames';
import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react';
import React from 'react';
import { object, optional, pipe, readonly, string } from 'valibot';

import useStyleSet from '../hooks/useStyleSet';
import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject';
import VideoContent from './VideoContent';

const ROOT_STYLE = {
display: 'flex',
flexDirection: 'column'
};
import styles from './MediaAttachment.module.css';

const videoAttachmentPropsSchema = pipe(
object({
Expand All @@ -27,11 +22,11 @@ const videoAttachmentPropsSchema = pipe(

const VideoAttachment = props => {
const { attachment } = validateProps(videoAttachmentPropsSchema, props);
const [{ videoAttachment: videoAttachmentStyleSet }] = useStyleSet();
const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + '';

const classNames = useStyles(styles, webChatPrefixedClassName);

return (
<div className={classNames(rootClassName, videoAttachmentStyleSet + '')}>
<div className={classNames['video-attachment']}>
<VideoContent alt={attachment.name} src={attachment.contentUrl} />
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions packages/component/src/Attachment/VideoContent.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ jest.mock('@msinternal/botframework-webchat-react-valibot', () => ({
validateProps: (_: unknown, props: unknown) => props
}));

// The video player needs the Web Chat context, it is not under test.
jest.mock('../hooks/useStyleSet', () => () => [{ videoContent: '' }]);
// Unit tests do not load CSS, styles are not under test.
jest.mock('./MediaAttachment.module.css', () => ({}));

test.each([
['root-relative', '/videos/intro.mp4'],
Expand Down
13 changes: 7 additions & 6 deletions packages/component/src/Attachment/VimeoContent.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot';
import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react';
import { hooks } from 'botframework-webchat-api';
import React, { memo } from 'react';
import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot';
import { boolean, object, optional, pipe, readonly, string } from 'valibot';

import useStyleSet from '../hooks/useStyleSet';
import styles from './MediaAttachment.module.css';

const { useLocalizer } = hooks;

Expand All @@ -17,12 +18,12 @@ const vimeoContentPropsSchema = pipe(
readonly()
);

type VimeoContentProps = InferInput<typeof vimeoContentPropsSchema>;
type VimeoContentProps = InferReactProps<typeof vimeoContentPropsSchema>;

function VimeoContent(props: VimeoContentProps) {
const { alt, autoPlay = false, embedID, loop = false } = validateProps(vimeoContentPropsSchema, props);

const [{ vimeoContent: vimeoContentStyleSet }] = useStyleSet();
const classNames = useStyles(styles, webChatPrefixedClassName);
const localize = useLocalizer();

const search = new URLSearchParams({
Expand All @@ -39,7 +40,7 @@ function VimeoContent(props: VimeoContentProps) {
<iframe
allowFullScreen={true}
aria-label={alt}
className={vimeoContentStyleSet}
className={classNames['vimeo-content']}
sandbox="allow-same-origin allow-scripts"
src={`https://player.vimeo.com/video/${encodeURI(embedID)}?${search}`}
title={title}
Expand Down
Loading
Loading