Merge pull request #509 from vector-im/bwindels/fix-menupositioning

automatically position popups using a simpler algorithm
This commit is contained in:
Bruno Windels 2021-09-24 18:32:36 +02:00 committed by GitHub
commit e2d7954846
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 73 additions and 125 deletions

View file

@ -118,8 +118,6 @@ the layout viewport up without resizing it when the keyboard shows */
width: 100%; width: 100%;
/* otherwise we don't get scrollbars and the content grows as large as it can */ /* otherwise we don't get scrollbars and the content grows as large as it can */
min-height: 0; min-height: 0;
/* make popups relative to this element so changing the left panel width doesn't affect their position */
position: relative;
} }
.middle { .middle {

View file

@ -16,6 +16,7 @@ limitations under the License.
*/ */
@import url('font.css'); @import url('font.css');
@import url('layout.css'); @import url('layout.css');
@import url('popup.css');
@import url('login.css'); @import url('login.css');
@import url('left-panel.css'); @import url('left-panel.css');
@import url('right-panel.css'); @import url('right-panel.css');

View file

@ -0,0 +1,20 @@
/*
Copyright 2021 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.
*/
.popupContainer {
position: absolute;
white-space: nowrap;
}

View file

@ -17,6 +17,7 @@ limitations under the License.
.Timeline { .Timeline {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
/* needed to position the jump to bottom button */
position: relative; position: relative;
min-height: 0; min-height: 0;
} }

View file

@ -14,20 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
const HorizontalAxis = { import {tag} from "./html";
scrollOffset(el) {return el.scrollLeft;},
size(el) {return el.offsetWidth;},
offsetStart(el) {return el.offsetLeft;},
setStart(el, value) {el.style.left = `${value}px`;},
setEnd(el, value) {el.style.right = `${value}px`;},
};
const VerticalAxis = {
scrollOffset(el) {return el.scrollTop;},
size(el) {return el.offsetHeight;},
offsetStart(el) {return el.offsetTop;},
setStart(el, value) {el.style.top = `${value}px`;},
setEnd(el, value) {el.style.bottom = `${value}px`;},
};
export class Popup { export class Popup {
constructor(view, closeCallback = null) { constructor(view, closeCallback = null) {
@ -40,27 +27,28 @@ export class Popup {
this._closeCallback = closeCallback; this._closeCallback = closeCallback;
} }
_getPopupContainer() {
const appContainer = this._target.closest(".hydrogen");
let popupContainer = appContainer.querySelector(".popupContainer");
if (!popupContainer) {
popupContainer = tag.div({className: "popupContainer"});
appContainer.appendChild(popupContainer);
}
return popupContainer;
}
trackInTemplateView(templateView) { trackInTemplateView(templateView) {
this._trackingTemplateView = templateView; this._trackingTemplateView = templateView;
this._trackingTemplateView.addSubView(this); this._trackingTemplateView.addSubView(this);
} }
/** showRelativeTo(target, verticalPadding = 0) {
@param {DOMElement}
@param {string} arrangement.relativeTo: whether top/left or bottom/right is used to position
@param {string} arrangement.align: how much of the popup axis size (start: 0, end: width or center: width/2)
is taken into account when positioning relative to the target
@param {number} arrangement.before extra padding to shift the final positioning with
@param {number} arrangement.after extra padding to shift the final positioning with
*/
showRelativeTo(target, arrangement) {
this._target = target; this._target = target;
this._arrangement = arrangement; this._verticalPadding = verticalPadding;
this._scroller = findScrollParent(this._target); this._scroller = findScrollParent(this._target);
this._view.mount(); this._view.mount();
this._target.offsetParent.appendChild(this._popup); this._getPopupContainer().appendChild(this._popup);
this._applyArrangementAxis(HorizontalAxis, this._arrangement.horizontal); this._position();
this._applyArrangementAxis(VerticalAxis, this._arrangement.vertical);
if (this._scroller) { if (this._scroller) {
document.body.addEventListener("scroll", this, true); document.body.addEventListener("scroll", this, true);
} }
@ -95,75 +83,48 @@ export class Popup {
handleEvent(evt) { handleEvent(evt) {
if (evt.type === "scroll") { if (evt.type === "scroll") {
this._onScroll(); if(!this._position()) {
this.close();
}
} else if (evt.type === "click") { } else if (evt.type === "click") {
this._onClick(evt); this._onClick(evt);
} }
} }
_onScroll() {
if (this._scroller && !this._isVisibleInScrollParent(VerticalAxis)) {
this.close();
} else {
this._applyArrangementAxis(HorizontalAxis, this._arrangement.horizontal);
this._applyArrangementAxis(VerticalAxis, this._arrangement.vertical);
}
}
_onClick() { _onClick() {
this.close(); this.close();
} }
_applyArrangementAxis(axis, {relativeTo, align, before, after}) { _position() {
// TODO: using {relativeTo: "end", align: "start"} to align the right edge of the popup const targetPosition = this._target.getBoundingClientRect();
// with the right side of the target doens't make sense here, we'd expect align: "right"? const popupWidth = this._popup.clientWidth;
// see RoomView const popupHeight = this._popup.clientHeight;
if (relativeTo === "end") { const viewport = (this._scroller ? this._scroller : document.documentElement).getBoundingClientRect();
let end = axis.size(this._target.offsetParent) - axis.offsetStart(this._target);
if (align === "end") {
end -= axis.size(this._popup);
} else if (align === "center") {
end -= ((axis.size(this._popup) / 2) - (axis.size(this._target) / 2));
}
if (typeof before === "number") {
end += before;
} else if (typeof after === "number") {
end -= (axis.size(this._target) + after);
}
axis.setEnd(this._popup, end);
} else if (relativeTo === "start") {
let scrollOffset = this._scroller ? axis.scrollOffset(this._scroller) : 0;
let start = axis.offsetStart(this._target) - scrollOffset;
if (align === "start") {
start -= axis.size(this._popup);
} else if (align === "center") {
start -= ((axis.size(this._popup) / 2) - (axis.size(this._target) / 2));
}
if (typeof before === "number") {
start -= before;
} else if (typeof after === "number") {
start += (axis.size(this._target) + after);
}
axis.setStart(this._popup, start);
} else {
throw new Error("unknown relativeTo: " + relativeTo);
}
}
_isVisibleInScrollParent(axis) { if (
// clipped at start? targetPosition.top > viewport.bottom ||
if ((axis.offsetStart(this._target) + axis.size(this._target)) < ( targetPosition.left > viewport.right ||
axis.offsetStart(this._scroller) + targetPosition.bottom < viewport.top ||
axis.scrollOffset(this._scroller) targetPosition.right < viewport.left
)) { ) {
return false; return false;
} }
// clipped at end? if (viewport.bottom >= targetPosition.bottom + popupHeight) {
if (axis.offsetStart(this._target) > ( // show below
axis.offsetStart(this._scroller) + this._popup.style.top = `${targetPosition.bottom + this._verticalPadding}px`;
axis.size(this._scroller) + } else if (viewport.top <= targetPosition.top - popupHeight) {
axis.scrollOffset(this._scroller) // show top
)) { this._popup.style.top = `${targetPosition.top - popupHeight - this._verticalPadding}px`;
} else {
return false;
}
if (viewport.right >= targetPosition.right + popupWidth) {
// show right
this._popup.style.left = `${targetPosition.left}px`;
} else if (viewport.left <= targetPosition.left - popupWidth) {
// show left
this._popup.style.left = `${targetPosition.right - popupWidth}px`;
} else {
return false; return false;
} }
return true; return true;
@ -196,10 +157,10 @@ function findScrollParent(el) {
// can cause the scrollHeight to be larger than the clientHeight in the parent // can cause the scrollHeight to be larger than the clientHeight in the parent
// see button.link class // see button.link class
const style = window.getComputedStyle(parent); const style = window.getComputedStyle(parent);
const {overflow} = style; const overflowY = style.getPropertyValue("overflow-y");
if (overflow === "auto" || overflow === "scroll") { if (overflowY === "auto" || overflowY === "scroll") {
return parent; return parent;
} }
} }
} while (parent !== el.offsetParent); } while (parent !== document.body);
} }

View file

@ -102,18 +102,7 @@ export class MessageComposer extends TemplateView {
Menu.option(vm.i18n`Send file`, () => vm.sendFile()).setIcon("file"), Menu.option(vm.i18n`Send file`, () => vm.sendFile()).setIcon("file"),
])); ]));
this._attachmentPopup.trackInTemplateView(this); this._attachmentPopup.trackInTemplateView(this);
this._attachmentPopup.showRelativeTo(evt.target, { this._attachmentPopup.showRelativeTo(evt.target, 12);
horizontal: {
relativeTo: "end",
align: "start",
after: 0
},
vertical: {
relativeTo: "end",
align: "start",
before: 8,
}
});
} }
} }
} }

