Merge pull request #6110 from matrix-org/gsouquet/sticky-header-sizing
commit
71b217e4a1
|
@ -61,6 +61,7 @@ limitations under the License.
|
||||||
&.mx_RoomSublist_headerContainer_sticky {
|
&.mx_RoomSublist_headerContainer_sticky {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: 32px; // to match the header container
|
height: 32px; // to match the header container
|
||||||
|
// width set by JS because of a compat issue between Firefox and Chrome
|
||||||
width: calc(100% - 15px);
|
width: calc(100% - 15px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ import TypingStore from "../stores/TypingStore";
|
||||||
import { EventIndexPeg } from "../indexing/EventIndexPeg";
|
import { EventIndexPeg } from "../indexing/EventIndexPeg";
|
||||||
import {VoiceRecordingStore} from "../stores/VoiceRecordingStore";
|
import {VoiceRecordingStore} from "../stores/VoiceRecordingStore";
|
||||||
import PerformanceMonitor from "../performance";
|
import PerformanceMonitor from "../performance";
|
||||||
|
import UIStore from "../stores/UIStore";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -82,6 +83,7 @@ declare global {
|
||||||
mxEventIndexPeg: EventIndexPeg;
|
mxEventIndexPeg: EventIndexPeg;
|
||||||
mxPerformanceMonitor: PerformanceMonitor;
|
mxPerformanceMonitor: PerformanceMonitor;
|
||||||
mxPerformanceEntryNames: any;
|
mxPerformanceEntryNames: any;
|
||||||
|
mxUIStore: UIStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Document {
|
interface Document {
|
||||||
|
|
|
@ -67,6 +67,7 @@ const cssClasses = [
|
||||||
|
|
||||||
@replaceableComponent("structures.LeftPanel")
|
@replaceableComponent("structures.LeftPanel")
|
||||||
export default class LeftPanel extends React.Component<IProps, IState> {
|
export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
|
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private groupFilterPanelWatcherRef: string;
|
private groupFilterPanelWatcherRef: string;
|
||||||
private bgImageWatcherRef: string;
|
private bgImageWatcherRef: string;
|
||||||
|
@ -93,6 +94,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
|
||||||
|
UIStore.instance.on("ListContainer", this.refreshStickyHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef);
|
SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef);
|
||||||
SettingsStore.unwatchSetting(this.bgImageWatcherRef);
|
SettingsStore.unwatchSetting(this.bgImageWatcherRef);
|
||||||
|
@ -100,6 +106,14 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
||||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
||||||
|
UIStore.instance.stopTrackingElementDimensions("ListContainer");
|
||||||
|
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||||
|
if (prevState.activeSpace !== this.state.activeSpace) {
|
||||||
|
this.refreshStickyHeaders();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateActiveSpace = (activeSpace: Room) => {
|
private updateActiveSpace = (activeSpace: Room) => {
|
||||||
|
@ -245,10 +259,24 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
if (!header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
if (!header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
||||||
header.classList.add("mx_RoomSublist_headerContainer_sticky");
|
header.classList.add("mx_RoomSublist_headerContainer_sticky");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const listDimensions = UIStore.instance.getElementDimensions("ListContainer");
|
||||||
|
if (listDimensions) {
|
||||||
|
const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles
|
||||||
|
const headerStickyWidth = listDimensions.width - headerRightMargin;
|
||||||
|
const newWidth = `${headerStickyWidth}px`;
|
||||||
|
if (header.style.width !== newWidth) {
|
||||||
|
header.style.width = newWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (!style.stickyTop && !style.stickyBottom) {
|
} else if (!style.stickyTop && !style.stickyBottom) {
|
||||||
if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
||||||
header.classList.remove("mx_RoomSublist_headerContainer_sticky");
|
header.classList.remove("mx_RoomSublist_headerContainer_sticky");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (header.style.width) {
|
||||||
|
header.style.removeProperty('width');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -407,6 +435,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
activeSpace={this.state.activeSpace}
|
activeSpace={this.state.activeSpace}
|
||||||
|
onListCollapse={this.refreshStickyHeaders}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
const containerClasses = classNames({
|
const containerClasses = classNames({
|
||||||
|
@ -420,7 +449,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses} ref={this.ref}>
|
||||||
{leftLeftPanel}
|
{leftLeftPanel}
|
||||||
<aside className="mx_LeftPanel_roomListContainer">
|
<aside className="mx_LeftPanel_roomListContainer">
|
||||||
{this.renderHeader()}
|
{this.renderHeader()}
|
||||||
|
|
|
@ -232,6 +232,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
private accountPasswordTimer?: NodeJS.Timeout;
|
private accountPasswordTimer?: NodeJS.Timeout;
|
||||||
private focusComposer: boolean;
|
private focusComposer: boolean;
|
||||||
private subTitleStatus: string;
|
private subTitleStatus: string;
|
||||||
|
private prevWindowWidth: number;
|
||||||
|
|
||||||
private readonly loggedInView: React.RefObject<LoggedInViewType>;
|
private readonly loggedInView: React.RefObject<LoggedInViewType>;
|
||||||
private readonly dispatcherRef: any;
|
private readonly dispatcherRef: any;
|
||||||
|
@ -277,6 +278,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.prevWindowWidth = UIStore.instance.windowWidth || 1000;
|
||||||
UIStore.instance.on(UI_EVENTS.Resize, this.handleResize);
|
UIStore.instance.on(UI_EVENTS.Resize, this.handleResize);
|
||||||
|
|
||||||
this.pageChanging = false;
|
this.pageChanging = false;
|
||||||
|
@ -1821,13 +1823,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
const LHS_THRESHOLD = 1000;
|
const LHS_THRESHOLD = 1000;
|
||||||
const width = UIStore.instance.windowWidth;
|
const width = UIStore.instance.windowWidth;
|
||||||
|
|
||||||
if (width <= LHS_THRESHOLD && !this.state.collapseLhs) {
|
if (this.prevWindowWidth < LHS_THRESHOLD && width >= LHS_THRESHOLD) {
|
||||||
dis.dispatch({ action: 'hide_left_panel' });
|
|
||||||
}
|
|
||||||
if (width > LHS_THRESHOLD && this.state.collapseLhs) {
|
|
||||||
dis.dispatch({ action: 'show_left_panel' });
|
dis.dispatch({ action: 'show_left_panel' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.prevWindowWidth >= LHS_THRESHOLD && width < LHS_THRESHOLD) {
|
||||||
|
dis.dispatch({ action: 'hide_left_panel' });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prevWindowWidth = width;
|
||||||
this.state.resizeNotifier.notifyWindowResized();
|
this.state.resizeNotifier.notifyWindowResized();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ interface IProps {
|
||||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||||
onFocus: (ev: React.FocusEvent) => void;
|
onFocus: (ev: React.FocusEvent) => void;
|
||||||
onBlur: (ev: React.FocusEvent) => void;
|
onBlur: (ev: React.FocusEvent) => void;
|
||||||
|
onListCollapse?: (isExpanded: boolean) => void;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
activeSpace: Room;
|
activeSpace: Room;
|
||||||
|
@ -538,6 +539,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
extraTiles={extraTiles}
|
extraTiles={extraTiles}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)}
|
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)}
|
||||||
|
onListCollapse={this.props.onListCollapse}
|
||||||
/>
|
/>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,7 @@ interface IProps {
|
||||||
alwaysVisible?: boolean;
|
alwaysVisible?: boolean;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
||||||
|
onListCollapse?: (isExpanded: boolean) => void;
|
||||||
|
|
||||||
// TODO: Account for https://github.com/vector-im/element-web/issues/14179
|
// TODO: Account for https://github.com/vector-im/element-web/issues/14179
|
||||||
}
|
}
|
||||||
|
@ -472,6 +473,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
private toggleCollapsed = () => {
|
private toggleCollapsed = () => {
|
||||||
this.layout.isCollapsed = this.state.isExpanded;
|
this.layout.isCollapsed = this.state.isExpanded;
|
||||||
this.setState({isExpanded: !this.layout.isCollapsed});
|
this.setState({isExpanded: !this.layout.isCollapsed});
|
||||||
|
if (this.props.onListCollapse) {
|
||||||
|
this.props.onListCollapse(!this.layout.isCollapsed)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
|
|
|
@ -24,12 +24,14 @@ export enum UI_EVENTS {
|
||||||
|
|
||||||
export type ResizeObserverCallbackFunction = (entries: ResizeObserverEntry[]) => void;
|
export type ResizeObserverCallbackFunction = (entries: ResizeObserverEntry[]) => void;
|
||||||
|
|
||||||
|
|
||||||
export default class UIStore extends EventEmitter {
|
export default class UIStore extends EventEmitter {
|
||||||
private static _instance: UIStore = null;
|
private static _instance: UIStore = null;
|
||||||
|
|
||||||
private resizeObserver: ResizeObserver;
|
private resizeObserver: ResizeObserver;
|
||||||
|
|
||||||
|
private uiElementDimensions = new Map<string, DOMRectReadOnly>();
|
||||||
|
private trackedUiElements = new Map<Element, string>();
|
||||||
|
|
||||||
public windowWidth: number;
|
public windowWidth: number;
|
||||||
public windowHeight: number;
|
public windowHeight: number;
|
||||||
|
|
||||||
|
@ -60,14 +62,51 @@ export default class UIStore extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private resizeObserverCallback = (entries: ResizeObserverEntry[]) => {
|
public getElementDimensions(name: string): DOMRectReadOnly {
|
||||||
const { width, height } = entries
|
return this.uiElementDimensions.get(name);
|
||||||
.find(entry => entry.target === document.body)
|
}
|
||||||
.contentRect;
|
|
||||||
|
|
||||||
this.windowWidth = width;
|
public trackElementDimensions(name: string, element: Element): void {
|
||||||
this.windowHeight = height;
|
this.trackedUiElements.set(element, name);
|
||||||
|
this.resizeObserver.observe(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopTrackingElementDimensions(name: string): void {
|
||||||
|
let trackedElement: Element;
|
||||||
|
this.trackedUiElements.forEach((trackedElementName, element) => {
|
||||||
|
if (trackedElementName === name) {
|
||||||
|
trackedElement = element;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (trackedElement) {
|
||||||
|
this.resizeObserver.unobserve(trackedElement);
|
||||||
|
this.uiElementDimensions.delete(name);
|
||||||
|
this.trackedUiElements.delete(trackedElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isTrackingElementDimensions(name: string): boolean {
|
||||||
|
return this.uiElementDimensions.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resizeObserverCallback = (entries: ResizeObserverEntry[]) => {
|
||||||
|
const windowEntry = entries.find(entry => entry.target === document.body);
|
||||||
|
|
||||||
|
if (windowEntry) {
|
||||||
|
this.windowWidth = windowEntry.contentRect.width;
|
||||||
|
this.windowHeight = windowEntry.contentRect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.forEach(entry => {
|
||||||
|
const trackedElementName = this.trackedUiElements.get(entry.target);
|
||||||
|
if (trackedElementName) {
|
||||||
|
this.uiElementDimensions.set(trackedElementName, entry.contentRect);
|
||||||
|
this.emit(trackedElementName, UI_EVENTS.Resize, entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.emit(UI_EVENTS.Resize, entries);
|
this.emit(UI_EVENTS.Resize, entries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.mxUIStore = UIStore.instance;
|
||||||
|
|
Loading…
Reference in New Issue