mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 13:14:58 +08:00
Merge pull request #4931 from matrix-org/travis/room-list/sticky-headers
Improve performance and stability in sticky headers for new room list
This commit is contained in:
commit
bd8e1f7198
@ -122,16 +122,19 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mx_LeftPanel2_roomListWrapper {
|
.mx_LeftPanel2_roomListWrapper {
|
||||||
|
// Create a flexbox to ensure the containing items cause appropriate overflow.
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
margin-top: 12px; // so we're not up against the search/filter
|
||||||
|
|
||||||
&.stickyBottom {
|
&.mx_LeftPanel2_roomListWrapper_stickyBottom {
|
||||||
padding-bottom: 32px;
|
padding-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.stickyTop {
|
&.mx_LeftPanel2_roomListWrapper_stickyTop {
|
||||||
padding-top: 32px;
|
padding-top: 32px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,10 +24,6 @@ limitations under the License.
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 12px; // so we're not up against the search/filter
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSublist2_headerContainer {
|
.mx_RoomSublist2_headerContainer {
|
||||||
// Create a flexbox to make alignment easy
|
// Create a flexbox to make alignment easy
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -49,10 +45,15 @@ limitations under the License.
|
|||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
|
||||||
|
// Hide the header container if the contained element is stickied.
|
||||||
|
// We don't use display:none as that causes the header to go away too.
|
||||||
|
&.mx_RoomSublist2_headerContainer_hasSticky {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomSublist2_stickable {
|
.mx_RoomSublist2_stickable {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
z-index: 2; // Prioritize headers in the visible list over sticky ones
|
|
||||||
|
|
||||||
// Create a flexbox to make ordering easy
|
// Create a flexbox to make ordering easy
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -64,7 +65,6 @@ limitations under the License.
|
|||||||
// when sticky scrolls instead of collapses the list.
|
// when sticky scrolls instead of collapses the list.
|
||||||
&.mx_RoomSublist2_headerContainer_sticky {
|
&.mx_RoomSublist2_headerContainer_sticky {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1; // over top of other elements, but still under the ones in the visible list
|
|
||||||
height: 32px; // to match the header container
|
height: 32px; // to match the header container
|
||||||
// width set by JS
|
// width set by JS
|
||||||
}
|
}
|
||||||
|
@ -115,86 +115,130 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private handleStickyHeaders(list: HTMLDivElement) {
|
private handleStickyHeaders(list: HTMLDivElement) {
|
||||||
// TODO: Evaluate if this has any performance benefit or detriment.
|
|
||||||
// See https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
|
|
||||||
if (this.isDoingStickyHeaders) return;
|
if (this.isDoingStickyHeaders) return;
|
||||||
this.isDoingStickyHeaders = true;
|
this.isDoingStickyHeaders = true;
|
||||||
if (window.requestAnimationFrame) {
|
window.requestAnimationFrame(() => {
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
this.doStickyHeaders(list);
|
|
||||||
this.isDoingStickyHeaders = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.doStickyHeaders(list);
|
this.doStickyHeaders(list);
|
||||||
this.isDoingStickyHeaders = false;
|
this.isDoingStickyHeaders = false;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private doStickyHeaders(list: HTMLDivElement) {
|
private doStickyHeaders(list: HTMLDivElement) {
|
||||||
const rlRect = list.getBoundingClientRect();
|
const topEdge = list.scrollTop;
|
||||||
const bottom = rlRect.bottom;
|
const bottomEdge = list.offsetHeight + list.scrollTop;
|
||||||
const top = rlRect.top;
|
|
||||||
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist2");
|
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist2");
|
||||||
const headerRightMargin = 24; // calculated from margins and widths to align with non-sticky tiles
|
|
||||||
|
|
||||||
const headerStickyWidth = rlRect.width - headerRightMargin;
|
const headerRightMargin = 16; // calculated from margins and widths to align with non-sticky tiles
|
||||||
|
const headerStickyWidth = list.clientWidth - headerRightMargin;
|
||||||
|
|
||||||
|
// We track which styles we want on a target before making the changes to avoid
|
||||||
|
// excessive layout updates.
|
||||||
|
const targetStyles = new Map<HTMLDivElement, {
|
||||||
|
stickyTop?: boolean;
|
||||||
|
stickyBottom?: boolean;
|
||||||
|
makeInvisible?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
let gotBottom = false;
|
|
||||||
let lastTopHeader;
|
let lastTopHeader;
|
||||||
|
let firstBottomHeader;
|
||||||
for (const sublist of sublists) {
|
for (const sublist of sublists) {
|
||||||
const slRect = sublist.getBoundingClientRect();
|
|
||||||
|
|
||||||
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist2_stickable");
|
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist2_stickable");
|
||||||
header.style.removeProperty("display"); // always clear display:none first
|
header.style.removeProperty("display"); // always clear display:none first
|
||||||
|
|
||||||
if (slRect.top + HEADER_HEIGHT > bottom && !gotBottom) {
|
// When an element is <=40% off screen, make it take over
|
||||||
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
const offScreenFactor = 0.4;
|
||||||
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
|
const isOffTop = (sublist.offsetTop + (offScreenFactor * HEADER_HEIGHT)) <= topEdge;
|
||||||
header.style.width = `${headerStickyWidth}px`;
|
const isOffBottom = (sublist.offsetTop + (offScreenFactor * HEADER_HEIGHT)) >= bottomEdge;
|
||||||
header.style.removeProperty("top");
|
|
||||||
gotBottom = true;
|
if (isOffTop || sublist === sublists[0]) {
|
||||||
} else if (((slRect.top - (HEADER_HEIGHT * 0.6) + HEADER_HEIGHT) < top) || sublist === sublists[0]) {
|
targetStyles.set(header, { stickyTop: true });
|
||||||
// the header should become sticky once it is 60% or less out of view at the top.
|
|
||||||
// We also add HEADER_HEIGHT because the sticky header is put above the scrollable area,
|
|
||||||
// into the padding of .mx_LeftPanel2_roomListWrapper,
|
|
||||||
// by subtracting HEADER_HEIGHT from the top below.
|
|
||||||
// We also always try to make the first sublist header sticky.
|
|
||||||
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
|
||||||
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
|
|
||||||
header.style.width = `${headerStickyWidth}px`;
|
|
||||||
header.style.top = `${rlRect.top - HEADER_HEIGHT}px`;
|
|
||||||
if (lastTopHeader) {
|
if (lastTopHeader) {
|
||||||
lastTopHeader.style.display = "none";
|
lastTopHeader.style.display = "none";
|
||||||
|
targetStyles.set(lastTopHeader, { makeInvisible: true });
|
||||||
}
|
}
|
||||||
lastTopHeader = header;
|
lastTopHeader = header;
|
||||||
|
} else if (isOffBottom && !firstBottomHeader) {
|
||||||
|
targetStyles.set(header, { stickyBottom: true });
|
||||||
|
firstBottomHeader = header;
|
||||||
} else {
|
} else {
|
||||||
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
|
targetStyles.set(header, {}); // nothing == clear
|
||||||
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
|
}
|
||||||
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
|
}
|
||||||
header.style.removeProperty("width");
|
|
||||||
header.style.removeProperty("top");
|
// Run over the style changes and make them reality. We check to see if we're about to
|
||||||
|
// cause a no-op update, as adding/removing properties that are/aren't there cause
|
||||||
|
// layout updates.
|
||||||
|
for (const header of targetStyles.keys()) {
|
||||||
|
const style = targetStyles.get(header);
|
||||||
|
const headerContainer = header.parentElement; // .mx_RoomSublist2_headerContainer
|
||||||
|
|
||||||
|
if (style.makeInvisible) {
|
||||||
|
// we will have already removed the 'display: none', so add it back.
|
||||||
|
header.style.display = "none";
|
||||||
|
continue; // nothing else to do, even if sticky somehow
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style.stickyTop) {
|
||||||
|
if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) {
|
||||||
|
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTop = `${list.parentElement.offsetTop}px`;
|
||||||
|
if (header.style.top !== newTop) {
|
||||||
|
header.style.top = newTop;
|
||||||
|
}
|
||||||
|
} else if (style.stickyBottom) {
|
||||||
|
if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) {
|
||||||
|
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style.stickyTop || style.stickyBottom) {
|
||||||
|
if (!header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) {
|
||||||
|
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
||||||
|
}
|
||||||
|
if (!headerContainer.classList.contains("mx_RoomSublist2_headerContainer_hasSticky")) {
|
||||||
|
headerContainer.classList.add("mx_RoomSublist2_headerContainer_hasSticky");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWidth = `${headerStickyWidth}px`;
|
||||||
|
if (header.style.width !== newWidth) {
|
||||||
|
header.style.width = newWidth;
|
||||||
|
}
|
||||||
|
} else if (!style.stickyTop && !style.stickyBottom) {
|
||||||
|
if (header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) {
|
||||||
|
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
|
||||||
|
}
|
||||||
|
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) {
|
||||||
|
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
|
||||||
|
}
|
||||||
|
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) {
|
||||||
|
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
|
||||||
|
}
|
||||||
|
if (headerContainer.classList.contains("mx_RoomSublist2_headerContainer_hasSticky")) {
|
||||||
|
headerContainer.classList.remove("mx_RoomSublist2_headerContainer_hasSticky");
|
||||||
|
}
|
||||||
|
if (header.style.width) {
|
||||||
|
header.style.removeProperty('width');
|
||||||
|
}
|
||||||
|
if (header.style.top) {
|
||||||
|
header.style.removeProperty('top');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add appropriate sticky classes to wrapper so it has
|
// add appropriate sticky classes to wrapper so it has
|
||||||
// the necessary top/bottom padding to put the sticky header in
|
// the necessary top/bottom padding to put the sticky header in
|
||||||
const listWrapper = list.parentElement;
|
const listWrapper = list.parentElement; // .mx_LeftPanel2_roomListWrapper
|
||||||
if (gotBottom) {
|
|
||||||
listWrapper.classList.add("stickyBottom");
|
|
||||||
} else {
|
|
||||||
listWrapper.classList.remove("stickyBottom");
|
|
||||||
}
|
|
||||||
if (lastTopHeader) {
|
if (lastTopHeader) {
|
||||||
listWrapper.classList.add("stickyTop");
|
listWrapper.classList.add("mx_LeftPanel2_roomListWrapper_stickyTop");
|
||||||
} else {
|
} else {
|
||||||
listWrapper.classList.remove("stickyTop");
|
listWrapper.classList.remove("mx_LeftPanel2_roomListWrapper_stickyTop");
|
||||||
}
|
}
|
||||||
|
if (firstBottomHeader) {
|
||||||
// ensure scroll doesn't go above the gap left by the header of
|
listWrapper.classList.add("mx_LeftPanel2_roomListWrapper_stickyBottom");
|
||||||
// the first sublist always being sticky if no other header is sticky
|
} else {
|
||||||
if (list.scrollTop < HEADER_HEIGHT) {
|
listWrapper.classList.remove("mx_LeftPanel2_roomListWrapper_stickyBottom");
|
||||||
list.scrollTop = HEADER_HEIGHT;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user