Merge pull request #2264 from matrix-org/bwindels/nativescrollbars
Redesign: use native auto-hiding scrollbars in room sub lists
This commit is contained in:
commit
b07514d21f
13 changed files with 196 additions and 20 deletions
|
@ -1,5 +1,6 @@
|
||||||
// autogenerated by rethemendex.sh
|
// autogenerated by rethemendex.sh
|
||||||
@import "./_common.scss";
|
@import "./_common.scss";
|
||||||
|
@import "./structures/_AutoHideScrollbar.scss";
|
||||||
@import "./structures/_CompatibilityPage.scss";
|
@import "./structures/_CompatibilityPage.scss";
|
||||||
@import "./structures/_ContextualMenu.scss";
|
@import "./structures/_ContextualMenu.scss";
|
||||||
@import "./structures/_CreateRoom.scss";
|
@import "./structures/_CreateRoom.scss";
|
||||||
|
|
56
res/css/structures/_AutoHideScrollbar.scss
Normal file
56
res/css/structures/_AutoHideScrollbar.scss
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. for browsers that support native overlay auto-hiding scrollbars
|
||||||
|
*/
|
||||||
|
.mx_AutoHideScrollbar {
|
||||||
|
overflow-y: auto;
|
||||||
|
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
2. webkit also supports overflow:overlay where the scrollbars don't take any space
|
||||||
|
in the layout but they don't autohide, so do that only on hover
|
||||||
|
*/
|
||||||
|
body.mx_scrollbar_overlay_noautohide .mx_AutoHideScrollbar {
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mx_scrollbar_overlay_noautohide .mx_AutoHideScrollbar:hover {
|
||||||
|
overflow-y: overlay;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
3. as a last fallback, compensate for the scrollbar taking up space in the layout
|
||||||
|
by playing with the paddings. the default below will add a right padding
|
||||||
|
of the scrollbar width and clear that on hover.
|
||||||
|
this won't work well on classes that also need to set their padding,
|
||||||
|
so this needs to be overriden and adjust the padding with calc like so:
|
||||||
|
```
|
||||||
|
body.mx_scrollbar_nooverlay .componentClass.mx_AutoHideScrollbar_overflow:hover {
|
||||||
|
padding-right: calc(15px - var(--scrollbar-width)) !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
body.mx_scrollbar_nooverlay .mx_AutoHideScrollbar {
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: hidden;
|
||||||
|
padding-right: var(--scrollbar-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mx_scrollbar_nooverlay .mx_AutoHideScrollbar:hover {
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
|
@ -16,14 +16,12 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_RoomSubList {
|
.mx_RoomSubList {
|
||||||
min-height: 31px;
|
min-height: 31px;
|
||||||
flex: 0 0 auto;
|
flex: 0 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSubList_nonEmpty {
|
.mx_RoomSubList_nonEmpty {
|
||||||
min-height: 80px;
|
|
||||||
flex: 1;
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,9 +129,18 @@ limitations under the License.
|
||||||
transform: rotateZ(-90deg);
|
transform: rotateZ(-90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSubList .gm-scrollbar-container {
|
.mx_RoomSubList_scroll {
|
||||||
/* let rooms list grab all available space */
|
/* let rooms list grab all available space */
|
||||||
flex: 1;
|
flex: 0 1 auto;
|
||||||
|
padding: 0 15px !important;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
for browsers that don't support overlay scrollbars,
|
||||||
|
subtract scrollbar width from right padding on hover when overflowing
|
||||||
|
so the content doesn't jump when showing the scrollbars
|
||||||
|
*/
|
||||||
|
body.mx_scrollbar_nooverlay .mx_RoomSubList_scroll.mx_AutoHideScrollbar_overflow:hover {
|
||||||
|
padding-right: calc(15px - var(--scrollbar-width)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsed {
|
.collapsed {
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_TopLeftMenuButton {
|
.mx_TopLeftMenuButton {
|
||||||
height: 52px;
|
flex: 0 0 52px;
|
||||||
border-bottom: 1px solid $panel-divider-color;
|
border-bottom: 1px solid $panel-divider-color;
|
||||||
color: $topleftmenu-color;
|
color: $topleftmenu-color;
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_RoomList {
|
.mx_RoomList {
|
||||||
/* take up remaining space below TopLeftMenu */
|
/* take up remaining space below TopLeftMenu */
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
/* use flexbox to layout sublists */
|
/* use flexbox to layout sublists */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
margin: 0 12px;
|
margin: 0;
|
||||||
padding: 2px 12px;
|
padding: 2px 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: $secondary-accent-color;
|
background-color: $secondary-accent-color;
|
||||||
|
|
118
src/components/structures/AutoHideScrollbar.js
Normal file
118
src/components/structures/AutoHideScrollbar.js
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
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";
|
||||||
|
|
||||||
|
// derived from code from github.com/noeldelgado/gemini-scrollbar
|
||||||
|
// Copyright (c) Noel Delgado <pixelia.me@gmail.com> (pixelia.me)
|
||||||
|
function getScrollbarWidth(alternativeOverflow) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.style.position = 'absolute';
|
||||||
|
div.style.top = '-9999px';
|
||||||
|
div.style.width = '100px';
|
||||||
|
div.style.height = '100px';
|
||||||
|
div.style.overflow = "scroll";
|
||||||
|
if (alternativeOverflow) {
|
||||||
|
div.style.overflow = alternativeOverflow;
|
||||||
|
}
|
||||||
|
div.style.msOverflowStyle = '-ms-autohiding-scrollbar';
|
||||||
|
document.body.appendChild(div);
|
||||||
|
const scrollbarWidth = (div.offsetWidth - div.clientWidth);
|
||||||
|
document.body.removeChild(div);
|
||||||
|
return scrollbarWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function install() {
|
||||||
|
const scrollbarWidth = getScrollbarWidth();
|
||||||
|
if (scrollbarWidth !== 0) {
|
||||||
|
const hasForcedOverlayScrollbar = getScrollbarWidth('overlay') === 0;
|
||||||
|
// overflow: overlay on webkit doesn't auto hide the scrollbar
|
||||||
|
if (hasForcedOverlayScrollbar) {
|
||||||
|
document.body.classList.add("mx_scrollbar_overlay_noautohide");
|
||||||
|
} else {
|
||||||
|
document.body.classList.add("mx_scrollbar_nooverlay");
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.type = 'text/css';
|
||||||
|
style.innerText =
|
||||||
|
`body.mx_scrollbar_nooverlay { --scrollbar-width: ${scrollbarWidth}px; }`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const installBodyClassesIfNeeded = (function() {
|
||||||
|
let installed = false;
|
||||||
|
return function() {
|
||||||
|
if (!installed) {
|
||||||
|
install();
|
||||||
|
installed = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
export default class AutoHideScrollbar extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.onOverflow = this.onOverflow.bind(this);
|
||||||
|
this.onUnderflow = this.onUnderflow.bind(this);
|
||||||
|
this._collectContainerRef = this._collectContainerRef.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onOverflow() {
|
||||||
|
this.containerRef.classList.add("mx_AutoHideScrollbar_overflow");
|
||||||
|
this.containerRef.classList.remove("mx_AutoHideScrollbar_underflow");
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnderflow() {
|
||||||
|
this.containerRef.classList.remove("mx_AutoHideScrollbar_overflow");
|
||||||
|
this.containerRef.classList.add("mx_AutoHideScrollbar_underflow");
|
||||||
|
}
|
||||||
|
|
||||||
|
_collectContainerRef(ref) {
|
||||||
|
if (ref && !this.containerRef) {
|
||||||
|
this.containerRef = ref;
|
||||||
|
const needsOverflowListener =
|
||||||
|
document.body.classList.contains("mx_scrollbar_nooverlay");
|
||||||
|
|
||||||
|
if (needsOverflowListener) {
|
||||||
|
this.containerRef.addEventListener("overflow", this.onOverflow);
|
||||||
|
this.containerRef.addEventListener("underflow", this.onUnderflow);
|
||||||
|
}
|
||||||
|
if (ref.scrollHeight > ref.clientHeight) {
|
||||||
|
this.onOverflow();
|
||||||
|
} else {
|
||||||
|
this.onUnderflow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.containerRef) {
|
||||||
|
this.containerRef.removeEventListener("overflow", this.onOverflow);
|
||||||
|
this.containerRef.removeEventListener("underflow", this.onUnderflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
installBodyClassesIfNeeded();
|
||||||
|
return (<div
|
||||||
|
ref={this._collectContainerRef}
|
||||||
|
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
|
||||||
|
>
|
||||||
|
{ this.props.children }
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
|
@ -179,10 +179,9 @@ const LeftPanel = React.createClass({
|
||||||
const RoomList = sdk.getComponent('rooms.RoomList');
|
const RoomList = sdk.getComponent('rooms.RoomList');
|
||||||
const TagPanel = sdk.getComponent('structures.TagPanel');
|
const TagPanel = sdk.getComponent('structures.TagPanel');
|
||||||
const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton');
|
const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton');
|
||||||
const BottomLeftMenu = sdk.getComponent('structures.BottomLeftMenu');
|
|
||||||
const CallPreview = sdk.getComponent('voip.CallPreview');
|
const CallPreview = sdk.getComponent('voip.CallPreview');
|
||||||
|
|
||||||
let topBox = <TopLeftMenuButton collapsed={ this.props.collapsed }/>;
|
const topBox = <TopLeftMenuButton collapsed={ this.props.collapsed } />;
|
||||||
/*
|
/*
|
||||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||||
const topBox = <SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />;
|
const topBox = <SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />;
|
||||||
|
|
|
@ -23,6 +23,7 @@ import dis from '../../dispatcher';
|
||||||
import Unread from '../../Unread';
|
import Unread from '../../Unread';
|
||||||
import * as RoomNotifs from '../../RoomNotifs';
|
import * as RoomNotifs from '../../RoomNotifs';
|
||||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||||
|
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||||
import { KeyCode } from '../../Keyboard';
|
import { KeyCode } from '../../Keyboard';
|
||||||
import { Group } from 'matrix-js-sdk';
|
import { Group } from 'matrix-js-sdk';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
@ -348,19 +349,17 @@ const RoomSubList = React.createClass({
|
||||||
{this._getHeaderJsx()}
|
{this._getHeaderJsx()}
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
const heightEstimation = (len * 40) + 31 + (8 + 8);
|
const heightEstimation = (len * 44) + 31 + (8 + 8);
|
||||||
const style = {
|
const style = {
|
||||||
flexGrow: `${heightEstimation}`,
|
|
||||||
maxHeight: `${heightEstimation}px`,
|
maxHeight: `${heightEstimation}px`,
|
||||||
};
|
};
|
||||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
|
||||||
const tiles = this.makeRoomTiles();
|
const tiles = this.makeRoomTiles();
|
||||||
tiles.push(...this.props.extraTiles);
|
tiles.push(...this.props.extraTiles);
|
||||||
return <div className={subListClasses} style={style}>
|
return <div style={style} className={subListClasses}>
|
||||||
{this._getHeaderJsx()}
|
{this._getHeaderJsx()}
|
||||||
<GeminiScrollbarWrapper>
|
<AutoHideScrollbar className="mx_RoomSubList_scroll">
|
||||||
{ tiles }
|
{ tiles }
|
||||||
</GeminiScrollbarWrapper>
|
</AutoHideScrollbar>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -26,7 +26,6 @@ import Avatar from '../../Avatar';
|
||||||
const AVATAR_SIZE = 28;
|
const AVATAR_SIZE = 28;
|
||||||
|
|
||||||
export default class TopLeftMenuButton extends React.Component {
|
export default class TopLeftMenuButton extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
collapsed: PropTypes.bool.isRequired,
|
collapsed: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,6 @@ import dis from '../../../dispatcher';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
export class TopLeftMenu extends React.Component {
|
export class TopLeftMenu extends React.Component {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.openSettings = this.openSettings.bind(this);
|
this.openSettings = this.openSettings.bind(this);
|
||||||
|
|
|
@ -74,7 +74,6 @@ class CollapseDistributor extends FixedDistributor {
|
||||||
}
|
}
|
||||||
|
|
||||||
class PercentageDistributor {
|
class PercentageDistributor {
|
||||||
|
|
||||||
constructor(sizer, item, _config, items, container) {
|
constructor(sizer, item, _config, items, container) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.totalSize = sizer.getTotalSize();
|
this.totalSize = sizer.getTotalSize();
|
||||||
|
|
|
@ -34,7 +34,6 @@ class RoomSizer extends Sizer {
|
||||||
item.style.maxHeight = `${Math.round(size)}px`;
|
item.style.maxHeight = `${Math.round(size)}px`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class RoomDistributor extends FixedDistributor {
|
class RoomDistributor extends FixedDistributor {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue