Merge pull request #509 from vector-im/bwindels/fix-menupositioning
automatically position popups using a simpler algorithm
This commit is contained in:
commit
e2d7954846
8 changed files with 73 additions and 125 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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');
|
||||||
|
|
20
src/platform/web/ui/css/popup.css
Normal file
20
src/platform/web/ui/css/popup.css
Normal 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;
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in a new issue