Initial attempt at sticky headers
Docs enclosed in diff.
This commit is contained in:
parent
cbe9ade1c9
commit
1bbf2e053b
4 changed files with 154 additions and 10 deletions
|
@ -131,6 +131,7 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
position: relative; // for sticky headers
|
||||||
|
|
||||||
// Create a flexbox to trick the layout engine
|
// Create a flexbox to trick the layout engine
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -30,9 +30,51 @@ limitations under the License.
|
||||||
// Create a flexbox to make ordering easy
|
// Create a flexbox to make ordering easy
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
// ***************************
|
||||||
|
// Sticky Headers Start
|
||||||
|
|
||||||
|
// Ideally we'd be able to use `position: sticky; top: 0; bottom: 0;` on the
|
||||||
|
// headerContainer, however due to our layout concerns we actually have to
|
||||||
|
// calculate it manually so we can sticky things in the right places. We also
|
||||||
|
// target the headerText instead of the container to reduce jumps when scrolling,
|
||||||
|
// and to help hide the badges/other buttons that could appear on hover. This
|
||||||
|
// all works by ensuring the header text has a fixed height when sticky so the
|
||||||
|
// fixed height of the container can maintain the scroll position.
|
||||||
|
|
||||||
|
// The combined height must be set in the LeftPanel2 component for sticky headers
|
||||||
|
// to work correctly.
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
|
||||||
|
.mx_RoomSublist2_headerText {
|
||||||
|
z-index: 2; // Prioritize headers in the visible list over sticky ones
|
||||||
|
|
||||||
|
// We use a generic sticky class for 2 reasons: to reduce style duplication and
|
||||||
|
// to identify when a header is sticky. If we didn't have a consistent sticky class,
|
||||||
|
// we'd have to do the "is sticky" checks again on click, as clicking the header
|
||||||
|
// when sticky scrolls instead of collapses the list.
|
||||||
|
&.mx_RoomSublist2_headerContainer_sticky {
|
||||||
|
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
|
||||||
|
// width set by JS
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_RoomSublist2_headerContainer_stickyBottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't have this style because the top is dependent on the room list header's
|
||||||
|
// height, and is therefore calculated in JS.
|
||||||
|
//&.mx_RoomSublist2_headerContainer_stickyTop {
|
||||||
|
// top: 0;
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sticky Headers End
|
||||||
|
// ***************************
|
||||||
|
|
||||||
.mx_RoomSublist2_badgeContainer {
|
.mx_RoomSublist2_badgeContainer {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
|
@ -76,18 +118,25 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSublist2_headerText {
|
.mx_RoomSublist2_headerText {
|
||||||
text-transform: uppercase;
|
|
||||||
opacity: 0.5;
|
|
||||||
line-height: $font-16px;
|
|
||||||
font-size: $font-12px;
|
|
||||||
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: calc(100% - 16px); // 16px is the badge width
|
max-width: calc(100% - 16px); // 16px is the badge width
|
||||||
|
|
||||||
// Ellipsize any text overflow
|
// Set the same background color as the room list for sticky headers
|
||||||
text-overflow: ellipsis;
|
background-color: $roomlist2-bg-color;
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
// Target the span inside the container so we don't opacify the
|
||||||
|
// whole header, which can make the sticky header experience annoying.
|
||||||
|
> span {
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.5;
|
||||||
|
line-height: $font-16px;
|
||||||
|
font-size: $font-12px;
|
||||||
|
|
||||||
|
// Ellipsize any text overflow
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,70 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Apply this on resize, init, etc for reliability
|
||||||
|
private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const list = ev.target as HTMLDivElement;
|
||||||
|
const rlRect = list.getBoundingClientRect();
|
||||||
|
const bottom = rlRect.bottom;
|
||||||
|
const top = rlRect.top;
|
||||||
|
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist2");
|
||||||
|
const headerHeight = 32; // Note: must match the CSS!
|
||||||
|
const headerRightMargin = 24; // calculated from margins and widths to align with non-sticky tiles
|
||||||
|
|
||||||
|
const headerStickyWidth = rlRect.width - headerRightMargin;
|
||||||
|
|
||||||
|
let gotBottom = false;
|
||||||
|
for (const sublist of sublists) {
|
||||||
|
const slRect = sublist.getBoundingClientRect();
|
||||||
|
|
||||||
|
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist2_headerText");
|
||||||
|
|
||||||
|
if (slRect.top + headerHeight > bottom && !gotBottom) {
|
||||||
|
console.log(`${header.textContent} is off the bottom`);
|
||||||
|
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
||||||
|
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
|
||||||
|
header.style.width = `${headerStickyWidth}px`;
|
||||||
|
gotBottom = true;
|
||||||
|
} else if (slRect.top < top) {
|
||||||
|
console.log(`${header.textContent} is off the top`);
|
||||||
|
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
||||||
|
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
|
||||||
|
header.style.width = `${headerStickyWidth}px`;
|
||||||
|
header.style.top = `${rlRect.top}px`;
|
||||||
|
} else {
|
||||||
|
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
|
||||||
|
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
|
||||||
|
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
|
||||||
|
header.style.width = `unset`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// const name = header.textContent;
|
||||||
|
// if (hRect.bottom + headerHeight < top) {
|
||||||
|
// // Before the content (top of list)
|
||||||
|
// header.classList.add(
|
||||||
|
// "mx_RoomSublist2_headerContainer_sticky",
|
||||||
|
// "mx_RoomSublist2_headerContainer_stickyTop",
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// header.classList.remove(
|
||||||
|
// "mx_RoomSublist2_headerContainer_sticky",
|
||||||
|
// "mx_RoomSublist2_headerContainer_stickyTop",
|
||||||
|
// "mx_RoomSublist2_headerContainer_stickyBottom",
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!hitMiddle && (headerHeight + hRect.top) >= bottom) {
|
||||||
|
// // if we got here, the header is visible
|
||||||
|
// hitMiddle = true;
|
||||||
|
// header.style.backgroundColor = 'red';
|
||||||
|
// } else {
|
||||||
|
// header.style.top = "0px";
|
||||||
|
// header.style.bottom = "unset";
|
||||||
|
// header.style.backgroundColor = "unset";
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private renderHeader(): React.ReactNode {
|
private renderHeader(): React.ReactNode {
|
||||||
// TODO: Update when profile info changes
|
// TODO: Update when profile info changes
|
||||||
// TODO: Presence
|
// TODO: Presence
|
||||||
|
@ -191,7 +255,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
<aside className="mx_LeftPanel2_roomListContainer">
|
<aside className="mx_LeftPanel2_roomListContainer">
|
||||||
{this.renderHeader()}
|
{this.renderHeader()}
|
||||||
{this.renderSearchExplore()}
|
{this.renderSearchExplore()}
|
||||||
<div className="mx_LeftPanel2_actualRoomListContainer">
|
<div className="mx_LeftPanel2_actualRoomListContainer" onScroll={this.onScroll}>
|
||||||
{roomList}
|
{roomList}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
30
src/utils/css.ts
Normal file
30
src/utils/css.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function addClass(classes: string, clazz: string): string {
|
||||||
|
if (!classes.includes(clazz)) return `${classes} ${clazz}`;
|
||||||
|
return classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeClass(classes: string, clazz: string): string {
|
||||||
|
const idx = classes.indexOf(clazz);
|
||||||
|
if (idx >= 0) {
|
||||||
|
const beforeStr = classes.substring(0, idx);
|
||||||
|
const afterStr = classes.substring(idx + clazz.length);
|
||||||
|
return `${beforeStr} ${afterStr}`.trim();
|
||||||
|
}
|
||||||
|
return classes;
|
||||||
|
}
|
Loading…
Reference in a new issue