View file

@ -83,18 +83,7 @@ export class RoomView extends TemplateView {
} }
this._optionsPopup = new Popup(new Menu(options)); this._optionsPopup = new Popup(new Menu(options));
this._optionsPopup.trackInTemplateView(this); this._optionsPopup.trackInTemplateView(this);
this._optionsPopup.showRelativeTo(evt.target, { this._optionsPopup.showRelativeTo(evt.target, 10);
horizontal: {
relativeTo: "end",
align: "start",
after: 0
},
vertical: {
relativeTo: "start",
align: "end",
before: -32 - 4
}
});
} }
} }

View file

@ -98,18 +98,7 @@ export class BaseMessageView extends TemplateView {
const onClose = () => this.root().classList.remove("menuOpen"); const onClose = () => this.root().classList.remove("menuOpen");
this._menuPopup = new Popup(new Menu(options), onClose); this._menuPopup = new Popup(new Menu(options), onClose);
this._menuPopup.trackInTemplateView(this); this._menuPopup.trackInTemplateView(this);
this._menuPopup.showRelativeTo(button, { this._menuPopup.showRelativeTo(button, 2);
horizontal: {
relativeTo: "end",
align: "start",
after: 0
},
vertical: {
relativeTo: "start",
align: "end",
before: -24
}
});
} }
} }