diff --git a/res/css/structures/_AutoHideScrollbar.scss b/res/css/structures/_AutoHideScrollbar.scss new file mode 100644 index 0000000000..0b8558a61e --- /dev/null +++ b/res/css/structures/_AutoHideScrollbar.scss @@ -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; +} diff --git a/src/components/structures/AutoHideScrollbar.js b/src/components/structures/AutoHideScrollbar.js new file mode 100644 index 0000000000..66c3b0d6e1 --- /dev/null +++ b/src/components/structures/AutoHideScrollbar.js @@ -0,0 +1,119 @@ +/* +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) +function getScrollbarWidth(alternativeOverflow) { + var e = document.createElement('div'), sw; + e.style.position = 'absolute'; + e.style.top = '-9999px'; + e.style.width = '100px'; + e.style.height = '100px'; + e.style.overflow = "scroll"; + if (alternativeOverflow) { + e.style.overflow = alternativeOverflow; + } + e.style.msOverflowStyle = '-ms-autohiding-scrollbar'; + document.body.appendChild(e); + sw = (e.offsetWidth - e.clientWidth); + document.body.removeChild(e); + return sw; +} + +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 (
+ { this.props.children } +
); + } +}