diff --git a/.changelog/6052-fix-css-modules-small-components.md b/.changelog/6052-fix-css-modules-small-components.md new file mode 100644 index 0000000000..ad66e2e40a --- /dev/null +++ b/.changelog/6052-fix-css-modules-small-components.md @@ -0,0 +1,3 @@ +### Changed + +- Converted the screen reader text, live region activity, fixed-width image and send failed retry link to CSS Modules with `webchat__` class names; in the Fluent theme, the send failed retry link now uses Fluent's accent color, in PR [#6052](https://github.com/microsoft/BotFramework-WebChat/pull/6052), by [@OEvgeny](https://github.com/OEvgeny) diff --git a/__tests__/html2/activityStatus/sendFailedRetry.accent.html b/__tests__/html2/activityStatus/sendFailedRetry.accent.html new file mode 100644 index 0000000000..ab7223d650 --- /dev/null +++ b/__tests__/html2/activityStatus/sendFailedRetry.accent.html @@ -0,0 +1,71 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/activityStatus/sendFailedRetry.accent.html.snap-1.png b/__tests__/html2/activityStatus/sendFailedRetry.accent.html.snap-1.png new file mode 100644 index 0000000000..8ab09bc90b Binary files /dev/null and b/__tests__/html2/activityStatus/sendFailedRetry.accent.html.snap-1.png differ diff --git a/__tests__/html2/activityStatus/sendFailedRetry.accent.html.snap-2.png b/__tests__/html2/activityStatus/sendFailedRetry.accent.html.snap-2.png new file mode 100644 index 0000000000..1b0e139ef9 Binary files /dev/null and b/__tests__/html2/activityStatus/sendFailedRetry.accent.html.snap-2.png differ diff --git a/packages/component/src/LiveRegion/LiveRegionActivity.module.css b/packages/component/src/LiveRegion/LiveRegionActivity.module.css new file mode 100644 index 0000000000..9fdb56c3e5 --- /dev/null +++ b/packages/component/src/LiveRegion/LiveRegionActivity.module.css @@ -0,0 +1,10 @@ +:global(.webchat) .live-region-activity { + color: transparent; + height: 1px; + opacity: 0; + overflow: hidden; + position: absolute; + top: 0; + white-space: nowrap; + width: 1px; +} diff --git a/packages/component/src/LiveRegion/LiveRegionActivity.tsx b/packages/component/src/LiveRegion/LiveRegionActivity.tsx index 2a0f235748..8115a85c92 100644 --- a/packages/component/src/LiveRegion/LiveRegionActivity.tsx +++ b/packages/component/src/LiveRegion/LiveRegionActivity.tsx @@ -1,30 +1,17 @@ /* eslint no-magic-numbers: ["error", { "ignore": [2] }] */ import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; -import classNames from 'classnames'; +import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react'; import React, { Fragment, useMemo } from 'react'; import { any, object, pipe, readonly } from 'valibot'; -import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject'; import useLocalizeActivityGreetingAlt from '../hooks/internal/useLocalizeActivityGreetingAlt.js'; import useRenderMarkdownAsHTML from '../hooks/useRenderMarkdownAsHTML'; import activityAltText from '../Utils/activityAltText'; +import styles from './LiveRegionActivity.module.css'; import LiveRegionAttachments from './private/LiveRegionAttachments'; import LiveRegionSuggestedActions from './private/LiveRegionSuggestedActions'; -const ROOT_STYLE = { - '&.webchat__live-region-activity': { - color: 'transparent', - height: 1, - opacity: 0, - overflow: 'hidden', - position: 'absolute', - top: 0, - whiteSpace: 'nowrap', - width: 1 - } -}; - const liveRegionActivityPropsSchema = pipe( object({ activity: any() @@ -41,8 +28,8 @@ function LiveRegionActivity(props: LiveRegionActivityProps) { const fallbackText: string | undefined = type === 'message' ? activity.channelData['webchat:fallback-text'] : undefined; const [greetingAlt] = useLocalizeActivityGreetingAlt()(activity); + const classNames = useStyles(styles, webChatPrefixedClassName); const renderMarkdownAsHTML = useRenderMarkdownAsHTML('accessible name'); - const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + ''; const textAlt = useMemo(() => activityAltText(activity, renderMarkdownAsHTML), [activity, renderMarkdownAsHTML]); const validFallbackText = fallbackText && typeof fallbackText === 'string'; @@ -50,7 +37,7 @@ function LiveRegionActivity(props: LiveRegionActivityProps) { // Lines are `role="none"`: on Windows, generic `
` and `

