From 0f226853f5466b8c4d71075b6bf03242512a19b0 Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Mon, 12 Nov 2018 12:57:21 +0100
Subject: [PATCH] add scroll indicator gradients to top and bottom of room sub
 list

---
 res/css/structures/_AutoHideScrollbar.scss    | 28 ++++++---
 res/css/structures/_RoomSubList.scss          | 30 +++++++++
 .../structures/AutoHideScrollbar.js           |  3 +
 .../structures/IndicatorScrollbar.js          | 62 +++++++++++++++++++
 src/components/structures/RoomSubList.js      |  6 +-
 5 files changed, 118 insertions(+), 11 deletions(-)
 create mode 100644 src/components/structures/IndicatorScrollbar.js

diff --git a/res/css/structures/_AutoHideScrollbar.scss b/res/css/structures/_AutoHideScrollbar.scss
index 70dbbe8d07..60aea7ce8f 100644
--- a/res/css/structures/_AutoHideScrollbar.scss
+++ b/res/css/structures/_AutoHideScrollbar.scss
@@ -41,14 +41,26 @@ is overflowing. This is what Firefox ends up using. Overflow is detected
 in javascript, and adds the mx_AutoHideScrollbar_overflow class to the container.
 This only works in Firefox, which should be fine as this fallback is only needed there.
 */
-body.mx_scrollbar_nooverlay .mx_AutoHideScrollbar {
-    overflow-y: hidden;
-}
+body.mx_scrollbar_nooverlay {
+    .mx_AutoHideScrollbar {
+        overflow-y: hidden;
+    }
 
-body.mx_scrollbar_nooverlay .mx_AutoHideScrollbar:hover {
-    overflow-y: auto;
-}
+    .mx_AutoHideScrollbar:hover {
+        overflow-y: auto;
+    }
 
-body.mx_scrollbar_nooverlay .mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow > .mx_AutoHideScrollbar_offset {
-    margin-right: calc(-1 * var(--scrollbar-width));
+    /*
+    offset scrollbar width with negative margin-right
+
+    include before and after psuedo-elements here so they can
+    be used to do something interesting like scroll-indicating
+    gradients (see IndicatorScrollBar)
+    */
+    .mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow > .mx_AutoHideScrollbar_offset,
+    .mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow::before,
+    .mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow::after
+    {
+        margin-right: calc(-1 * var(--scrollbar-width));
+    }
 }
diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss
index 142a43af95..c156f1f07e 100644
--- a/res/css/structures/_RoomSubList.scss
+++ b/res/css/structures/_RoomSubList.scss
@@ -110,6 +110,36 @@ limitations under the License.
     padding: 0 8px;
 }
 
+.mx_RoomSubList_scroll.mx_IndicatorScrollbar_topOverflow::before,
+.mx_RoomSubList_scroll.mx_IndicatorScrollbar_bottomOverflow::after {
+    position: sticky;
+    left: 0;
+    right: 0;
+    height: 40px;
+    content: "";
+    display: block;
+    z-index: 100;
+    pointer-events: none;
+}
+
+
+.mx_RoomSubList_scroll.mx_IndicatorScrollbar_topOverflow > .mx_AutoHideScrollbar_offset {
+    margin-top: -40px;
+}
+.mx_RoomSubList_scroll.mx_IndicatorScrollbar_bottomOverflow > .mx_AutoHideScrollbar_offset {
+    margin-bottom: -40px;
+}
+
+.mx_RoomSubList_scroll.mx_IndicatorScrollbar_topOverflow::before {
+    top: 0;
+    background: linear-gradient($secondary-accent-color, transparent);
+}
+
+.mx_RoomSubList_scroll.mx_IndicatorScrollbar_bottomOverflow::after {
+    bottom: 0;
+    background: linear-gradient(transparent, $secondary-accent-color);
+}
+
 .collapsed {
 
     .mx_RoomSubList_scroll {
diff --git a/src/components/structures/AutoHideScrollbar.js b/src/components/structures/AutoHideScrollbar.js
index a462b2bf14..a328d478bc 100644
--- a/src/components/structures/AutoHideScrollbar.js
+++ b/src/components/structures/AutoHideScrollbar.js
@@ -97,6 +97,9 @@ export default class AutoHideScrollbar extends React.Component {
                 this.onUnderflow();
             }
         }
+        if (this.props.wrappedRef) {
+            this.props.wrappedRef(ref);
+        }
     }
 
     componentWillUnmount() {
diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js
new file mode 100644
index 0000000000..247131cfab
--- /dev/null
+++ b/src/components/structures/IndicatorScrollbar.js
@@ -0,0 +1,62 @@
+/*
+Copyright 2018 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import AutoHideScrollbar from "./AutoHideScrollbar";
+
+export default class IndicatorScrollbar extends React.Component {
+    constructor(props) {
+        super(props);
+        this._collectScroller = this._collectScroller.bind(this);
+        this.checkOverflow = this.checkOverflow.bind(this);
+    }
+
+    _collectScroller(scroller) {
+        if (scroller && !this._scroller) {
+            this._scroller = scroller;
+            this._scroller.addEventListener("scroll", this.checkOverflow);
+            this.checkOverflow();
+        }
+    }
+
+    checkOverflow() {
+        const hasTopOverflow = this._scroller.scrollTop > 0;
+        const hasBottomOverflow = this._scroller.scrollHeight >
+            (this._scroller.scrollTop + this._scroller.clientHeight);
+        if (hasTopOverflow) {
+            this._scroller.classList.add("mx_IndicatorScrollbar_topOverflow");
+        } else {
+            this._scroller.classList.remove("mx_IndicatorScrollbar_topOverflow");
+        }
+        if (hasBottomOverflow) {
+            this._scroller.classList.add("mx_IndicatorScrollbar_bottomOverflow");
+        } else {
+            this._scroller.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
+        }
+    }
+
+    componentWillUnmount() {
+        if (this._scroller) {
+            this._scroller.removeEventListener("scroll", this.checkOverflow);
+        }
+    }
+
+    render() {
+        return (<AutoHideScrollbar wrappedRef={this._collectScroller} {... this.props}>
+            { this.props.children }
+        </AutoHideScrollbar>);
+    }
+}
diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js
index 0bebc50da6..6399fd80d9 100644
--- a/src/components/structures/RoomSubList.js
+++ b/src/components/structures/RoomSubList.js
@@ -23,7 +23,7 @@ import dis from '../../dispatcher';
 import Unread from '../../Unread';
 import * as RoomNotifs from '../../RoomNotifs';
 import * as FormattingUtils from '../../utils/FormattingUtils';
-import AutoHideScrollbar from './AutoHideScrollbar';
+import IndicatorScrollbar from './IndicatorScrollbar';
 import { KeyCode } from '../../Keyboard';
 import { Group } from 'matrix-js-sdk';
 import PropTypes from 'prop-types';
@@ -333,9 +333,9 @@ const RoomSubList = React.createClass({
                 tiles.push(...this.props.extraTiles);
                 return <div style={style} className={subListClasses}>
                     {this._getHeaderJsx()}
-                    <AutoHideScrollbar className="mx_RoomSubList_scroll">
+                    <IndicatorScrollbar className="mx_RoomSubList_scroll">
                         { tiles }
-                    </AutoHideScrollbar>
+                    </IndicatorScrollbar>
                 </div>;
             }
         } else {