Merge pull request #2364 from matrix-org/bwindels/fixresizepersistenceandscrollindicator-again

Redesign: left panel fixes
This commit is contained in:
Bruno Windels 2018-12-18 14:23:38 +00:00 committed by GitHub
commit acba36c975
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 106 additions and 47 deletions

View file

@ -19,14 +19,14 @@ limitations under the License.
each with a flex-shrink difference of 4 order of magnitude, each with a flex-shrink difference of 4 order of magnitude,
so they ideally wouldn't affect each other. so they ideally wouldn't affect each other.
lowest category: .mx_RoomSubList lowest category: .mx_RoomSubList
flex:-shrink: 10000000 flex-shrink: 10000000
distribute size of items within the same categery by their size distribute size of items within the same categery by their size
middle category: .mx_RoomSubList.resized-sized middle category: .mx_RoomSubList.resized-sized
flex:-shrink: 1000 flex-shrink: 1000
applied when using the resizer, will have a max-height set to it, applied when using the resizer, will have a max-height set to it,
to limit the size to limit the size
highest category: .mx_RoomSubList.resized-all highest category: .mx_RoomSubList.resized-all
flex:-shrink: 1 flex-shrink: 1
small flex-shrink value (1), is only added if you can drag the resizer so far small flex-shrink value (1), is only added if you can drag the resizer so far
so in practice you can only assign this category if there is enough space. so in practice you can only assign this category if there is enough space.
*/ */
@ -39,7 +39,7 @@ limitations under the License.
} }
.mx_RoomSubList_nonEmpty { .mx_RoomSubList_nonEmpty {
min-height: 76px; min-height: 70px;
.mx_AutoHideScrollbar_offset { .mx_AutoHideScrollbar_offset {
padding-bottom: 4px; padding-bottom: 4px;
@ -154,7 +154,7 @@ limitations under the License.
position: sticky; position: sticky;
left: 0; left: 0;
right: 0; right: 0;
height: 40px; height: 30px;
content: ""; content: "";
display: block; display: block;
z-index: 100; z-index: 100;
@ -162,10 +162,10 @@ limitations under the License.
} }
&.mx_IndicatorScrollbar_topOverflow > .mx_AutoHideScrollbar_offset { &.mx_IndicatorScrollbar_topOverflow > .mx_AutoHideScrollbar_offset {
margin-top: -40px; margin-top: -30px;
} }
&.mx_IndicatorScrollbar_bottomOverflow > .mx_AutoHideScrollbar_offset { &.mx_IndicatorScrollbar_bottomOverflow > .mx_AutoHideScrollbar_offset {
margin-bottom: -40px; margin-bottom: -30px;
} }
&.mx_IndicatorScrollbar_topOverflow::before { &.mx_IndicatorScrollbar_topOverflow::before {

View file

@ -69,6 +69,7 @@ export default class AutoHideScrollbar extends React.Component {
this.onOverflow = this.onOverflow.bind(this); this.onOverflow = this.onOverflow.bind(this);
this.onUnderflow = this.onUnderflow.bind(this); this.onUnderflow = this.onUnderflow.bind(this);
this._collectContainerRef = this._collectContainerRef.bind(this); this._collectContainerRef = this._collectContainerRef.bind(this);
this._needsOverflowListener = null;
} }
onOverflow() { onOverflow() {
@ -81,36 +82,49 @@ export default class AutoHideScrollbar extends React.Component {
this.containerRef.classList.add("mx_AutoHideScrollbar_underflow"); this.containerRef.classList.add("mx_AutoHideScrollbar_underflow");
} }
_collectContainerRef(ref) { checkOverflow() {
if (ref && !this.containerRef) { if (!this._needsOverflowListener) {
this.containerRef = ref; return;
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) { if (this.containerRef.scrollHeight > this.containerRef.clientHeight) {
this.onOverflow(); this.onOverflow();
} else { } else {
this.onUnderflow(); this.onUnderflow();
} }
} }
componentDidUpdate() {
this.checkOverflow();
}
componentDidMount() {
installBodyClassesIfNeeded();
this._needsOverflowListener =
document.body.classList.contains("mx_scrollbar_nooverlay");
if (this._needsOverflowListener) {
this.containerRef.addEventListener("overflow", this.onOverflow);
this.containerRef.addEventListener("underflow", this.onUnderflow);
}
this.checkOverflow();
}
_collectContainerRef(ref) {
if (ref && !this.containerRef) {
this.containerRef = ref;
}
if (this.props.wrappedRef) { if (this.props.wrappedRef) {
this.props.wrappedRef(ref); this.props.wrappedRef(ref);
} }
} }
componentWillUnmount() { componentWillUnmount() {
if (this.containerRef) { if (this._needsOverflowListener && this.containerRef) {
this.containerRef.removeEventListener("overflow", this.onOverflow); this.containerRef.removeEventListener("overflow", this.onOverflow);
this.containerRef.removeEventListener("underflow", this.onUnderflow); this.containerRef.removeEventListener("underflow", this.onUnderflow);
} }
} }
render() { render() {
installBodyClassesIfNeeded();
return (<div return (<div
ref={this._collectContainerRef} ref={this._collectContainerRef}
className={["mx_AutoHideScrollbar", this.props.className].join(" ")} className={["mx_AutoHideScrollbar", this.props.className].join(" ")}

View file

@ -21,41 +21,52 @@ export default class IndicatorScrollbar extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this._collectScroller = this._collectScroller.bind(this); this._collectScroller = this._collectScroller.bind(this);
this._collectScrollerComponent = this._collectScrollerComponent.bind(this);
this.checkOverflow = this.checkOverflow.bind(this); this.checkOverflow = this.checkOverflow.bind(this);
this._scrollElement = null;
this._autoHideScrollbar = null;
} }
_collectScroller(scroller) { _collectScroller(scroller) {
if (scroller && !this._scroller) { if (scroller && !this._scrollElement) {
this._scroller = scroller; this._scrollElement = scroller;
this._scroller.addEventListener("scroll", this.checkOverflow); this._scrollElement.addEventListener("scroll", this.checkOverflow);
this.checkOverflow(); this.checkOverflow();
} }
} }
_collectScrollerComponent(autoHideScrollbar) {
this._autoHideScrollbar = autoHideScrollbar;
}
checkOverflow() { checkOverflow() {
const hasTopOverflow = this._scroller.scrollTop > 0; const hasTopOverflow = this._scrollElement.scrollTop > 0;
const hasBottomOverflow = this._scroller.scrollHeight > const hasBottomOverflow = this._scrollElement.scrollHeight >
(this._scroller.scrollTop + this._scroller.clientHeight); (this._scrollElement.scrollTop + this._scrollElement.clientHeight);
if (hasTopOverflow) { if (hasTopOverflow) {
this._scroller.classList.add("mx_IndicatorScrollbar_topOverflow"); this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
} else { } else {
this._scroller.classList.remove("mx_IndicatorScrollbar_topOverflow"); this._scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow");
} }
if (hasBottomOverflow) { if (hasBottomOverflow) {
this._scroller.classList.add("mx_IndicatorScrollbar_bottomOverflow"); this._scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow");
} else { } else {
this._scroller.classList.remove("mx_IndicatorScrollbar_bottomOverflow"); this._scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
}
if (this._autoHideScrollbar) {
this._autoHideScrollbar.checkOverflow();
} }
} }
componentWillUnmount() { componentWillUnmount() {
if (this._scroller) { if (this._scrollElement) {
this._scroller.removeEventListener("scroll", this.checkOverflow); this._scrollElement.removeEventListener("scroll", this.checkOverflow);
} }
} }
render() { render() {
return (<AutoHideScrollbar wrappedRef={this._collectScroller} {... this.props}> return (<AutoHideScrollbar ref={this._collectScrollerComponent} wrappedRef={this._collectScroller} {... this.props}>
{ this.props.children } { this.props.children }
</AutoHideScrollbar>); </AutoHideScrollbar>);
} }

View file

@ -110,8 +110,9 @@ const RoomSubList = React.createClass({
if (this.isCollapsableOnClick()) { if (this.isCollapsableOnClick()) {
// The header isCollapsable, so the click is to be interpreted as collapse and truncation logic // The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
const isHidden = !this.state.hidden; const isHidden = !this.state.hidden;
this.setState({hidden: isHidden}); this.setState({hidden: isHidden}, () => {
this.props.onHeaderClick(isHidden); this.props.onHeaderClick(isHidden);
});
} else { } else {
// The header is stuck, so the click is to be interpreted as a scroll to the header // The header is stuck, so the click is to be interpreted as a scroll to the header
this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition); this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition);

View file

@ -21,6 +21,7 @@ const ResizeHandle = (props) => {
ResizeHandle.propTypes = { ResizeHandle.propTypes = {
vertical: PropTypes.bool, vertical: PropTypes.bool,
reverse: PropTypes.bool, reverse: PropTypes.bool,
id: PropTypes.string,
}; };
export default ResizeHandle; export default ResizeHandle;

View file

@ -152,6 +152,8 @@ module.exports = React.createClass({
} }
this.subListSizes[id] = newSize; this.subListSizes[id] = newSize;
window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.subListSizes)); window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.subListSizes));
// update overflow indicators
this._checkSubListsOverflow();
}, },
componentDidMount: function() { componentDidMount: function() {
@ -167,12 +169,10 @@ module.exports = React.createClass({
}); });
// load stored sizes // load stored sizes
Object.entries(this.subListSizes).forEach(([id, size]) => { Object.keys(this.subListSizes).forEach((key) => {
const handle = this.resizer.forHandleWithId(id); this._restoreSubListSize(key);
if (handle) {
handle.resize(size);
}
}); });
this._checkSubListsOverflow();
this.resizer.attach(); this.resizer.attach();
this.mounted = true; this.mounted = true;
@ -181,7 +181,11 @@ module.exports = React.createClass({
componentDidUpdate: function(prevProps) { componentDidUpdate: function(prevProps) {
this._repositionIncomingCallBox(undefined, false); this._repositionIncomingCallBox(undefined, false);
if (this.props.searchFilter !== prevProps.searchFilter) { if (this.props.searchFilter !== prevProps.searchFilter) {
Object.values(this._subListRefs).forEach(l => l.checkOverflow()); // restore sizes
Object.keys(this.subListSizes).forEach((key) => {
this._restoreSubListSize(key);
});
this._checkSubListsOverflow();
} }
}, },
@ -354,6 +358,11 @@ module.exports = React.createClass({
// Do this here so as to not render every time the selected tags // Do this here so as to not render every time the selected tags
// themselves change. // themselves change.
selectedTags: TagOrderStore.getSelectedTags(), selectedTags: TagOrderStore.getSelectedTags(),
}, () => {
// we don't need to restore any size here, do we?
// i guess we could have triggered a new group to appear
// that already an explicit size the last time it appeared ...
this._checkSubListsOverflow();
}); });
// this._lastRefreshRoomListTs = Date.now(); // this._lastRefreshRoomListTs = Date.now();
@ -485,9 +494,30 @@ module.exports = React.createClass({
(filter[0] === '#' && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter)))); (filter[0] === '#' && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))));
}, },
_persistCollapsedState: function(key, collapsed) { _handleCollapsedState: function(key, collapsed) {
// persist collapsed state
this.collapsedState[key] = collapsed; this.collapsedState[key] = collapsed;
window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState)); window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState));
// load the persisted size configuration of the expanded sub list
if (!collapsed) {
this._restoreSubListSize(key);
}
// check overflow, as sub lists sizes have changed
// important this happens after calling resize above
this._checkSubListsOverflow();
},
_restoreSubListSize(key) {
const size = this.subListSizes[key];
const handle = this.resizer.forHandleWithId(key);
if (handle) {
handle.resize(size);
}
},
// check overflow for scroll indicator gradient
_checkSubListsOverflow() {
Object.values(this._subListRefs).forEach(l => l.checkOverflow());
}, },
_subListRef: function(key, ref) { _subListRef: function(key, ref) {
@ -520,7 +550,7 @@ module.exports = React.createClass({
const {key, label, onHeaderClick, ... otherProps} = props; const {key, label, onHeaderClick, ... otherProps} = props;
const chosenKey = key || label; const chosenKey = key || label;
const onSubListHeaderClick = (collapsed) => { const onSubListHeaderClick = (collapsed) => {
this._persistCollapsedState(chosenKey, collapsed); this._handleCollapsedState(chosenKey, collapsed);
if (onHeaderClick) { if (onHeaderClick) {
onHeaderClick(collapsed); onHeaderClick(collapsed);
} }

View file

@ -84,8 +84,10 @@ export class Resizer {
} }
_onMouseDown(event) { _onMouseDown(event) {
const target = event.target; // use closest in case the resize handle contains
if (!this._isResizeHandle(target) || target.parentElement !== this.container) { // child dom nodes that can be the target
const resizeHandle = event.target && event.target.closest(`.${this.classNames.handle}`);
if (!resizeHandle || resizeHandle.parentElement !== this.container) {
return; return;
} }
// prevent starting a drag operation // prevent starting a drag operation
@ -96,7 +98,7 @@ export class Resizer {
this.container.classList.add(this.classNames.resizing); this.container.classList.add(this.classNames.resizing);
} }
const {sizer, distributor} = this._createSizerAndDistributor(target); const {sizer, distributor} = this._createSizerAndDistributor(resizeHandle);
const onMouseMove = (event) => { const onMouseMove = (event) => {
const offset = sizer.offsetFromEvent(event); const offset = sizer.offsetFromEvent(event);