Skip to content

Commit

Permalink
Fluent: sendbox transcript navigation (#5191)
Browse files Browse the repository at this point in the history
* Fluent: allow transcript scroll when sendbox focused

* Rework empty check

* Fix failing tests

* Changelog

---------

Co-authored-by: William Wong <compulim@users.noreply.github.com>
  • Loading branch information
OEvgeny and compulim committed May 30, 2024
1 parent 5b1dae0 commit 0c29473
Show file tree
Hide file tree
Showing 20 changed files with 168 additions and 53 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
- Fixed potential memory usage issues caused by `useActivitiesWithRenderer`, in PR [5183](https://github.com/microsoft/BotFramework-WebChat/pull/5183), by [@OEvgeny](https://github.com/OEvgeny)
- Improved performance for `useMemoized`, in PR [5190](https://github.com/microsoft/BotFramework-WebChat/pull/5190), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed send box zoomed in when clicked on mobile Safari, in PR [5192](https://github.com/microsoft/BotFramework-WebChat/pull/5192), by [@OEvgeny](https://github.com/OEvgeny)
- Added missing support for chat history scroll with keyboard when Fluent send box is focused, in PR [5191](https://github.com/microsoft/BotFramework-WebChat/pull/5191), by [@OEvgeny](https://github.com/OEvgeny)

### Changed

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions __tests__/html/fluentTheme/transcript.navigation.pageUpDown.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.6/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="/test-harness.js"></script>
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
<script crossorigin="anonymous" src="/__dist__/botframework-webchat-fluent-theme.production.min.js"></script>
</head>
<body>
<main id="webchat"></main>
<script type="text/babel">
run(async function () {
const {
React,
ReactDOM: { render },
WebChat: { FluentThemeProvider, ReactWebChat }
} = window; // Imports in UMD fashion.

const directLine = testHelpers.createDirectLineWithTranscript(
testHelpers.transcriptNavigation.generateTranscript()
);
const store = testHelpers.createStore();

const App = () => <ReactWebChat directLine={directLine} store={store} />;

render(
<FluentThemeProvider>
<App />
</FluentThemeProvider>,
document.getElementById('webchat')
);
await pageConditions.uiConnected();
await pageConditions.numActivitiesShown(32);
await pageConditions.scrollToBottomCompleted();

document.querySelector(`[data-testid="${WebChat.testIds.sendBoxTextBox}"]`).focus();

// Should scroll a page up
await host.sendKeys('PAGE_UP');
await pageConditions.scrollStabilized();
await host.snapshot();

// Should scroll another page up
await host.sendKeys('PAGE_UP');
await pageConditions.scrollStabilized();
await host.snapshot();

// Should scroll back down
await host.sendKeys('PAGE_DOWN');
await pageConditions.scrollStabilized();
await host.snapshot();

// Should scroll to the top
await host.sendKeys('HOME');
await pageConditions.scrollStabilized();
await host.snapshot();

// Should scroll to the bottom
await host.sendKeys('END');
await pageConditions.scrollStabilized();
await host.snapshot();

// Should not scroll because the send box is not empty
await host.sendKeys('A', 'PAGE_UP');
await pageConditions.scrollStabilized();
await host.snapshot();

// Should page up because the send box is no longer empty
await host.sendKeys('BACK_SPACE', 'PAGE_UP');
await pageConditions.scrollStabilized();
await host.snapshot();
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */

describe('Fluent theme applied', () => {
test('performs transcript navigation', () => runHTML('fluentTheme/transcript.navigation.pageUpDown'));
});
4 changes: 2 additions & 2 deletions __tests__/html/hooks.useScrollUpDown.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@
expect(scrollable.scrollTop).toBe(scrollTopAtBottommost);

// Call useScrollUp() should scroll the transcript view up
await renderWithFunction(() => useScrollUp()());
await renderWithFunction(() => Promise.resolve(useScrollUp()).then(fn => fn()));
await pageConditions.scrollStabilized();
expect(scrollable.scrollTop).toBeLessThan(scrollTopAtBottommost);

// Call useScrollDown() should scroll the transcript view down, back at the bottommost position
await renderWithFunction(() => useScrollDown()());
await renderWithFunction(() => Promise.resolve(useScrollDown()).then(fn => fn()));
await pageConditions.scrollStabilized();
expect(scrollable.scrollTop).toBe(scrollTopAtBottommost);
});
Expand Down
9 changes: 6 additions & 3 deletions packages/component/src/BasicTranscript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import useFocusRelativeActivity from './providers/TranscriptFocus/useFocusRelati
import useObserveFocusVisible from './hooks/internal/useObserveFocusVisible';
import usePrevious from './hooks/internal/usePrevious';
import useRegisterFocusTranscript from './hooks/internal/useRegisterFocusTranscript';
import useRegisterScrollRelative from './hooks/internal/useRegisterScrollRelative';
import useRegisterScrollTo from './hooks/internal/useRegisterScrollTo';
import useRegisterScrollToEnd from './hooks/internal/useRegisterScrollToEnd';
import useStyleSet from './hooks/useStyleSet';
Expand All @@ -48,6 +47,10 @@ import useUniqueId from './hooks/internal/useUniqueId';
import useValueRef from './hooks/internal/useValueRef';
import TranscriptActivity from './TranscriptActivity';
import useMemoized from './hooks/internal/useMemoized';
import {
useRegisterScrollRelativeTranscript,
type TranscriptScrollRelativeOptions
} from './hooks/transcriptScrollRelative';

const {
useActivityKeys,
Expand Down Expand Up @@ -274,7 +277,7 @@ const InternalTranscript = forwardRef<HTMLDivElement, InternalTranscriptProps>(
);

const scrollRelative = useCallback(
(direction: 'down' | 'up', { displacement }: { displacement?: number } = {}) => {
({ direction, displacement }: TranscriptScrollRelativeOptions) => {
const { current: rootElement } = rootElementRef;

if (!rootElement) {
Expand Down Expand Up @@ -306,7 +309,7 @@ const InternalTranscript = forwardRef<HTMLDivElement, InternalTranscriptProps>(
// We call `useRegisterScrollXXX` to register a callback function, the `useScrollXXX` will multiplex the call into each instance of <BasicTranscript>.
useRegisterScrollTo(scrollTo);
useRegisterScrollToEnd(scrollToEnd);
useRegisterScrollRelative(scrollRelative);
useRegisterScrollRelativeTranscript(scrollRelative);

const markActivityKeyAsRead = useMarkActivityKeyAsRead();

Expand Down
6 changes: 0 additions & 6 deletions packages/component/src/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,6 @@ const ComposerCore = ({
const scrollToCallbacksRef = useRef([]);
const scrollToEndCallbacksRef = useRef([]);

// Instead of having a `scrollUpCallbacksRef` and `scrollDownCallbacksRef`, they are combined into a single `scrollRelativeCallbacksRef`.
// The first argument tells whether it should go "up" or "down".
const scrollRelativeCallbacksRef = useRef([]);

const internalRenderMarkdownInline = useMemo(
() => markdown => {
const tree = internalMarkdownIt.parseInline(markdown);
Expand Down Expand Up @@ -220,7 +216,6 @@ const ComposerCore = ({
observeScrollPosition,
observeTranscriptFocus,
renderMarkdown,
scrollRelativeCallbacksRef,
scrollToCallbacksRef,
scrollToEndCallbacksRef,
setDictateAbortable,
Expand All @@ -241,7 +236,6 @@ const ComposerCore = ({
observeTranscriptFocus,
patchedStyleSet,
renderMarkdown,
scrollRelativeCallbacksRef,
scrollToCallbacksRef,
scrollToEndCallbacksRef,
setDictateAbortable,
Expand Down
21 changes: 0 additions & 21 deletions packages/component/src/hooks/internal/useRegisterScrollRelative.js

This file was deleted.

12 changes: 0 additions & 12 deletions packages/component/src/hooks/internal/useScrollRelative.js

This file was deleted.

8 changes: 8 additions & 0 deletions packages/component/src/hooks/transcriptScrollRelative.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createPropagation } from 'use-propagate';

export type TranscriptScrollRelativeOptions = { direction: 'down' | 'up'; displacement?: number };

const { useListen: useRegisterScrollRelativeTranscript, usePropagate: useScrollRelativeTranscript } =
createPropagation<TranscriptScrollRelativeOptions>();

export { useRegisterScrollRelativeTranscript, useScrollRelativeTranscript };
7 changes: 3 additions & 4 deletions packages/component/src/hooks/useScrollDown.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useCallback } from 'react';

import useScrollRelative from './internal/useScrollRelative';
import { useScrollRelativeTranscript } from './transcriptScrollRelative';

export default function useScrollDown(): (options?: { displacement: number }) => void {
const scrollRelative = useScrollRelative();
const scrollRelative = useScrollRelativeTranscript();

return useCallback((...args) => scrollRelative('down', ...args), [scrollRelative]);
return useCallback(options => scrollRelative({ direction: 'down', ...options }), [scrollRelative]);
}
7 changes: 3 additions & 4 deletions packages/component/src/hooks/useScrollUp.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useCallback } from 'react';

import useScrollRelative from './internal/useScrollRelative';
import { useScrollRelativeTranscript } from './transcriptScrollRelative';

export default function useScrollUp(): (options?: { displacement: number }) => void {
const scrollRelative = useScrollRelative();
const scrollRelative = useScrollRelativeTranscript();

return useCallback((...args) => scrollRelative('up', ...args), [scrollRelative]);
return useCallback(options => scrollRelative({ direction: 'up', ...options }), [scrollRelative]);
}
9 changes: 8 additions & 1 deletion packages/fluent-theme/src/components/sendBox/SendBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import useSubmitError from './private/useSubmitError';
import useUniqueId from './private/useUniqueId';
import styles from './SendBox.module.css';
import { useStyles } from '../../styles';
import useTranscriptNavigation from './private/useTranscriptNavigation';

const {
useFocus,
Expand Down Expand Up @@ -146,6 +147,8 @@ function SendBox(
[sendMessage]
);

const handleTranscriptNavigation = useTranscriptNavigation();

const aria = {
'aria-invalid': 'false' as const,
...(errorMessage && {
Expand All @@ -157,7 +160,11 @@ function SendBox(
return (
<form {...aria} className={cx(classNames['sendbox'], props.className)} onSubmit={handleFormSubmit}>
<SuggestedActions />
<div className={cx(classNames['sendbox__sendbox'])} onClickCapture={handleSendBoxClick}>
<div
className={cx(classNames['sendbox__sendbox'])}
onClickCapture={handleSendBoxClick}
onKeyDown={handleTranscriptNavigation}
>
<TextArea
aria-label={isMessageLengthExceeded ? localize('TEXT_INPUT_LENGTH_EXCEEDED_ALT') : localize('TEXT_INPUT_ALT')}
className={cx(classNames['sendbox__sendbox-text'], classNames['sendbox__text-area--in-grid'])}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useCallback, type KeyboardEvent } from 'react';
import { hooks } from 'botframework-webchat-component';

const { useScrollDown, useScrollUp } = hooks;

export default function useTranscriptNavigation() {
const scrollDown = useScrollDown();
const scrollUp = useScrollUp();

return useCallback(
(event: KeyboardEvent<unknown>) => {
if (event.target instanceof HTMLTextAreaElement && event.target.value) {
return;
}

const { ctrlKey, metaKey, shiftKey } = event;

if (ctrlKey || metaKey || shiftKey) {
return;
}

let handled = true;

switch (event.key) {
case 'End':
scrollDown({ displacement: Infinity });
break;

case 'Home':
scrollUp({ displacement: Infinity });
break;

case 'PageDown':
scrollDown();
break;

case 'PageUp':
scrollUp();
break;

default:
handled = false;
break;
}

if (handled) {
event.preventDefault();
event.stopPropagation();
}
},
[scrollDown, scrollUp]
);
}

0 comments on commit 0c29473

Please sign in to comment.