Skip to content

Commit d441bf9

Browse files
committed
component: basic transcript transcript focus and logical grouping adjustments
1 parent 9d7dae8 commit d441bf9

File tree

1 file changed

+36
-55
lines changed

1 file changed

+36
-55
lines changed

packages/component/src/BasicTranscript.tsx

Lines changed: 36 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ import usePrevious from './hooks/internal/usePrevious';
4343
import useRegisterFocusTranscript from './hooks/internal/useRegisterFocusTranscript';
4444
import useRegisterScrollTo from './hooks/internal/useRegisterScrollTo';
4545
import useRegisterScrollToEnd from './hooks/internal/useRegisterScrollToEnd';
46-
import useUniqueId from './hooks/internal/useUniqueId';
4746
import useValueRef from './hooks/internal/useValueRef';
4847
import {
4948
useRegisterScrollRelativeTranscript,
@@ -60,8 +59,10 @@ import TranscriptFocusComposer from './providers/TranscriptFocus/TranscriptFocus
6059
import useActiveDescendantId from './providers/TranscriptFocus/useActiveDescendantId';
6160
import useFocusByActivityKey from './providers/TranscriptFocus/useFocusByActivityKey';
6261
import useFocusRelativeActivity from './providers/TranscriptFocus/useFocusRelativeActivity';
63-
import useFocusedActivityKey from './providers/TranscriptFocus/useFocusedActivityKey';
62+
import useFocusedKey from './providers/TranscriptFocus/useFocusedKey';
6463
import useFocusedExplicitly from './providers/TranscriptFocus/useFocusedExplicitly';
64+
import { ActivityLogicalGroupingComposer } from './providers/ActivityLogicalGrouping';
65+
import { TranscriptFocusArea, TranscriptFocusTerminator } from './Transcript/TranscriptFocus';
6566

6667
const {
6768
useActivityKeys,
@@ -114,12 +115,11 @@ type InternalTranscriptProps = Readonly<{
114115
// TODO: [P1] #4133 Add telemetry for computing how many re-render done so far.
115116
const InternalTranscript = forwardRef<HTMLDivElement, InternalTranscriptProps>(
116117
({ className, terminatorRef }: InternalTranscriptProps, ref) => {
117-
const [{ basicTranscript: basicTranscriptStyleSet }] = useStyleSet();
118118
const [activeDescendantId] = useActiveDescendantId();
119119
const [direction] = useDirection();
120-
const [focusedActivityKey] = useFocusedActivityKey();
120+
const [focusedKey] = useFocusedKey();
121121
const [focusedExplicitly] = useFocusedExplicitly();
122-
const activityElementMapRef = useActivityElementMapRef();
122+
const focusElementMapRef = useActivityElementMapRef();
123123
const focus = useFocus();
124124
const focusByActivityKey = useFocusByActivityKey();
125125
const focusRelativeActivity = useFocusRelativeActivity();
@@ -128,10 +128,8 @@ const InternalTranscript = forwardRef<HTMLDivElement, InternalTranscriptProps>(
128128
const localize = useLocalizer();
129129
const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + '';
130130
const rootElementRef = useRef<HTMLDivElement>(null);
131-
const terminatorLabelId = useUniqueId('webchat__basic-transcript__terminator-label');
132131

133-
const focusedActivityKeyRef = useValueRef(focusedActivityKey);
134-
const terminatorText = localize('TRANSCRIPT_TERMINATOR_TEXT');
132+
const focusedKeyRef = useValueRef(focusedKey);
135133
const transcriptAriaLabel = localize('TRANSCRIPT_ARIA_LABEL_ALT');
136134

137135
const callbackRef = useCallback(
@@ -165,7 +163,7 @@ const InternalTranscript = forwardRef<HTMLDivElement, InternalTranscriptProps>(
165163
if (typeof scrollTop !== 'undefined') {
166164
scrollToBottomScrollTo(scrollTop, { behavior });
167165
} else if (typeof activityId !== 'undefined') {
168-
const activityBoundingBoxElement = activityElementMapRef.current
166+
const activityBoundingBoxElement = focusElementMapRef.current
169167
.get(getKeyByActivityId(activityId))
170168
?.querySelector('.webchat__basic-transcript__activity-active-descendant');
171169

@@ -193,7 +191,7 @@ const InternalTranscript = forwardRef<HTMLDivElement, InternalTranscriptProps>(
193191
}
194192
}
195193
},
196-
[activityElementMapRef, getKeyByActivityId, rootElementRef, scrollToBottomScrollTo]
194+
[focusElementMapRef, getKeyByActivityId, rootElementRef, scrollToBottomScrollTo]
197195
);
198196

199197
const scrollToEnd = useCallback(
@@ -266,7 +264,7 @@ const InternalTranscript = forwardRef<HTMLDivElement, InternalTranscriptProps>(
266264

267265
// Find the activity just above scroll view bottom.
268266
// If the scroll view is already on top, get the first activity.
269-
const activityElements = Array.from(activityElementMapRef.current.entries());
267+
const activityElements = Array.from(focusElementMapRef.current.entries());
270268
const activityKeyJustAboveScrollBottom: string | undefined = (
271269
scrollableElement.scrollTop
272270
? activityElements
@@ -293,7 +291,7 @@ const InternalTranscript = forwardRef<HTMLDivElement, InternalTranscriptProps>(
293291
}
294292
},
295293
[
296-
activityElementMapRef,
294+
focusElementMapRef,
297295
dispatchScrollPositionWithActivityId,
298296
getActivityByKey,
299297
markActivityKeyAsRead,
@@ -334,9 +332,10 @@ const InternalTranscript = forwardRef<HTMLDivElement, InternalTranscriptProps>(
334332
// This is capturing plain ENTER.
335333
// When screen reader is not running, or screen reader is running outside of scan mode, the ENTER key will be captured here.
336334
if (!fromEndOfTranscriptIndicator) {
337-
const activityFocusTrapTarget: HTMLElement = activityElementMapRef.current
338-
.get(focusedActivityKeyRef.current)
339-
?.querySelector('.webchat__basic-transcript__activity-focus-target');
335+
const focusElement = focusElementMapRef.current?.get(focusedKeyRef.current);
336+
const activityFocusTrapTarget: HTMLElement =
337+
focusElement?.querySelector('.webchat__basic-transcript__group-focus-target') ??
338+
focusElement?.querySelector('.webchat__basic-transcript__activity-focus-target');
340339
// TODO: review focus approach:
341340
// It is not clear how to handle focus without introducing something like context.
342341
// Ideally we would want a way to interact with focus outside of React
@@ -367,7 +366,7 @@ const InternalTranscript = forwardRef<HTMLDivElement, InternalTranscriptProps>(
367366
event.stopPropagation();
368367
}
369368
},
370-
[activityElementMapRef, focus, focusedActivityKeyRef, focusRelativeActivity, terminatorRef]
369+
[focusElementMapRef, focus, focusedKeyRef, focusRelativeActivity, terminatorRef]
371370
);
372371

373372
const handleTranscriptKeyDownCapture = useCallback<KeyboardEventHandler<HTMLDivElement>>(
@@ -399,8 +398,8 @@ const InternalTranscript = forwardRef<HTMLDivElement, InternalTranscriptProps>(
399398
// Dispatch a "transcript focus" event based on user selection.
400399
// We should not dispatch "transcript focus" when a new activity come. Although the selection change, it is not initiated from the user.
401400
useMemo(
402-
() => dispatchTranscriptFocusByActivityKey(focusedExplicitly ? focusedActivityKey : undefined),
403-
[dispatchTranscriptFocusByActivityKey, focusedActivityKey, focusedExplicitly]
401+
() => dispatchTranscriptFocusByActivityKey(focusedExplicitly ? focusedKey : undefined),
402+
[dispatchTranscriptFocusByActivityKey, focusedKey, focusedExplicitly]
404403
);
405404

406405
// When the transcript is being focused on, we should dispatch a "transcriptfocus" event.
@@ -426,17 +425,12 @@ const InternalTranscript = forwardRef<HTMLDivElement, InternalTranscriptProps>(
426425
const hasAnyChild = !!numRenderingActivities;
427426

428427
return (
429-
<div
428+
<TranscriptFocusArea
430429
// Although Android TalkBack 12.1 does not support `aria-activedescendant`, when used, it become buggy and will narrate content twice.
431430
// We are disabling `aria-activedescendant` for Android. See <ActivityRow> for details.
432431
aria-activedescendant={android ? undefined : activeDescendantId}
433432
aria-label={transcriptAriaLabel}
434-
className={classNames(
435-
'webchat__basic-transcript',
436-
basicTranscriptStyleSet + '',
437-
rootClassName,
438-
(className || '') + ''
439-
)}
433+
className={classNames('webchat__basic-transcript', rootClassName, (className || '') + '')}
440434
dir={direction}
441435
onFocus={handleFocus}
442436
onKeyDown={handleTranscriptKeyDown}
@@ -450,33 +444,18 @@ const InternalTranscript = forwardRef<HTMLDivElement, InternalTranscriptProps>(
450444
// https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_focus_activedescendant
451445
tabIndex={0}
452446
>
453-
<LiveRegionTranscript activityElementMapRef={activityElementMapRef} />
447+
<LiveRegionTranscript activityElementMapRef={focusElementMapRef} />
454448
{hasAnyChild && <FocusRedirector redirectRef={terminatorRef} />}
455449
<InternalTranscriptScrollable onFocusFiller={handleFocusFiller}>
456450
{hasAnyChild && <ActivityTree />}
457451
</InternalTranscriptScrollable>
458452
{hasAnyChild && (
459453
<Fragment>
460454
<FocusRedirector redirectRef={rootElementRef} />
461-
<div
462-
aria-labelledby={terminatorLabelId}
463-
className="webchat__basic-transcript__terminator"
464-
ref={terminatorRef}
465-
role="note"
466-
tabIndex={0}
467-
>
468-
<div className="webchat__basic-transcript__terminator-body">
469-
{/* `id` is required for `aria-labelledby` */}
470-
{/* eslint-disable-next-line react/forbid-dom-props */}
471-
<div className="webchat__basic-transcript__terminator-text" id={terminatorLabelId}>
472-
{terminatorText}
473-
</div>
474-
</div>
475-
</div>
455+
<TranscriptFocusTerminator ref={terminatorRef} role="note" tabIndex={0} />
476456
</Fragment>
477457
)}
478-
<div className="webchat__basic-transcript__focus-indicator" />
479-
</div>
458+
</TranscriptFocusArea>
480459
);
481460
}
482461
);
@@ -667,18 +646,20 @@ const BasicTranscript = ({ className = '' }: BasicTranscriptProps) => {
667646

668647
return (
669648
<ChatHistoryBox className={className}>
670-
<RenderingActivitiesComposer>
671-
<TranscriptFocusComposer containerRef={containerRef}>
672-
<ReactScrollToBottomComposer nonce={nonce} scroller={scroller} styleOptions={styleOptions}>
673-
<ChatHistoryToolbar>
674-
<ScrollToEndButton terminatorRef={terminatorRef} />
675-
</ChatHistoryToolbar>
676-
<GroupedRenderingActivitiesComposer>
677-
<InternalTranscript ref={containerRef} terminatorRef={terminatorRef} />
678-
</GroupedRenderingActivitiesComposer>
679-
</ReactScrollToBottomComposer>
680-
</TranscriptFocusComposer>
681-
</RenderingActivitiesComposer>
649+
<ActivityLogicalGroupingComposer>
650+
<RenderingActivitiesComposer>
651+
<TranscriptFocusComposer containerRef={containerRef}>
652+
<ReactScrollToBottomComposer nonce={nonce} scroller={scroller} styleOptions={styleOptions}>
653+
<ChatHistoryToolbar>
654+
<ScrollToEndButton terminatorRef={terminatorRef} />
655+
</ChatHistoryToolbar>
656+
<GroupedRenderingActivitiesComposer>
657+
<InternalTranscript ref={containerRef} terminatorRef={terminatorRef} />
658+
</GroupedRenderingActivitiesComposer>
659+
</ReactScrollToBottomComposer>
660+
</TranscriptFocusComposer>
661+
</RenderingActivitiesComposer>
662+
</ActivityLogicalGroupingComposer>
682663
</ChatHistoryBox>
683664
);
684665
};

0 commit comments

Comments
 (0)