` are exposed as "group", screen readers would read "group" before each line. // The message itself stays in the accessibility tree, as required by `aria-atomic`. return ( -

+
{greetingAlt}
{validFallbackText ? (
{fallbackText}
diff --git a/packages/component/src/ScreenReaderText.module.css b/packages/component/src/ScreenReaderText.module.css new file mode 100644 index 0000000000..0fd0fd94e7 --- /dev/null +++ b/packages/component/src/ScreenReaderText.module.css @@ -0,0 +1,17 @@ +/* .sr-only - This component is intended to be invisible to the visual Web Chat user, but read by the AT when using a screen reader */ +:global(.webchat) .screen-reader-text { + color: transparent; + height: 1px; + overflow: hidden; + position: absolute; + /* + * We need to set top: 0, otherwise, it will repro: + * - Run NVDA + * - Make the transcript long enough to show the scrollbar + * - Press SHIFT-TAB, focus on upload button + * - Press up arrow multiple times + */ + top: 0; + white-space: nowrap; + width: 1px; +} diff --git a/packages/component/src/ScreenReaderText.tsx b/packages/component/src/ScreenReaderText.tsx index a1003392be..f722153a77 100644 --- a/packages/component/src/ScreenReaderText.tsx +++ b/packages/component/src/ScreenReaderText.tsx @@ -1,26 +1,11 @@ /* eslint react/forbid-dom-props: ["off"] */ import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; +import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react'; import React, { forwardRef, memo } from 'react'; - import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot'; -import { useStyleToEmotionObject } from './hooks/internal/styleToEmotionObject'; - -const ROOT_STYLE = { - // .sr-only - This component is intended to be invisible to the visual Web Chat user, but read by the AT when using a screen reader - color: 'transparent', - height: 1, - overflow: 'hidden', - position: 'absolute', - // We need to set top: 0, otherwise, it will repro: - // - Run NVDA - // - Make the transcript long enough to show the scrollbar - // - Press SHIFT-TAB, focus on upload button - // - Press up arrow multiple times - top: 0, - whiteSpace: 'nowrap', - width: 1 -}; + +import styles from './ScreenReaderText.module.css'; const screenReaderTextPropsSchema = pipe( object({ @@ -37,7 +22,7 @@ type ScreenReaderTextProps = InferInput; const ScreenReaderText = forwardRef(function ScreenReaderText(props, ref) { const { 'aria-hidden': ariaHidden, id, text } = validateProps(screenReaderTextPropsSchema, props); - const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + ''; + const classNames = useStyles(styles, webChatPrefixedClassName); if (ariaHidden && !id) { console.warn( @@ -46,7 +31,7 @@ const ScreenReaderText = forwardRef(funct } return ( -
+
{text}
); diff --git a/packages/component/src/Utils/FixedWidthImage.module.css b/packages/component/src/Utils/FixedWidthImage.module.css new file mode 100644 index 0000000000..c4a92ed6c1 --- /dev/null +++ b/packages/component/src/Utils/FixedWidthImage.module.css @@ -0,0 +1,22 @@ +:global(.webchat) .fixed-width-image { + display: flex; + justify-content: center; + max-height: var(--webchat__max-height--image-bubble); + min-height: var(--webchat__min-height--image-bubble); + overflow: hidden; + position: relative; + + .fixed-width-image__filler { + visibility: hidden; + width: 100%; + } + + .fixed-width-image__image { + left: 50%; + object-fit: cover; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 100%; + } +} diff --git a/packages/component/src/Utils/FixedWidthImage.tsx b/packages/component/src/Utils/FixedWidthImage.tsx index c3dbabffdb..43b70e2699 100644 --- a/packages/component/src/Utils/FixedWidthImage.tsx +++ b/packages/component/src/Utils/FixedWidthImage.tsx @@ -1,5 +1,6 @@ import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; -import classNames from 'classnames'; +import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react'; +import cx from 'classnames'; import React, { memo, type SyntheticEvent } from 'react'; import { args, @@ -17,33 +18,7 @@ import { void_ } from 'valibot'; -import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject'; -import CSSTokens from '../Styles/CSSTokens'; - -const ROOT_STYLE = { - '&.webchat__fixed-width-image': { - display: 'flex', - justifyContent: 'center', - maxHeight: CSSTokens.MaxHeightImageBubble, - minHeight: CSSTokens.MinHeightImageBubble, - overflow: 'hidden', - position: 'relative', - - '& .webchat__fixed-width-image__filler': { - visibility: 'hidden', - width: '100%' - }, - - '& .webchat__fixed-width-image__image': { - left: '50%', - objectFit: 'cover', - position: 'absolute', - top: '50%', - transform: 'translate(-50%, -50%)', - width: '100%' - } - } -}; +import styles from './FixedWidthImage.module.css'; // React calls event handlers with a `SyntheticEvent`, which is not an `Event`, it wraps one in `nativeEvent`. // `custom()` passes the event as-is, `object()` would pass a copy with only the validated properties. @@ -72,16 +47,12 @@ type FixedWidthImageProps = InferReactProps; function FixedWidthImage(props: FixedWidthImageProps) { const { alt, className, onError, onLoad, src } = validateProps(fixedWidthImagePropsSchema, props); + const classNames = useStyles(styles, webChatPrefixedClassName); + return ( -
- - {alt} +
+ + {alt}
); } diff --git a/packages/component/src/Utils/InlineMarkdown.module.css b/packages/component/src/Utils/InlineMarkdown.module.css new file mode 100644 index 0000000000..f9ec009055 --- /dev/null +++ b/packages/component/src/Utils/InlineMarkdown.module.css @@ -0,0 +1,17 @@ +:global(.webchat) .inline-markdown { + button[data-markdown-href] { + appearance: none; + background-color: transparent; + border: 0; + color: var(--webchat__color--accent); + cursor: pointer; + font-family: inherit; + font-size: inherit; + padding: 0; + + @media screen and (forced-colors: active) { + color: LinkText; + text-decoration: underline; + } + } +} diff --git a/packages/component/src/Utils/InlineMarkdown.tsx b/packages/component/src/Utils/InlineMarkdown.tsx index e97dbb6254..9ed9726a10 100644 --- a/packages/component/src/Utils/InlineMarkdown.tsx +++ b/packages/component/src/Utils/InlineMarkdown.tsx @@ -7,7 +7,7 @@ import { stripParagraphContainer } from '@msinternal/botframework-webchat-component-better-link'; import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; -import { useStyleOptions } from 'botframework-webchat-api/hook.js'; +import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react'; import { micromark } from 'micromark'; import React, { useCallback, useMemo } from 'react'; import { @@ -26,8 +26,8 @@ import { type InferInput } from 'valibot'; -import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject'; import createCustomEvent from './createCustomEvent'; +import styles from './InlineMarkdown.module.css'; const referenceEventSchema = union([instance(Event), object({ data: string() })]); @@ -45,34 +45,7 @@ type InlineMarkdownProps = InferInput; const InlineMarkdown = (props: InlineMarkdownProps) => { const { markdown, onReference, references } = validateProps(inlineMarkdownPropsSchema, props); - const [{ accent }] = useStyleOptions(); - const styleToClassName = useStyleToEmotionObject(); - - // We inlined the style here because this style is: - // 1. Internal to Web Chat - // 2. Not customizable from developers (other than setting `styleOptions.accent`) - const className = useMemo( - () => - styleToClassName({ - '& button[data-markdown-href]': { - appearance: 'none', - backgroundColor: 'transparent', - border: 0, - color: accent, - cursor: 'pointer', - fontFamily: 'inherit', - fontSize: 'inherit', - padding: 0 - }, - '@media screen and (forced-colors: active)': { - '& button[data-markdown-href]': { - color: 'LinkText', - textDecoration: 'underline' - } - } - }) + '', - [accent, styleToClassName] - ); + const classNames = useStyles(styles, webChatPrefixedClassName); const html = useMemo(() => { let markdownWithLinkReferenceDefinitions = markdown; @@ -114,7 +87,7 @@ const InlineMarkdown = (props: InlineMarkdownProps) => { [onReference] ); - return ; + return ; }; export default InlineMarkdown;