Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Saving and restoring scroll position in composition mode #3268

Merged
merged 17 commits into from
Jun 25, 2020
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

- Resolves [#3205](https://github.com/microsoft/BotFramework-WebChat/issues/3205). Added Direct Line App Service Extension protocol, by [@compulim](https://github.com/compulim) in PR [#3206](https://github.com/microsoft/BotFramework-WebChat/pull/3206)
- Resolves [#3225](https://github.com/microsoft/BotFramework-WebChat/issues/3225). Support allowed scheme with `openUrl` card action, by [@compulim](https://github.com/compulim) in PR [#3226](https://github.com/microsoft/BotFramework-WebChat/pull/3226)
- Resolves [#3252](https://github.com/microsoft/BotFramework-WebChat/issues/3252). Added `useObserveScrollPosition` and `useScrollTo` hooks, by [@compulim](https://github.com/compulim) in PR [#3268](https://github.com/microsoft/BotFramework-WebChat/pull/3268)
- Resolves [#3271](https://github.com/microsoft/BotFramework-WebChat/issues/3252). Added composition mode, which split up `<ReactWebChat>` into `<Composer>` and `<BasicWebChat>`, by [@compulim](https://github.com/compulim) in PR [#3268](https://github.com/microsoft/BotFramework-WebChat/pull/3268)
compulim marked this conversation as resolved.
Show resolved Hide resolved

### Fixed

Expand Down Expand Up @@ -70,6 +72,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Samples

- Resolves [#3205](https://github.com/microsoft/BotFramework-WebChat/issues/3205). Added [Direct Line App Service Extension chat adapter](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/i.protocol-direct-line-app-service-extension) sample, by [@compulim](https://github.com/compulim) in PR [#3206](https://github.com/microsoft/BotFramework-WebChat/pull/3206)
- Resolves [#3271](https://github.com/microsoft/BotFramework-WebChat/issues/3252). Added [enable composition mode](https://microsoft.github.io/BotFramework-WebChat/04.api/m.enable-composition-mode) sample, by [@compulim](https://github.com/compulim) in PR [#3268](https://github.com/microsoft/BotFramework-WebChat/pull/3268)
- Resolves [#3252](https://github.com/microsoft/BotFramework-WebChat/issues/3252). Added [save and restore scroll position](https://microsoft.github.io/BotFramework-WebChat/04.api/n.save-restore-scroll-position) sample, by [@compulim](https://github.com/compulim) in PR [#3268](https://github.com/microsoft/BotFramework-WebChat/pull/3268)
- Resolves [#3271](https://github.com/microsoft/BotFramework-WebChat/issues/3252). Updated [post activity event](https://microsoft.github.io/BotFramework-WebChat/04.api/d.post-activity-event) sample to use composition mode, by [@compulim](https://github.com/compulim) in PR [#3268](https://github.com/microsoft/BotFramework-WebChat/pull/3268)

## [4.9.0] - 2020-05-11

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions __tests__/adaptiveCards.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ test('disable card inputs', async () => {
expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);

await pageObjects.updateProps({ disabled: true });
await driver.wait(scrollToBottomCompleted(), timeouts.scrollToBottom);

// Click "Submit" button should have no effect
await driver.executeScript(() => {
Expand Down
83 changes: 83 additions & 0 deletions __tests__/html/hooks.useObserveScrollPosition.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<script crossorigin="anonymous" src="/__dist__/testharness.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
<style type="text/css">
#app {
height: 100%;
}

.app__button-bar {
display: flex;
flex-direction: column;
position: absolute;
top: 0;
z-index: 1;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="text/babel" data-presets="env,stage-3,react">
const { conditions, createStore, elements, expect, host, pageObjects, timeouts, token } = window.WebChatTest;
const {
Components: { BasicWebChat, Composer },
createDirectLine,
hooks: { useObserveScrollPosition, useSendMessage }
} = window.WebChat;
const { useCallback, useLayoutEffect, useRef, useState } = window.React;

(async function() {
const directLine = createDirectLine({ token: await token.fetchDirectLineToken() });
const store = createStore();

const RunFunction = ({ fn }) => {
fn();

return false;
};

const positions = [];

const ScrollPositionObserver = () => {
useObserveScrollPosition(position => positions.push(position));

return false;
};

const renderWithFunction = fn =>
new Promise(resolve =>
ReactDOM.render(
<Composer directLine={directLine} store={store}>
<BasicWebChat />
<RunFunction fn={() => resolve(fn && fn())} key={Date.now() + ''} />
<ScrollPositionObserver />
</Composer>,
document.getElementById('app')
)
);

await renderWithFunction();

await pageObjects.wait(conditions.uiConnected(), timeouts.directLine);

// Send "markdown", so it show a message long enough to occupy the whole scroll view.
await renderWithFunction(() => useSendMessage()('markdown'));
await pageObjects.wait(conditions.minNumActivitiesShown(2), timeouts.directLine);

// Wait until the scroll view stabilized, we should have a few numbers in the "positions" array.
await pageObjects.wait(conditions.scrollStabilized(), timeouts.ui);

// There should be at least 5 measurements while the transcript auto scroll.
expect(positions.length).toBeGreaterThanOrEqual(5);

await host.done();
})().catch(async err => {
console.error(err);

await host.error(err);
});
</script>
</body>
</html>
90 changes: 90 additions & 0 deletions __tests__/html/hooks.useScrollTo.keepNewMessagesButton.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<script crossorigin="anonymous" src="/__dist__/testharness.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
<style type="text/css">
#app {
height: 100%;
}

.app__button-bar {
display: flex;
flex-direction: column;
position: absolute;
top: 0;
z-index: 1;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="text/babel" data-presets="env,stage-3,react">
const { conditions, createStore, elements, expect, host, pageObjects, timeouts, token } = window.WebChatTest;
const {
Components: { BasicWebChat, Composer },
createDirectLine,
hooks: { useScrollTo, useScrollToEnd, useSendMessage }
} = window.WebChat;
const { useCallback, useLayoutEffect, useRef, useState } = window.React;

(async function() {
const directLine = createDirectLine({ token: await token.fetchDirectLineToken() });
const store = createStore();

const RunFunction = ({ fn }) => {
fn();

return false;
};

const renderWithFunction = fn =>
new Promise(resolve =>
ReactDOM.render(
<Composer directLine={directLine} store={store}>
<BasicWebChat />
<RunFunction fn={() => resolve(fn && fn())} key={Date.now() + ''} />
</Composer>,
document.getElementById('app')
)
);

await renderWithFunction();

await pageObjects.wait(conditions.uiConnected(), timeouts.directLine);

// Send "markdown", so it show a message long enough to occupy the whole scroll view.
await renderWithFunction(() => useSendMessage()('markdown'));
await pageObjects.wait(conditions.minNumActivitiesShown(2), timeouts.directLine);

// Wait until the view start scrolling to the bottom, then we stop it by fixing it to the top.
await pageObjects.wait(conditions.scrolling(), timeouts.ui);
await renderWithFunction(() => useScrollTo()({ scrollTop: 0 }, { behavior: 'auto' }));

// Wait until the view is not sticky.
await new Promise(resolve => setTimeout(resolve, 200));

// Send another "markdown", it should not scroll to bottom ("useSendMessage" do not automatically scroll-to-bottom)
await renderWithFunction(() => useSendMessage()('markdown'));
await pageObjects.wait(conditions.minNumActivitiesShown(4), timeouts.directLine);

// The "New messages" button should show up
await pageObjects.wait(conditions.newMessageButtonShown(), timeouts.ui);

// Call "scrollToEnd" should hide the "New messages" button immediately
await renderWithFunction(() => useScrollTo()({ scrollTop: 100 }));

await pageObjects.wait(conditions.scrollStabilized(), timeouts.scrollToBottom);

// The "New messages" button should continue to show when "scrollTo({ scrollTop: 100 })" is called, because the transcript did not reach the end.
expect(conditions.newMessageButtonShown().fn()).toBeTruthy();

await host.done();
})().catch(async err => {
console.error(err);

await host.error(err);
});
</script>
</body>
</html>
92 changes: 92 additions & 0 deletions __tests__/html/hooks.useScrollToEnd.hideNewMessagesButton.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<script crossorigin="anonymous" src="/__dist__/testharness.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
<style type="text/css">
#app {
height: 100%;
}

.app__button-bar {
display: flex;
flex-direction: column;
position: absolute;
top: 0;
z-index: 1;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="text/babel" data-presets="env,stage-3,react">
const { conditions, createStore, elements, expect, host, pageObjects, timeouts, token } = window.WebChatTest;
const {
Components: { BasicWebChat, Composer },
createDirectLine,
hooks: { useScrollTo, useScrollToEnd, useSendMessage }
} = window.WebChat;
const { useCallback, useLayoutEffect, useRef, useState } = window.React;

(async function() {
const directLine = createDirectLine({ token: await token.fetchDirectLineToken() });
const store = createStore();

const RunFunction = ({ fn }) => {
fn();

return false;
};

const renderWithFunction = fn =>
new Promise(resolve =>
ReactDOM.render(
<Composer directLine={directLine} store={store}>
<BasicWebChat />
<RunFunction fn={() => resolve(fn && fn())} key={Date.now() + ''} />
</Composer>,
document.getElementById('app')
)
);

await renderWithFunction();

await pageObjects.wait(conditions.uiConnected(), timeouts.directLine);

// Send "markdown", so it show a message long enough to occupy the whole scroll view.
await renderWithFunction(() => useSendMessage()('markdown'));
await pageObjects.wait(conditions.minNumActivitiesShown(2), timeouts.directLine);

// Wait until the view start scrolling to the bottom, then we stop it by fixing it to the top.
await pageObjects.wait(conditions.scrolling(), timeouts.ui);
await renderWithFunction(() => useScrollTo()({ scrollTop: 0 }, { behavior: 'auto' }));

// Wait until the view is not sticky.
await new Promise(resolve => setTimeout(resolve, 200));

// Send another "markdown", it should not scroll to bottom ("useSendMessage" do not automatically scroll-to-bottom)
await renderWithFunction(() => useSendMessage()('markdown'));
await pageObjects.wait(conditions.minNumActivitiesShown(4), timeouts.directLine);

// The "New messages" button should show up
await pageObjects.wait(conditions.newMessageButtonShown(), timeouts.ui);

// Call "scrollToEnd" should hide the "New messages" button immediately
await renderWithFunction(() => useScrollToEnd()());
await pageObjects.wait(conditions.negationOf(conditions.newMessageButtonShown()), 100);

// Tests to add:
// - "New messages" button shown, then issue a `scrollToEnd()`, the "New messages" button should gone immediately
// - "New messages" button shown, then issue a `scrollTo({ scrollTop: 100 })`, the "New messages" button should stay
// - Type "help", scroll to top, type "help" again, the "New messages" button should not flash-appear
// - Calling "useObserveScrollPosition" should get some scroll positions

await host.done();
})().catch(async err => {
console.error(err);

await host.error(err);
});
</script>
</body>
</html>
50 changes: 48 additions & 2 deletions docs/HOOKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,12 @@ Following is the list of hooks supported by Web Chat API.
- [`useGroupTimestamp`](#usegrouptimestamp)
- [`useLanguage`](#uselanguage)
- [`useLastTypingAt`](#uselasttypingat)
- [`useLastTypingAt`](#uselasttypingat) (Deprecated)
- [`useLocalize`](#uselocalize) (Deprecated)
- [`useLocalizer`](#useLocalizer)
- [`useLastTypingAt`](#uselasttypingat) (Deprecated)
- [`useMarkActivityAsSpoken`](#usemarkactivityasspoken)
- [`useNotification`](#usenotification)
- [`useObserveScrollPosition`](#useobservescrollposition)
- [`usePerformCardAction`](#useperformcardaction)
- [`usePostActivity`](#usepostactivity)
- [`useReferenceGrammarID`](#usereferencegrammarid)
Expand All @@ -88,6 +89,7 @@ Following is the list of hooks supported by Web Chat API.
- [`useRenderMarkdownAsHTML`](#userendermarkdownashtml)
- [`useRenderToast`](#userendertoast)
- [`useRenderTypingIndicator`](#userendertypingindicator)
- [`useScrollTo`](#usescrollto)
- [`useScrollToEnd`](#usescrolltoend)
- [`useSendBoxValue`](#usesendboxvalue)
- [`useSendEvent`](#usesendevent)
Expand Down Expand Up @@ -537,6 +539,30 @@ useNotifications(): [Notification[]]

When called, this hook will return an array of notifications.

## `useObserveScrollPosition`

<!-- prettier-ignore-start -->
```js
useObserveScrollPosition(observer: (ScrollObserver? | false), deps: any[]): void

type ScrollObserver = (position: ScrollPosition) => void;

type ScrollPosition {
scrollTop: number
}
```
<!-- prettier-ignore-end -->

This function accept an observer function. When the scroll position has changed, the observer function will be called with the latest `ScrollPosition`.

The `position` argument can be passed to [`useScrollTo`](#usescrollto) hook to restore scroll position.

Since the observer function will be called rapidly, please keep the code in the function as lightweight as possible.

To stop observing scroll positions, pass a falsy value to the `observer` argument.

> If there are more than one transcripts, scrolling any of them will trigger the observer function, and there is no clear distinction of which transcript is being scrolled.
compulim marked this conversation as resolved.
Show resolved Hide resolved

## `usePerformCardAction`

<!-- prettier-ignore-start -->
Expand Down Expand Up @@ -715,6 +741,26 @@ This function is for rendering typing indicator for all participants of the conv
- `typing` lists participants who did not explicitly stopped typing. This list is a superset of `activeTyping`.
- `visible` indicates whether typing indicator should be shown in normal case. This is based on participants in `activeTyping` and their `role` (role not equal to `"user"`).

## `useScrollTo`

<!-- prettier-ignore-start -->
```js
useScrollTo(): (position: ScrollPosition, options: ScrollOptions) => void

type ScrollOptions {
behavior: 'auto' | 'smooth'
}

type ScrollPosition {
scrollTop: number
}
```
<!-- prettier-ignore-end -->

This function will return a function that, when called, will scroll the transcript to the specific scroll position.
compulim marked this conversation as resolved.
Show resolved Hide resolved

If `options` is passed with `behavior` set to `smooth`, it will smooth-scrolling to the scroll position. Otherwise, it will jump to the scroll position instantly.

## `useScrollToEnd`

<!-- prettier-ignore-start -->
Expand All @@ -723,7 +769,7 @@ useScrollToEnd(): () => void
```
<!-- prettier-ignore-end -->

This function will return a function that, when called, will scroll the transcript view to the end.
This function will return a function that, when called, will scroll the transcript view to the end using smooth scrolling.
compulim marked this conversation as resolved.
Show resolved Hide resolved

## `useSendBoxValue`

Expand Down
Loading