diff --git a/res/css/views/rooms/_RoomBreadcrumbs.scss b/res/css/views/rooms/_RoomBreadcrumbs.scss index c5a149d5cd3..dbd1d3cf5fc 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs.scss @@ -15,33 +15,29 @@ limitations under the License. */ .mx_RoomBreadcrumbs { - overflow-x: auto; position: relative; - height: 32px; + height: 42px; margin: 8px; margin-bottom: 0; - - overflow-x: hidden; + overflow-x: visible; display: flex; flex-direction: row; - > * { - margin-left: 4px; + .mx_AutoHideScrollbar_offset { + display: flex; + flex-direction: row; + height: 100%; } - &::after { - content: ""; - position: absolute; - width: 15px; - top: 0; - right: 0; - height: 100%; - background: linear-gradient(to right, $panel-gradient); + .mx_RoomBreadcrumbs_crumb { + margin-left: 4px; + height: 32px; + display: inline-block; + transition: transform 0.3s, width 0.3s; } .mx_RoomBreadcrumbs_animate { margin-left: 0; - transition: transform 0.3s, width 0.3s; width: 32px; transform: scale(1); } @@ -50,5 +46,34 @@ limitations under the License. width: 0; transform: scale(0); } + + + // Note: we have to manually control the gradient and stuff, but the IndicatorScrollbar + // will deal with left/right positioning for us. Normally we'd use position:sticky on + // a few key elements, however that doesn't work in horizontal scrolling scenarios. + + .mx_IndicatorScrollbar_leftOverflowIndicator, + .mx_IndicatorScrollbar_rightOverflowIndicator { + display: none; + } + + &.mx_IndicatorScrollbar_leftOverflow .mx_IndicatorScrollbar_leftOverflowIndicator, + &.mx_IndicatorScrollbar_rightOverflow .mx_IndicatorScrollbar_rightOverflowIndicator { + position: absolute; + top: 0; + bottom: 0; + width: 15px; + display: block; + pointer-events: none; + z-index: 100; + } + + .mx_IndicatorScrollbar_leftOverflowIndicator { + background: linear-gradient(to left, $panel-gradient); + } + + .mx_IndicatorScrollbar_rightOverflowIndicator { + background: linear-gradient(to right, $panel-gradient); + } } diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index e1516d1f64c..263a0a22bac 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -15,9 +15,17 @@ limitations under the License. */ import React from "react"; +import PropTypes from "prop-types"; import AutoHideScrollbar from "./AutoHideScrollbar"; export default class IndicatorScrollbar extends React.Component { + static PropTypes = { + // If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator + // and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning + // by the parent element. + trackHorizontalOverflow: PropTypes.bool, + }; + constructor(props) { super(props); this._collectScroller = this._collectScroller.bind(this); @@ -25,6 +33,11 @@ export default class IndicatorScrollbar extends React.Component { this.checkOverflow = this.checkOverflow.bind(this); this._scrollElement = null; this._autoHideScrollbar = null; + + this.state = { + leftIndicatorOffset: 0, + rightIndicatorOffset: 0, + }; } _collectScroller(scroller) { @@ -43,6 +56,10 @@ export default class IndicatorScrollbar extends React.Component { const hasTopOverflow = this._scrollElement.scrollTop > 0; const hasBottomOverflow = this._scrollElement.scrollHeight > (this._scrollElement.scrollTop + this._scrollElement.clientHeight); + const hasLeftOverflow = this._scrollElement.scrollLeft > 0; + const hasRightOverflow = this._scrollElement.scrollWidth > + (this._scrollElement.scrollLeft + this._scrollElement.clientWidth); + if (hasTopOverflow) { this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow"); } else { @@ -53,10 +70,30 @@ export default class IndicatorScrollbar extends React.Component { } else { this._scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow"); } + if (hasLeftOverflow) { + this._scrollElement.classList.add("mx_IndicatorScrollbar_leftOverflow"); + } else { + this._scrollElement.classList.remove("mx_IndicatorScrollbar_leftOverflow"); + } + if (hasRightOverflow) { + this._scrollElement.classList.add("mx_IndicatorScrollbar_rightOverflow"); + } else { + this._scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow"); + } if (this._autoHideScrollbar) { this._autoHideScrollbar.checkOverflow(); } + + if (this.props.trackHorizontalOverflow) { + this.setState({ + // Offset from absolute position of the container + leftIndicatorOffset: hasLeftOverflow ? `${this._scrollElement.scrollLeft}px` : '0', + + // Negative because we're coming from the right + rightIndicatorOffset: hasRightOverflow ? `-${this._scrollElement.scrollLeft}px` : '0', + }); + } } getScrollTop() { @@ -70,12 +107,21 @@ export default class IndicatorScrollbar extends React.Component { } render() { + const leftIndicatorStyle = {left: this.state.leftIndicatorOffset}; + const rightIndicatorStyle = {right: this.state.rightIndicatorOffset}; + const leftOverflowIndicator = this.props.trackHorizontalOverflow + ?
: null; + const rightOverflowIndicator = this.props.trackHorizontalOverflow + ?
: null; + return ( + { leftOverflowIndicator } { this.props.children } + { rightOverflowIndicator } ); } } diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index d866cdcd184..1feff19ca2a 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -21,6 +21,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import AccessibleButton from '../elements/AccessibleButton'; import RoomAvatar from '../avatars/RoomAvatar'; import classNames from 'classnames'; +import sdk from "../../../index"; const MAX_ROOMS = 20; @@ -101,7 +102,26 @@ export default class RoomBreadcrumbs extends React.Component { dis.dispatch({action: "view_room", room_id: room.roomId}); } + _onMouseEnter(room) { + this._onHover(room); + } + + _onMouseLeave(room) { + this._onHover(null); // clear hover states + } + + _onHover(room) { + const rooms = this.state.rooms.slice(); + for (const r of rooms) { + r.hover = room && r.room.roomId === room.roomId; + } + this.setState({rooms}); + } + render() { + const Tooltip = sdk.getComponent('elements.Tooltip'); + const IndicatorScrollbar = sdk.getComponent('structures.IndicatorScrollbar'); + // check for collapsed here and // not at parent so we keep // rooms in our state @@ -110,18 +130,31 @@ export default class RoomBreadcrumbs extends React.Component { return null; } const rooms = this.state.rooms; - const avatars = rooms.map(({room, animated}, i) => { + const avatars = rooms.map(({room, animated, hover}, i) => { const isFirst = i === 0; const classes = classNames({ + "mx_RoomBreadcrumbs_crumb": true, "mx_RoomBreadcrumbs_preAnimate": isFirst && !animated, "mx_RoomBreadcrumbs_animate": isFirst, }); + + let tooltip = null; + if (hover) { + tooltip = ; + } + return ( - this._viewRoom(room)}> + this._viewRoom(room)} + onMouseEnter={() => this._onMouseEnter(room)} onMouseLeave={() => this._onMouseLeave(room)}> + {tooltip} ); }); - return (
{ avatars }
); + return ( + + { avatars } + + ); } }