Skip to content

Commit 9c53a32

Browse files
committed
component: componetize transcript focus
1 parent ca311ee commit 9c53a32

10 files changed

Lines changed: 341 additions & 0 deletions
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/* Focus area container */
2+
3+
:global(.webchat) .transcript-focus-area {
4+
outline: 0;
5+
display: grid;
6+
grid-template-areas: 'content';
7+
8+
.transcript-focus-area__indicator,
9+
.transcript-focus-area__transcript-indicator {
10+
box-sizing: border-box;
11+
font-size: 0;
12+
pointer-events: none;
13+
grid-area: content;
14+
min-height: 0;
15+
}
16+
17+
.transcript-focus-area__root,
18+
.transcript-focus-area__content-root {
19+
grid-area: content;
20+
min-height: 0;
21+
}
22+
23+
.transcript-focus-area__content-root {
24+
padding-top: calc(var(--webchat__padding--regular) / 2);
25+
}
26+
27+
&:focus .transcript-focus-area__content--focused > .transcript-focus-area__indicator {
28+
border-color: var(--webchat__color--transcript-activity-visual-keyboard-indicator);
29+
border-style: var(--webchat__border-style--transcript-activity-visual-keyboard-indicator);
30+
border-width: var(--webchat__border-width--transcript-activity-visual-keyboard-indicator);
31+
height: calc(100% - var(--webchat__padding--regular) / 2);
32+
margin: 0 calc(var(--webchat__padding--regular) / 2);
33+
width: calc(100% - var(--webchat__padding--regular));
34+
}
35+
36+
&:focus-visible .transcript-focus-area__transcript-indicator,
37+
.transcript-focus-area__terminator:focus-visible + .transcript-focus-area__transcript-indicator {
38+
border-color: var(--webchat__border-color--transcript-visual-keyboard-indicator);
39+
border-style: var(--webchat__border-style--transcript-visual-keyboard-indicator);
40+
border-width: var(--webchat__border-width--transcript-visual-keyboard-indicator);
41+
}
42+
43+
.transcript-focus-area__content {
44+
display: grid;
45+
grid-template-areas: 'content';
46+
47+
&:first-child {
48+
margin-top: calc(var(--webchat__padding--regular) / 2);
49+
}
50+
51+
&:not(:first-child) {
52+
margin-top: calc(var(--webchat__padding--regular) / -2);
53+
}
54+
}
55+
56+
/* When the content is focused as active descendant, scrollIntoView() will scroll this invisible div into view. */
57+
.transcript-focus-area__content-active-descendant {
58+
margin-top: calc(var(--webchat__padding--regular) / -2);
59+
padding-bottom: calc(var(--webchat__padding--regular) / 2);
60+
/* The bounding box is expanded to both top and bottom to scroll focus indicator into view. */
61+
/* We should ignore clicks to make sure this expansion don't register click as focus. */
62+
/* Otherwise, when clicking on the very bottom edge of the activity, it will focus on next activity instead. */
63+
pointer-events: none;
64+
width: 100%;
65+
grid-area: content;
66+
min-height: 0;
67+
}
68+
69+
.transcript-focus-area__content-body:not(:empty) {
70+
padding-bottom: var(--webchat__padding--regular);
71+
}
72+
73+
.transcript-focus-area__terminator {
74+
width: 100%;
75+
display: grid;
76+
grid-template-areas: 'content';
77+
}
78+
79+
.transcript-focus-area__terminator-body {
80+
display: flex;
81+
justify-content: center;
82+
width: 100%;
83+
grid-area: content;
84+
min-height: 0;
85+
}
86+
87+
.transcript-focus-area__terminator:not(:focus) .transcript-focus-area__terminator-body {
88+
display: none;
89+
}
90+
91+
.transcript-focus-area__terminator-text {
92+
background: var(--webchat__background--transcript-terminator);
93+
border-radius: var(--webchat__border-radius--transcript-terminator);
94+
color: var(--webchat__color--transcript-terminator);
95+
font-family: var(--webchat__font--primary);
96+
font-size: var(--webchat__font-size--transcript-terminator);
97+
margin: calc(var(--webchat__padding--regular) / 2);
98+
padding: calc(var(--webchat__padding--regular) / 2);
99+
}
100+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useStyles } from 'botframework-webchat-styles/react';
2+
import cx from 'classnames';
3+
import React, { forwardRef, type HTMLAttributes } from 'react';
4+
5+
import styles from './TranscriptFocus.module.css';
6+
import TranscriptFocusIndicator from './TranscriptFocusIndicator';
7+
8+
type TranscriptFocusAreaProps = HTMLAttributes<HTMLDivElement>;
9+
10+
const TranscriptFocusArea = forwardRef<HTMLDivElement, TranscriptFocusAreaProps>(
11+
({ className, children, ...props }, ref) => {
12+
const classNames = useStyles(styles);
13+
14+
return (
15+
<div {...props} className={cx(classNames['transcript-focus-area'], className)} ref={ref}>
16+
<div className={classNames['transcript-focus-area__root']}>{children}</div>
17+
<TranscriptFocusIndicator type="transcript" />
18+
</div>
19+
);
20+
}
21+
);
22+
23+
TranscriptFocusArea.displayName = 'TranscriptFocusArea';
24+
25+
export default TranscriptFocusArea;
26+
export { type TranscriptFocusAreaProps };
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useStyles } from 'botframework-webchat-styles/react';
2+
import cx from 'classnames';
3+
import React, { forwardRef, type HTMLAttributes } from 'react';
4+
5+
import styles from './TranscriptFocus.module.css';
6+
import TranscriptFocusIndicator from './TranscriptFocusIndicator';
7+
8+
type TranscriptFocusContentProps = HTMLAttributes<HTMLDivElement> &
9+
Readonly<{
10+
tag?: React.ElementType;
11+
focused?: boolean;
12+
}>;
13+
14+
const TranscriptFocusContent = forwardRef<HTMLDivElement, TranscriptFocusContentProps>(
15+
({ className, children, tag: Tag = 'div', focused = false, ...props }, ref) => {
16+
const classNames = useStyles(styles);
17+
18+
return (
19+
<Tag
20+
{...props}
21+
className={cx(
22+
classNames['transcript-focus-area__content'],
23+
{ [classNames['transcript-focus-area__content--focused']]: focused },
24+
className
25+
)}
26+
ref={ref}
27+
>
28+
<div className={classNames['transcript-focus-area__content-root']}>{children}</div>
29+
<TranscriptFocusIndicator />
30+
</Tag>
31+
);
32+
}
33+
);
34+
35+
TranscriptFocusContent.displayName = 'TranscriptFocusContent';
36+
37+
export default TranscriptFocusContent;
38+
export { type TranscriptFocusContentProps };
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useStyles } from 'botframework-webchat-styles/react';
2+
import cx from 'classnames';
3+
import React, { forwardRef, type HTMLAttributes } from 'react';
4+
5+
import styles from './TranscriptFocus.module.css';
6+
7+
type TranscriptFocusContentActiveDescendantProps = HTMLAttributes<HTMLDivElement>;
8+
9+
const TranscriptFocusContentActiveDescendant = forwardRef<HTMLDivElement, TranscriptFocusContentActiveDescendantProps>(
10+
({ className, ...props }, ref) => {
11+
const classNames = useStyles(styles);
12+
13+
return (
14+
<div
15+
{...props}
16+
className={cx(classNames['transcript-focus-area__content-active-descendant'], className)}
17+
ref={ref}
18+
/>
19+
);
20+
}
21+
);
22+
23+
TranscriptFocusContentActiveDescendant.displayName = 'TranscriptFocusContentActiveDescendant';
24+
25+
export default TranscriptFocusContentActiveDescendant;
26+
export { type TranscriptFocusContentActiveDescendantProps };
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useStyles } from 'botframework-webchat-styles/react';
2+
import cx from 'classnames';
3+
import React, { forwardRef, type HTMLAttributes } from 'react';
4+
5+
import styles from './TranscriptFocus.module.css';
6+
7+
type TranscriptFocusContentBodyProps = HTMLAttributes<HTMLDivElement>;
8+
9+
const TranscriptFocusContentBody = forwardRef<HTMLDivElement, TranscriptFocusContentBodyProps>(
10+
({ className, ...props }, ref) => {
11+
const classNames = useStyles(styles);
12+
13+
return <div {...props} className={cx(classNames['transcript-focus-area__content-body'], className)} ref={ref} />;
14+
}
15+
);
16+
17+
TranscriptFocusContentBody.displayName = 'TranscriptFocusContentBody';
18+
19+
export default TranscriptFocusContentBody;
20+
export { type TranscriptFocusContentBodyProps };
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/* eslint-disable react/require-default-props */
2+
import { useStyles } from 'botframework-webchat-styles/react';
3+
import cx from 'classnames';
4+
import React, { forwardRef, type HTMLAttributes } from 'react';
5+
6+
import styles from './TranscriptFocus.module.css';
7+
8+
type TranscriptFocusIndicatorProps = HTMLAttributes<HTMLDivElement> &
9+
Readonly<{
10+
type?: 'content' | 'transcript';
11+
}>;
12+
13+
const TranscriptFocusIndicator = forwardRef<HTMLDivElement, TranscriptFocusIndicatorProps>(
14+
({ className, type = 'content', ...props }, ref) => {
15+
const classNames = useStyles(styles);
16+
17+
const indicatorClass =
18+
type === 'content'
19+
? classNames['transcript-focus-area__indicator']
20+
: classNames['transcript-focus-area__transcript-indicator'];
21+
22+
return <div {...props} className={cx(indicatorClass, className)} ref={ref} />;
23+
}
24+
);
25+
26+
TranscriptFocusIndicator.displayName = 'TranscriptFocusIndicator';
27+
28+
export default TranscriptFocusIndicator;
29+
export { type TranscriptFocusIndicatorProps };
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { hooks } from 'botframework-webchat-api';
2+
import { useStyles } from 'botframework-webchat-styles/react';
3+
import cx from 'classnames';
4+
import React, { forwardRef, type HTMLAttributes } from 'react';
5+
6+
import styles from './TranscriptFocus.module.css';
7+
import useUniqueId from '../../hooks/internal/useUniqueId';
8+
9+
type TranscriptFocusTerminatorProps = HTMLAttributes<HTMLDivElement>;
10+
11+
const { useLocalizer } = hooks;
12+
13+
const TranscriptFocusTerminator = forwardRef<HTMLDivElement, TranscriptFocusTerminatorProps>(
14+
({ className, ...props }, ref) => {
15+
const classNames = useStyles(styles);
16+
const localize = useLocalizer();
17+
const terminatorText = localize('TRANSCRIPT_TERMINATOR_TEXT');
18+
const terminatorLabelId = useUniqueId('webchat__basic-transcript__terminator-label');
19+
20+
return (
21+
<div
22+
{...props}
23+
aria-labelledby={terminatorLabelId}
24+
className={cx(classNames['transcript-focus-area__terminator'], className)}
25+
ref={ref}
26+
>
27+
<div className={cx(classNames['transcript-focus-area__terminator-body'])}>
28+
{/* `id` is required for `aria-labelledby` */}
29+
{/* eslint-disable-next-line react/forbid-dom-props */}
30+
<div className={cx(classNames['transcript-focus-area__terminator-text'])} id={terminatorLabelId}>
31+
{terminatorText}
32+
</div>
33+
</div>
34+
</div>
35+
);
36+
}
37+
);
38+
39+
TranscriptFocusTerminator.displayName = 'TranscriptFocusTerminator';
40+
41+
export default TranscriptFocusTerminator;
42+
export { type TranscriptFocusTerminatorProps };
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useStyles } from 'botframework-webchat-styles/react';
2+
import cx from 'classnames';
3+
import React, { forwardRef, type HTMLAttributes } from 'react';
4+
5+
import styles from './TranscriptFocus.module.css';
6+
7+
type TranscriptFocusTerminatorBodyProps = HTMLAttributes<HTMLDivElement>;
8+
9+
const TranscriptFocusTerminatorBody = forwardRef<HTMLDivElement, TranscriptFocusTerminatorBodyProps>(
10+
({ className, ...props }, ref) => {
11+
const classNames = useStyles(styles);
12+
13+
return <div {...props} className={cx(classNames['transcript-focus-area__terminator-body'], className)} ref={ref} />;
14+
}
15+
);
16+
17+
TranscriptFocusTerminatorBody.displayName = 'TranscriptFocusTerminatorBody';
18+
19+
export default TranscriptFocusTerminatorBody;
20+
export { type TranscriptFocusTerminatorBodyProps };
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useStyles } from 'botframework-webchat-styles/react';
2+
import cx from 'classnames';
3+
import React, { forwardRef, type HTMLAttributes } from 'react';
4+
5+
import styles from './TranscriptFocus.module.css';
6+
7+
type TranscriptFocusTerminatorTextProps = HTMLAttributes<HTMLDivElement>;
8+
9+
const TranscriptFocusTerminatorText = forwardRef<HTMLDivElement, TranscriptFocusTerminatorTextProps>(
10+
({ className, ...props }, ref) => {
11+
const classNames = useStyles(styles);
12+
13+
return <div {...props} className={cx(classNames['transcript-focus-area__terminator-text'], className)} ref={ref} />;
14+
}
15+
);
16+
17+
TranscriptFocusTerminatorText.displayName = 'TranscriptFocusTerminatorText';
18+
19+
export default TranscriptFocusTerminatorText;
20+
export { type TranscriptFocusTerminatorTextProps };
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export { default as TranscriptFocusArea, type TranscriptFocusAreaProps } from './TranscriptFocusArea';
2+
export { default as TranscriptFocusContent, type TranscriptFocusContentProps } from './TranscriptFocusContent';
3+
export {
4+
default as TranscriptFocusContentActiveDescendant,
5+
type TranscriptFocusContentActiveDescendantProps
6+
} from './TranscriptFocusContentActiveDescendant';
7+
export {
8+
default as TranscriptFocusContentBody,
9+
type TranscriptFocusContentBodyProps
10+
} from './TranscriptFocusContentBody';
11+
export { default as TranscriptFocusIndicator, type TranscriptFocusIndicatorProps } from './TranscriptFocusIndicator';
12+
export { default as TranscriptFocusTerminator, type TranscriptFocusTerminatorProps } from './TranscriptFocusTerminator';
13+
export {
14+
default as TranscriptFocusTerminatorBody,
15+
type TranscriptFocusTerminatorBodyProps
16+
} from './TranscriptFocusTerminatorBody';
17+
export {
18+
default as TranscriptFocusTerminatorText,
19+
type TranscriptFocusTerminatorTextProps
20+
} from './TranscriptFocusTerminatorText';

0 commit comments

Comments
 (0)