Merge pull request #2839 from matrix-org/travis/breadcrumbs/visual
Add custom tooltips and scrolling to breadcrumbspull/21833/head
commit
acb33e1aa0
|
@ -15,33 +15,29 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_RoomBreadcrumbs {
|
.mx_RoomBreadcrumbs {
|
||||||
overflow-x: auto;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 32px;
|
height: 42px;
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
overflow-x: visible;
|
||||||
overflow-x: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
> * {
|
.mx_AutoHideScrollbar_offset {
|
||||||
margin-left: 4px;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
.mx_RoomBreadcrumbs_crumb {
|
||||||
content: "";
|
margin-left: 4px;
|
||||||
position: absolute;
|
height: 32px;
|
||||||
width: 15px;
|
display: inline-block;
|
||||||
top: 0;
|
transition: transform 0.3s, width 0.3s;
|
||||||
right: 0;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(to right, $panel-gradient);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomBreadcrumbs_animate {
|
.mx_RoomBreadcrumbs_animate {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
transition: transform 0.3s, width 0.3s;
|
|
||||||
width: 32px;
|
width: 32px;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
@ -50,5 +46,34 @@ limitations under the License.
|
||||||
width: 0;
|
width: 0;
|
||||||
transform: scale(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,17 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
|
|
||||||
export default class IndicatorScrollbar extends React.Component {
|
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) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this._collectScroller = this._collectScroller.bind(this);
|
this._collectScroller = this._collectScroller.bind(this);
|
||||||
|
@ -25,6 +33,11 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
this.checkOverflow = this.checkOverflow.bind(this);
|
this.checkOverflow = this.checkOverflow.bind(this);
|
||||||
this._scrollElement = null;
|
this._scrollElement = null;
|
||||||
this._autoHideScrollbar = null;
|
this._autoHideScrollbar = null;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
leftIndicatorOffset: 0,
|
||||||
|
rightIndicatorOffset: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_collectScroller(scroller) {
|
_collectScroller(scroller) {
|
||||||
|
@ -43,6 +56,10 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
const hasTopOverflow = this._scrollElement.scrollTop > 0;
|
const hasTopOverflow = this._scrollElement.scrollTop > 0;
|
||||||
const hasBottomOverflow = this._scrollElement.scrollHeight >
|
const hasBottomOverflow = this._scrollElement.scrollHeight >
|
||||||
(this._scrollElement.scrollTop + this._scrollElement.clientHeight);
|
(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) {
|
if (hasTopOverflow) {
|
||||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
|
this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
|
||||||
} else {
|
} else {
|
||||||
|
@ -53,10 +70,30 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
|
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) {
|
if (this._autoHideScrollbar) {
|
||||||
this._autoHideScrollbar.checkOverflow();
|
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() {
|
getScrollTop() {
|
||||||
|
@ -70,12 +107,21 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const leftIndicatorStyle = {left: this.state.leftIndicatorOffset};
|
||||||
|
const rightIndicatorStyle = {right: this.state.rightIndicatorOffset};
|
||||||
|
const leftOverflowIndicator = this.props.trackHorizontalOverflow
|
||||||
|
? <div className="mx_IndicatorScrollbar_leftOverflowIndicator" style={leftIndicatorStyle} /> : null;
|
||||||
|
const rightOverflowIndicator = this.props.trackHorizontalOverflow
|
||||||
|
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
||||||
|
|
||||||
return (<AutoHideScrollbar
|
return (<AutoHideScrollbar
|
||||||
ref={this._collectScrollerComponent}
|
ref={this._collectScrollerComponent}
|
||||||
wrappedRef={this._collectScroller}
|
wrappedRef={this._collectScroller}
|
||||||
{... this.props}
|
{... this.props}
|
||||||
>
|
>
|
||||||
|
{ leftOverflowIndicator }
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
|
{ rightOverflowIndicator }
|
||||||
</AutoHideScrollbar>);
|
</AutoHideScrollbar>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import RoomAvatar from '../avatars/RoomAvatar';
|
import RoomAvatar from '../avatars/RoomAvatar';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import sdk from "../../../index";
|
||||||
|
|
||||||
const MAX_ROOMS = 20;
|
const MAX_ROOMS = 20;
|
||||||
|
|
||||||
|
@ -101,7 +102,26 @@ export default class RoomBreadcrumbs extends React.Component {
|
||||||
dis.dispatch({action: "view_room", room_id: room.roomId});
|
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() {
|
render() {
|
||||||
|
const Tooltip = sdk.getComponent('elements.Tooltip');
|
||||||
|
const IndicatorScrollbar = sdk.getComponent('structures.IndicatorScrollbar');
|
||||||
|
|
||||||
// check for collapsed here and
|
// check for collapsed here and
|
||||||
// not at parent so we keep
|
// not at parent so we keep
|
||||||
// rooms in our state
|
// rooms in our state
|
||||||
|
@ -110,18 +130,31 @@ export default class RoomBreadcrumbs extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const rooms = this.state.rooms;
|
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 isFirst = i === 0;
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
|
"mx_RoomBreadcrumbs_crumb": true,
|
||||||
"mx_RoomBreadcrumbs_preAnimate": isFirst && !animated,
|
"mx_RoomBreadcrumbs_preAnimate": isFirst && !animated,
|
||||||
"mx_RoomBreadcrumbs_animate": isFirst,
|
"mx_RoomBreadcrumbs_animate": isFirst,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let tooltip = null;
|
||||||
|
if (hover) {
|
||||||
|
tooltip = <Tooltip label={room.name} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleButton className={classes} key={room.roomId} title={room.name} onClick={() => this._viewRoom(room)}>
|
<AccessibleButton className={classes} key={room.roomId} onClick={() => this._viewRoom(room)}
|
||||||
|
onMouseEnter={() => this._onMouseEnter(room)} onMouseLeave={() => this._onMouseLeave(room)}>
|
||||||
<RoomAvatar room={room} width={32} height={32} />
|
<RoomAvatar room={room} width={32} height={32} />
|
||||||
|
{tooltip}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return (<div className="mx_RoomBreadcrumbs">{ avatars }</div>);
|
return (
|
||||||
|
<IndicatorScrollbar ref="scroller" className="mx_RoomBreadcrumbs" trackHorizontalOverflow={true}>
|
||||||
|
{ avatars }
|
||||||
|
</IndicatorScrollbar>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue