Skip to content

Commit

Permalink
fix: better scroll handling
Browse files Browse the repository at this point in the history
  • Loading branch information
supersnager committed Nov 10, 2020
1 parent a5d636b commit 6f60d88
Show file tree
Hide file tree
Showing 10 changed files with 1,744 additions and 10 deletions.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@
"@fortawesome/free-solid-svg-icons": "^5.12.0",
"@fortawesome/react-fontawesome": "^0.1.8",
"classnames": "^2.2.6",
"prop-types": "^15.7.2",
"react-perfect-scrollbar": "^1.5.8"
"prop-types": "^15.7.2"
},
"husky": {
"hooks": {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ConversationList/ConversationList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useMemo } from "react";
import PropTypes from "prop-types";
import { allowedChildren } from "../utils";
import { prefix } from "../settings";
import PerfectScrollbar from "react-perfect-scrollbar";
import PerfectScrollbar from "../Scroll";
import classNames from "classnames";
import Overlay from "../Overlay";
import Loader from "../Loader";
Expand Down
2 changes: 1 addition & 1 deletion src/components/MessageInput/MessageInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { prefix } from "../settings";
import ContentEditable from "../ContentEditable";
import SendButton from "../Buttons/SendButton";
import AttachmentButton from "../Buttons/AttachmentButton";
import PerfectScrollbar from "react-perfect-scrollbar";
import PerfectScrollbar from "../Scroll";

// Because container depends on fancyScroll
// it must be wrapped in additional container
Expand Down
109 changes: 104 additions & 5 deletions src/components/MessageList/MessageList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from "prop-types";
import classNames from "classnames";
import { allowedChildren, getChildren } from "../utils";
import { prefix } from "../settings";
import PerfectScrollbar from "react-perfect-scrollbar";
import PerfectScrollbar from "../Scroll";
import Loader from "../Loader";
import Overlay from "../Overlay";
import Message from "../Message";
Expand All @@ -14,22 +14,34 @@ import MessageListContent from "./MessageListContent";
class MessageListInner extends React.Component {
constructor(props) {
super(props);

this.scrollPointRef = React.createRef();
this.containerRef = React.createRef();
this.scrollRef = React.createRef();
this.lastClientHeight = 0;
this.preventScrollTop = false;
this.resizeObserver = undefined;
this.scrollTicking = false;
this.resizeTicking = false;
this.noScroll = undefined;
}

getSnapshotBeforeUpdate() {
const list = this.containerRef.current;

const sticky = list.scrollHeight === list.scrollTop + list.clientHeight;
const topHeight = Math.round(list.scrollTop + list.clientHeight);
// 1 px fix for firefox
const sticky =
list.scrollHeight === topHeight ||
list.scrollHeight + 1 === topHeight ||
list.scrollHeight - 1 === topHeight;

return {
sticky,
clientHeight: list.clientHeight,
scrollHeight: list.scrollHeight,
lastMessageOrGroup: this.getLastMessageOrGroup(),
diff: list.scrollHeight - list.scrollTop,
};
}

Expand All @@ -42,30 +54,107 @@ class MessageListInner extends React.Component {
this.scrollRef.current.updateScroll();
};

handleContainerResize = () => {
if (this.resizeTicking === false) {
window.requestAnimationFrame(() => {
const list = this.containerRef.current;

const currentHeight = list.clientHeight;

const diff = currentHeight - this.lastClientHeight;

if (diff >= 1) {
// Because fractional

if (this.preventScrollTop === false) {
list.scrollTop = Math.round(list.scrollTop) - diff;
}
} else {
list.scrollTop = list.scrollTop - diff;
}

this.lastClientHeight = list.clientHeight;

this.scrollRef.current.updateScroll();

this.resizeTicking = false;
});

this.resizeTicking = true;
}
};

isSticked = () => {
const list = this.containerRef.current;

return list.scrollHeight === Math.round(list.scrollTop + list.clientHeight);
};

handleScroll = () => {
if (this.scrollTicking === false) {
window.requestAnimationFrame(() => {
if (this.noScroll === false) {
this.preventScrollTop = this.isSticked();
} else {
this.noScroll = false;
}

this.scrollTicking = false;
});

this.scrollTicking = true;
}
};

componentDidMount() {
// Set scrollbar to bottom on start (getSnaphotBeforeUpdate is not invoked on mount)
this.scrollToEnd();

this.lastClientHeight = this.containerRef.current.clientHeight;

window.addEventListener("resize", this.handleResize);

this.resizeObserver = new ResizeObserver(this.handleContainerResize);
this.resizeObserver.observe(this.containerRef.current);
this.containerRef.current.addEventListener("scroll", this.handleScroll);
}

componentDidUpdate(prevProps, prevState, snapshot) {
if (typeof snapshot !== "undefined") {
const list = this.containerRef.current;

const last = this.getLastMessageOrGroup();
if (last === snapshot.lastMessageOrGroup) {
list.scrollTop =
list.scrollHeight -
snapshot.diff +
(this.lastClientHeight - list.clientHeight);
}

if (snapshot.sticky === true) {
this.scrollToEnd();
this.preventScrollTop = true;
} else {
if (snapshot.clientHeight < this.lastClientHeight) {
if (list.scrollHeight === list.scrollTop + this.lastClientHeight) {
// If was sticky because scrollHeight is not changing, so here will be equal to lastHeight plus current scrollTop
// 1px fix id for firefox
const sHeight = list.scrollTop + this.lastClientHeight;
if (
list.scrollHeight === sHeight ||
list.scrollHeight + 1 === sHeight ||
list.scrollHeight - 1 === sHeight
) {
this.scrollToEnd();
this.preventScrollTop = true;
} else {
this.preventScrollTop = false;
}
} else {
this.preventScrollTop = false;
const last = this.getLastMessageOrGroup();

if (last === snapshot.lastMessageOrGroup) {
// New elements were not added at end

// New elements were added at start
if (
list.scrollTop === 0 &&
Expand All @@ -83,6 +172,8 @@ class MessageListInner extends React.Component {

componentWillUnmount() {
window.removeEventListener("resize", this.handleResize);
this.resizeObserver.disconnect();
this.containerRef.current.removeEventListener("scroll", this.handleScroll);
}

scrollToEnd() {
Expand All @@ -103,6 +194,10 @@ class MessageListInner extends React.Component {
}

this.lastClientHeight = list.clientHeight;

// Important flag! Blocks strange Chrome mobile behaviour - automatic scroll.
// Chrome mobile sometimes trigger scroll when new content is entered to MessageInput. It's probably Chrome Bug - sth related with overflow-anchor
this.noScroll = true;
}

getLastMessageOrGroup = () =>
Expand Down Expand Up @@ -146,14 +241,18 @@ class MessageListInner extends React.Component {
containerRef={(ref) => (this.containerRef.current = ref)}
options={{ suppressScrollX: true }}
{...{ [`data-${prefix}-message-list`]: "" }}
style={{
overscrollBehaviorY: "none",
overflowAnchor: "auto",
touchAction: "none",
}}
>
{customContent ? customContent : children}
<div
className={`${cName}__scroll-to`}
ref={this.scrollPointRef}
></div>
</PerfectScrollbar>

{typeof typingIndicator !== "undefined" && (
<div className={`${cName}__typing-indicator-container`}>
{typingIndicator}
Expand Down
21 changes: 21 additions & 0 deletions src/components/Scroll/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2020 chatscope.io <chatscope@chatscope.io>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
30 changes: 30 additions & 0 deletions src/components/Scroll/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Strange things

**perfect-scrollbar.ems.js**
Builded version of https://github.com/mdbootstrap/perfect-scrollbar with two fixes apllied manually:

- Issue: https://github.com/mdbootstrap/perfect-scrollbar/issues/947
Fix: https://github.com/mdbootstrap/perfect-scrollbar/pull/946/commits/8f9db2986da2d5fa3fa402b169f1c9b1ffe53369

- Issue: https://github.com/mdbootstrap/perfect-scrollbar/issues/920#issuecomment-722570659
Fix: https://github.com/mdbootstrap/perfect-scrollbar/commit/daeeddf5972c44a960f1d244a4b9719cb7c3d0b7

- Issue and fix: https://github.com/mdbootstrap/perfect-scrollbar/issues/975

- Issue: https://github.com/mdbootstrap/perfect-scrollbar/issues/975
Fix: fixed by adding { passive: false } Yes! false not true! to some event handlers. Also added css property touch-action: none to MessageList scrollbar container.

Modified code is based on v1.5.0

**ReactPerfectScrollbar.jsx**
Based on https://github.com/goldenyz/react-perfect-scrollbar/ but modified for using directly local PerfectScrollbar version.

Modified code is based on v1.5.8

# Why

These hacks were made because the necessary Perfect Scrollbar fixes haven't been released for a very long time.

# License

Both libraries are licensed under the MIT license.
Loading

0 comments on commit 6f60d88

Please sign in to comment.