Merge pull request #2297 from matrix-org/bwindels/roomlistsizingimprovements

Redesign: improve room sub list sizing & persist sizes
This commit is contained in:
Bruno Windels 2018-11-27 13:40:48 +00:00 committed by GitHub
commit 8f4292399b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 163 additions and 124 deletions

View file

@ -14,15 +14,51 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
/* a word of explanation about the flex-shrink values employed here:
there are 3 priotized categories of screen real-estate grabbing,
each with a flex-shrink difference of 4 order of magnitude,
so they ideally wouldn't affect each other.
lowest category: .mx_RoomSubList
flex:-shrink: 10000000
distribute size of items within the same categery by their size
middle category: .mx_RoomSubList.resized-sized
flex:-shrink: 1000
applied when using the resizer, will have a max-height set to it,
to limit the size
highest category: .mx_RoomSubList.resized-all
flex:-shrink: 1
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.
*/
.mx_RoomSubList { .mx_RoomSubList {
min-height: 31px; min-height: 31px;
flex: 0 1 auto; flex: 0 100000000 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.mx_RoomSubList_nonEmpty { .mx_RoomSubList_nonEmpty {
margin-bottom: 4px; min-height: 76px;
.mx_AutoHideScrollbar_offset {
padding-bottom: 4px;
}
}
.mx_RoomSubList_hidden {
flex: none !important;
}
.mx_RoomSubList.resized-all {
flex: 0 1 auto;
}
.mx_RoomSubList.resized-sized {
/* resizer set max-height on resized-sized,
so that limits the height and hence
needs a very small flex-shrink */
flex: 0 10000 auto;
} }
.mx_RoomSubList_labelContainer { .mx_RoomSubList_labelContainer {
@ -105,39 +141,42 @@ limitations under the License.
} }
.mx_RoomSubList_scroll { .mx_RoomSubList_scroll {
/* let rooms list grab all available space */ /* let rooms list grab as much space as it needs (auto),
potentially overflowing and showing a scrollbar */
flex: 0 1 auto; flex: 0 1 auto;
padding: 0 8px; padding: 0 8px;
} }
.mx_RoomSubList_scroll.mx_IndicatorScrollbar_topOverflow::before, // overflow indicators
.mx_RoomSubList_scroll.mx_IndicatorScrollbar_bottomOverflow::after { .mx_RoomSubList:not(.resized-all) > .mx_RoomSubList_scroll {
position: sticky; &.mx_IndicatorScrollbar_topOverflow::before,
left: 0; &.mx_IndicatorScrollbar_bottomOverflow::after {
right: 0; position: sticky;
height: 40px; left: 0;
content: ""; right: 0;
display: block; height: 40px;
z-index: 100; content: "";
pointer-events: none; display: block;
} z-index: 100;
pointer-events: none;
}
&.mx_IndicatorScrollbar_topOverflow > .mx_AutoHideScrollbar_offset {
margin-top: -40px;
}
&.mx_IndicatorScrollbar_bottomOverflow > .mx_AutoHideScrollbar_offset {
margin-bottom: -40px;
}
.mx_RoomSubList_scroll.mx_IndicatorScrollbar_topOverflow > .mx_AutoHideScrollbar_offset { &.mx_IndicatorScrollbar_topOverflow::before {
margin-top: -40px; top: 0;
} background: linear-gradient($secondary-accent-color, transparent);
.mx_RoomSubList_scroll.mx_IndicatorScrollbar_bottomOverflow > .mx_AutoHideScrollbar_offset { }
margin-bottom: -40px;
}
.mx_RoomSubList_scroll.mx_IndicatorScrollbar_topOverflow::before { &.mx_IndicatorScrollbar_bottomOverflow::after {
top: 0; bottom: 0;
background: linear-gradient($secondary-accent-color, transparent); background: linear-gradient(transparent, $secondary-accent-color);
} }
.mx_RoomSubList_scroll.mx_IndicatorScrollbar_bottomOverflow::after {
bottom: 0;
background: linear-gradient(transparent, $secondary-accent-color);
} }
.collapsed { .collapsed {

View file

@ -24,6 +24,10 @@ limitations under the License.
min-height: 0; min-height: 0;
} }
.mx_SearchBox {
flex: none;
}
/* hide resize handles next to collapsed / empty sublists */ /* hide resize handles next to collapsed / empty sublists */
.mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle { .mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle {
display: none; display: none;

View file

@ -164,15 +164,13 @@ const LoggedInView = React.createClass({
}; };
const collapseConfig = { const collapseConfig = {
toggleSize: 260 - 50, toggleSize: 260 - 50,
onCollapsed: (collapsed, item) => { onCollapsed: (collapsed) => {
if (item.classList.contains("mx_LeftPanel_container")) { this.setState({collapseLhs: collapsed});
this.setState({collapseLhs: collapsed}); if (collapsed) {
if (collapsed) { window.localStorage.setItem("mx_lhs_size", '0');
window.localStorage.setItem("mx_lhs_size", '0');
}
} }
}, },
onResized: (size, item) => { onResized: (size) => {
window.localStorage.setItem("mx_lhs_size", '' + size); window.localStorage.setItem("mx_lhs_size", '' + size);
}, },
}; };

View file

@ -318,20 +318,17 @@ const RoomSubList = React.createClass({
if (len) { if (len) {
const subListClasses = classNames({ const subListClasses = classNames({
"mx_RoomSubList": true, "mx_RoomSubList": true,
"mx_RoomSubList_hidden": this.state.hidden,
"mx_RoomSubList_nonEmpty": len && !this.state.hidden, "mx_RoomSubList_nonEmpty": len && !this.state.hidden,
}); });
if (this.state.hidden) { if (this.state.hidden) {
return <div className={subListClasses} style={{flexBasis: "unset", flexGrow: "unset"}}> return <div className={subListClasses}>
{this._getHeaderJsx()} {this._getHeaderJsx()}
</div>; </div>;
} else { } else {
const heightEstimation = (len * 44) + 31 + (8 + 8);
const style = {
maxHeight: `${heightEstimation}px`,
};
const tiles = this.makeRoomTiles(); const tiles = this.makeRoomTiles();
tiles.push(...this.props.extraTiles); tiles.push(...this.props.extraTiles);
return <div style={style} className={subListClasses}> return <div className={subListClasses}>
{this._getHeaderJsx()} {this._getHeaderJsx()}
<IndicatorScrollbar className="mx_RoomSubList_scroll"> <IndicatorScrollbar className="mx_RoomSubList_scroll">
{ tiles } { tiles }

View file

@ -14,7 +14,7 @@ const ResizeHandle = (props) => {
classNames.push('mx_ResizeHandle_reverse'); classNames.push('mx_ResizeHandle_reverse');
} }
return ( return (
<div className={classNames.join(' ')} /> <div className={classNames.join(' ')} data-id={props.id} />
); );
}; };

View file

@ -36,7 +36,7 @@ import GroupStore from '../../../stores/GroupStore';
import RoomSubList from '../../structures/RoomSubList'; import RoomSubList from '../../structures/RoomSubList';
import ResizeHandle from '../elements/ResizeHandle'; import ResizeHandle from '../elements/ResizeHandle';
import {Resizer, FixedDistributor, FlexSizer} from '../../../resizer' import {Resizer, RoomDistributor, RoomSizer} from '../../../resizer'
const HIDE_CONFERENCE_CHANS = true; const HIDE_CONFERENCE_CHANS = true;
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
@ -70,6 +70,10 @@ module.exports = React.createClass({
}, },
getInitialState: function() { getInitialState: function() {
const sizesJson = window.localStorage.getItem("mx_roomlist_sizes");
this.subListSizes = sizesJson ? JSON.parse(sizesJson) : {};
return { return {
isLoadingLeftRooms: false, isLoadingLeftRooms: false,
totalRoomCount: null, totalRoomCount: null,
@ -134,14 +138,34 @@ module.exports = React.createClass({
this._delayedRefreshRoomListLoopCount = 0; this._delayedRefreshRoomListLoopCount = 0;
}, },
_onSubListResize: function(newSize, id) {
if (!id) {
return;
}
if (typeof newSize === "string") {
newSize = Number.MAX_SAFE_INTEGER;
}
this.subListSizes[id] = newSize;
window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.subListSizes));
},
componentDidMount: function() { componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
this.resizer = new Resizer(this.resizeContainer, FixedDistributor, null, FlexSizer); const cfg = {
onResized: this._onSubListResize,
};
this.resizer = new Resizer(this.resizeContainer, RoomDistributor, cfg, RoomSizer);
this.resizer.setClassNames({ this.resizer.setClassNames({
handle: "mx_ResizeHandle", handle: "mx_ResizeHandle",
vertical: "mx_ResizeHandle_vertical", vertical: "mx_ResizeHandle_vertical",
reverse: "mx_ResizeHandle_reverse" reverse: "mx_ResizeHandle_reverse"
}); });
// load stored sizes
Object.entries(this.subListSizes).forEach(([id, size]) => {
this.resizer.forHandleWithId(id).resize(size);
});
this.resizer.attach(); this.resizer.attach();
this.mounted = true; this.mounted = true;
}, },
@ -476,7 +500,7 @@ module.exports = React.createClass({
if (!isLast) { if (!isLast) {
return components.concat( return components.concat(
subList, subList,
<ResizeHandle key={chosenKey+"-resizer"} vertical={true} /> <ResizeHandle key={chosenKey+"-resizer"} vertical={true} id={chosenKey} />
); );
} else { } else {
return components.concat(subList); return components.concat(subList);
@ -484,6 +508,10 @@ module.exports = React.createClass({
}, []); }, []);
}, },
_collectResizeContainer: function(el) {
this.resizeContainer = el;
},
render: function() { render: function() {
let subLists = [ let subLists = [
{ {
@ -560,7 +588,7 @@ module.exports = React.createClass({
const subListComponents = this._mapSubListProps(subLists); const subListComponents = this._mapSubListProps(subLists);
return ( return (
<div ref={(d) => this.resizeContainer = d} className="mx_RoomList"> <div ref={this._collectResizeContainer} className="mx_RoomList">
{ subListComponents } { subListComponents }
</div> </div>
); );

View file

@ -18,43 +18,46 @@ limitations under the License.
distributors translate a moving cursor into distributors translate a moving cursor into
CSS/DOM changes by calling the sizer CSS/DOM changes by calling the sizer
they have one method, `resize` that receives they have two methods:
`resize` receives then new item size
`resizeFromContainerOffset` receives resize handle location
within the container bounding box. For internal use.
This method usually ends up calling `resize` once the start offset is subtracted.
the offset from the container edge of where the offset from the container edge of where
the mouse cursor is. the mouse cursor is.
*/ */
class FixedDistributor { class FixedDistributor {
constructor(sizer, item, config) { constructor(sizer, item, id, config) {
this.sizer = sizer; this.sizer = sizer;
this.item = item; this.item = item;
this.id = id;
this.beforeOffset = sizer.getItemOffset(this.item); this.beforeOffset = sizer.getItemOffset(this.item);
this.onResized = config && config.onResized; this.onResized = config && config.onResized;
} }
resize(offset) { resize(itemSize) {
const itemSize = offset - this.beforeOffset;
this.sizer.setItemSize(this.item, itemSize); this.sizer.setItemSize(this.item, itemSize);
if (this.onResized) { if (this.onResized) {
this.onResized(itemSize, this.item); this.onResized(itemSize, this.id, this.item);
} }
return itemSize; return itemSize;
} }
sizeFromOffset(offset) { resizeFromContainerOffset(offset) {
return offset - this.beforeOffset; this.resize(offset - this.beforeOffset);
} }
} }
class CollapseDistributor extends FixedDistributor { class CollapseDistributor extends FixedDistributor {
constructor(sizer, item, config) { constructor(sizer, item, id, config) {
super(sizer, item, config); super(sizer, item, id, config);
this.toggleSize = config && config.toggleSize; this.toggleSize = config && config.toggleSize;
this.onCollapsed = config && config.onCollapsed; this.onCollapsed = config && config.onCollapsed;
this.isCollapsed = false; this.isCollapsed = false;
} }
resize(offset) { resize(newSize) {
const newSize = this.sizeFromOffset(offset);
const isCollapsedSize = newSize < this.toggleSize; const isCollapsedSize = newSize < this.toggleSize;
if (isCollapsedSize && !this.isCollapsed) { if (isCollapsedSize && !this.isCollapsed) {
this.isCollapsed = true; this.isCollapsed = true;
@ -68,60 +71,12 @@ class CollapseDistributor extends FixedDistributor {
this.isCollapsed = false; this.isCollapsed = false;
} }
if (!isCollapsedSize) { if (!isCollapsedSize) {
super.resize(offset); super.resize(newSize);
} }
} }
} }
class PercentageDistributor {
constructor(sizer, item, _config, items, container) {
this.container = container;
this.totalSize = sizer.getTotalSize();
this.sizer = sizer;
const itemIndex = items.indexOf(item);
this.beforeItems = items.slice(0, itemIndex);
this.afterItems = items.slice(itemIndex);
const percentages = PercentageDistributor._getPercentages(sizer, items);
this.beforePercentages = percentages.slice(0, itemIndex);
this.afterPercentages = percentages.slice(itemIndex);
}
resize(offset) {
const percent = offset / this.totalSize;
const beforeSum =
this.beforePercentages.reduce((total, p) => total + p, 0);
const beforePercentages =
this.beforePercentages.map(p => (p / beforeSum) * percent);
const afterSum =
this.afterPercentages.reduce((total, p) => total + p, 0);
const afterPercentages =
this.afterPercentages.map(p => (p / afterSum) * (1 - percent));
this.beforeItems.forEach((item, index) => {
this.sizer.setItemPercentage(item, beforePercentages[index]);
});
this.afterItems.forEach((item, index) => {
this.sizer.setItemPercentage(item, afterPercentages[index]);
});
}
static _getPercentages(sizer, items) {
const percentages = items.map(i => sizer.getItemPercentage(i));
const setPercentages = percentages.filter(p => p !== null);
const unsetCount = percentages.length - setPercentages.length;
const setTotal = setPercentages.reduce((total, p) => total + p, 0);
const implicitPercentage = (1 - setTotal) / unsetCount;
return percentages.map(p => p === null ? implicitPercentage : p);
}
static setPercentage(el, percent) {
el.style.flexGrow = Math.round(percent * 1000);
}
}
module.exports = { module.exports = {
FixedDistributor, FixedDistributor,
CollapseDistributor, CollapseDistributor,
PercentageDistributor,
}; };

View file

@ -15,8 +15,9 @@ limitations under the License.
*/ */
import {Sizer, FlexSizer} from "./sizer"; import {Sizer, FlexSizer} from "./sizer";
import {FixedDistributor, CollapseDistributor, PercentageDistributor} from "./distributors"; import {FixedDistributor, CollapseDistributor} from "./distributors";
import {Resizer} from "./resizer"; import {Resizer} from "./resizer";
import {RoomSizer, RoomDistributor} from "./room";
module.exports = { module.exports = {
Resizer, Resizer,
@ -24,5 +25,6 @@ module.exports = {
FlexSizer, FlexSizer,
FixedDistributor, FixedDistributor,
CollapseDistributor, CollapseDistributor,
PercentageDistributor, RoomSizer,
RoomDistributor,
}; };

View file

@ -64,8 +64,19 @@ export class Resizer {
forHandleAt(handleIndex) { forHandleAt(handleIndex) {
const handles = this._getResizeHandles(); const handles = this._getResizeHandles();
const handle = handles[handleIndex]; const handle = handles[handleIndex];
const {distributor} = this._createSizerAndDistributor(handle); if (handle) {
return distributor; const {distributor} = this._createSizerAndDistributor(handle);
return distributor;
}
}
forHandleWithId(id) {
const handles = this._getResizeHandles();
const handle = handles.find((h) => h.getAttribute("data-id") === id);
if (handle) {
const {distributor} = this._createSizerAndDistributor(handle);
return distributor;
}
} }
_isResizeHandle(el) { _isResizeHandle(el) {
@ -79,6 +90,7 @@ export class Resizer {
} }
// prevent starting a drag operation // prevent starting a drag operation
event.preventDefault(); event.preventDefault();
// mark as currently resizing // mark as currently resizing
if (this.classNames.resizing) { if (this.classNames.resizing) {
this.container.classList.add(this.classNames.resizing); this.container.classList.add(this.classNames.resizing);
@ -88,7 +100,7 @@ export class Resizer {
const onMouseMove = (event) => { const onMouseMove = (event) => {
const offset = sizer.offsetFromEvent(event); const offset = sizer.offsetFromEvent(event);
distributor.resize(offset); distributor.resizeFromContainerOffset(offset);
}; };
const body = document.body; const body = document.body;
@ -115,9 +127,10 @@ export class Resizer {
// if reverse, resize the item after the handle instead of before, so + 1 // if reverse, resize the item after the handle instead of before, so + 1
const itemIndex = items.indexOf(prevItem) + (reverse ? 1 : 0); const itemIndex = items.indexOf(prevItem) + (reverse ? 1 : 0);
const item = items[itemIndex]; const item = items[itemIndex];
const id = resizeHandle.getAttribute("data-id");
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const distributor = new this.distributorCtor( const distributor = new this.distributorCtor(
sizer, item, this.distributorCfg, sizer, item, id, this.distributorCfg,
items, this.container); items, this.container);
return {sizer, distributor}; return {sizer, distributor};
} }

View file

@ -22,30 +22,33 @@ class RoomSizer extends Sizer {
const isString = typeof size === "string"; const isString = typeof size === "string";
const cl = item.classList; const cl = item.classList;
if (isString) { if (isString) {
item.style.flex = null; if (size === "resized-all") {
if (size === "show-content") { cl.add("resized-all");
cl.add("show-content"); cl.remove("resized-sized");
cl.remove("show-available");
item.style.maxHeight = null; item.style.maxHeight = null;
} }
} else { } else {
cl.add("show-available"); cl.add("resized-sized");
//item.style.flex = `0 1 ${Math.round(size)}px`; cl.remove("resized-all");
item.style.maxHeight = `${Math.round(size)}px`; item.style.maxHeight = `${Math.round(size)}px`;
} }
} }
} }
class RoomDistributor extends FixedDistributor { class RoomDistributor extends FixedDistributor {
resize(offset) { resize(itemSize) {
const itemSize = offset - this.sizer.getItemOffset(this.item); const scrollItem = this.item.querySelector(".mx_RoomSubList_scroll");
const fixedHeight = this.item.offsetHeight - scrollItem.offsetHeight;
if (itemSize > this.item.scrollHeight) { if (itemSize > (fixedHeight + scrollItem.scrollHeight)) {
this.sizer.setItemSize(this.item, "show-content"); super.resize("resized-all");
} else { } else {
this.sizer.setItemSize(this.item, itemSize); super.resize(itemSize);
} }
} }
resizeFromContainerOffset(offset) {
return this.resize(offset - this.sizer.getItemOffset(this.item));
}
} }
module.exports = { module.exports = {