about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/components/scrollable_list.js
diff options
context:
space:
mode:
authorpluralcafe-docker <git@plural.cafe>2019-01-12 04:24:14 +0000
committerpluralcafe-docker <git@plural.cafe>2019-01-12 04:24:14 +0000
commitae24ef877b82466cab026a76ea6ed8598ddaae18 (patch)
tree584c25ada377f2e6da49a9b45b3da947501b1831 /app/javascript/flavours/glitch/components/scrollable_list.js
parentfa1ab7fbe4166f3d8b481891dcc443df5aa212bb (diff)
parent2cfa55185a5fc7d93a160a4e9a4730aae6725b0f (diff)
Merge branch 'glitch'
Diffstat (limited to 'app/javascript/flavours/glitch/components/scrollable_list.js')
-rw-r--r--app/javascript/flavours/glitch/components/scrollable_list.js68
1 files changed, 63 insertions, 5 deletions
diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js
index 21d717b81..7cd0774a9 100644
--- a/app/javascript/flavours/glitch/components/scrollable_list.js
+++ b/app/javascript/flavours/glitch/components/scrollable_list.js
@@ -10,6 +10,8 @@ import classNames from 'classnames';
 import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen';
 import LoadingIndicator from './loading_indicator';
 
+const MOUSE_IDLE_DELAY = 300;
+
 export default class ScrollableList extends PureComponent {
 
   static contextTypes = {
@@ -56,11 +58,66 @@ export default class ScrollableList extends PureComponent {
       } else if (this.props.onScroll) {
         this.props.onScroll();
       }
+
+      if (!this.lastScrollWasSynthetic) {
+        // If the last scroll wasn't caused by setScrollTop(), assume it was
+        // intentional and cancel any pending scroll reset on mouse idle
+        this.scrollToTopOnMouseIdle = false;
+      }
+      this.lastScrollWasSynthetic = false;
     }
   }, 150, {
     trailing: true,
   });
 
+  mouseIdleTimer = null;
+  mouseMovedRecently = false;
+  lastScrollWasSynthetic = false;
+  scrollToTopOnMouseIdle = false;
+
+  setScrollTop = newScrollTop => {
+    if (this.node.scrollTop !== newScrollTop) {
+      this.lastScrollWasSynthetic = true;
+      this.node.scrollTop = newScrollTop;
+    }
+  };
+
+  clearMouseIdleTimer = () => {
+    if (this.mouseIdleTimer === null) {
+      return;
+    }
+    clearTimeout(this.mouseIdleTimer);
+    this.mouseIdleTimer = null;
+  };
+
+  handleMouseMove = throttle(() => {
+    // As long as the mouse keeps moving, clear and restart the idle timer.
+    this.clearMouseIdleTimer();
+    this.mouseIdleTimer =
+      setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
+
+    if (!this.mouseMovedRecently && this.node.scrollTop === 0) {
+      // Only set if we just started moving and are scrolled to the top.
+      this.scrollToTopOnMouseIdle = true;
+    }
+    // Save setting this flag for last, so we can do the comparison above.
+    this.mouseMovedRecently = true;
+  }, MOUSE_IDLE_DELAY / 2);
+
+  handleWheel = throttle(() => {
+    this.scrollToTopOnMouseIdle = false;
+  }, 150, {
+    trailing: true,
+  });
+
+  handleMouseIdle = () => {
+    if (this.scrollToTopOnMouseIdle) {
+      this.setScrollTop(0);
+    }
+    this.mouseMovedRecently = false;
+    this.scrollToTopOnMouseIdle = false;
+  }
+
   componentDidMount () {
     this.attachScrollListener();
     this.attachIntersectionObserver();
@@ -81,16 +138,14 @@ export default class ScrollableList extends PureComponent {
   updateScrollBottom = (snapshot) => {
     const newScrollTop = this.node.scrollHeight - snapshot;
 
-    if (this.node.scrollTop !== newScrollTop) {
-      this.node.scrollTop = newScrollTop;
-    }
+    this.setScrollTop(newScrollTop);
   }
 
   getSnapshotBeforeUpdate (prevProps, prevState) {
     const someItemInserted = React.Children.count(prevProps.children) > 0 &&
       React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
       this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
-    if (someItemInserted && this.node.scrollTop > 0) {
+    if (someItemInserted && (this.node.scrollTop > 0 || this.mouseMovedRecently)) {
       return this.node.scrollHeight - this.node.scrollTop;
     } else {
       return null;
@@ -104,6 +159,7 @@ export default class ScrollableList extends PureComponent {
   }
 
   componentWillUnmount () {
+    this.clearMouseIdleTimer();
     this.detachScrollListener();
     this.detachIntersectionObserver();
     detachFullscreenListener(this.onFullScreenChange);
@@ -126,10 +182,12 @@ export default class ScrollableList extends PureComponent {
 
   attachScrollListener () {
     this.node.addEventListener('scroll', this.handleScroll);
+    this.node.addEventListener('wheel', this.handleWheel);
   }
 
   detachScrollListener () {
     this.node.removeEventListener('scroll', this.handleScroll);
+    this.node.removeEventListener('wheel', this.handleWheel);
   }
 
   getFirstChildKey (props) {
@@ -181,7 +239,7 @@ export default class ScrollableList extends PureComponent {
       );
     } else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
       scrollableArea = (
-        <div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
+        <div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
           <div role='feed' className='item-list'>
             {prepend}