DEV: uses popperjs for positioning user and group card (#20063)
Behavior should be very similar but the code is simplified and it should fix various bugs where the card was showing out of screen even if we had available space.
This commit is contained in:
parent
18f7b47ecb
commit
335c3f4621
|
@ -2,12 +2,13 @@ import { alias, match } from "@ember/object/computed";
|
|||
import { schedule, throttle } from "@ember/runloop";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import Mixin from "@ember/object/mixin";
|
||||
import afterTransition from "discourse/lib/after-transition";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
import { createPopper } from "@popperjs/core";
|
||||
import { headerOffset } from "discourse/lib/offset-calculator";
|
||||
|
||||
const DEFAULT_SELECTOR = "#main-outlet";
|
||||
|
||||
|
@ -27,6 +28,7 @@ export default Mixin.create({
|
|||
elementId: null, //click detection added for data-{elementId}
|
||||
triggeringLinkClass: null, //the <a> classname where this card should appear
|
||||
_showCallback: null, //username, $target - load up data for when show is called, should call this._positionCard($target) when it's done.
|
||||
_popperReference: null,
|
||||
|
||||
postStream: alias("topic.postStream"),
|
||||
viewingTopic: match("router.currentRouteName", /^topic\./),
|
||||
|
@ -36,7 +38,6 @@ export default Mixin.create({
|
|||
loading: null,
|
||||
cardTarget: null,
|
||||
post: null,
|
||||
isFixed: false,
|
||||
isDocked: false,
|
||||
|
||||
_show(username, target, event) {
|
||||
|
@ -58,6 +59,10 @@ export default Mixin.create({
|
|||
|
||||
const currentUsername = this.username;
|
||||
if (username === currentUsername || this.loading === username) {
|
||||
// prevents opacity flasing when clicking on same trigger
|
||||
if (username !== currentUsername) {
|
||||
this.element.dataset.popperPlacement = "";
|
||||
}
|
||||
this._positionCard($(target));
|
||||
return;
|
||||
}
|
||||
|
@ -100,7 +105,7 @@ export default Mixin.create({
|
|||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
afterTransition($(this.element), this._hide);
|
||||
|
||||
const id = this.elementId;
|
||||
const triggeringLinkClass = this.triggeringLinkClass;
|
||||
const previewClickEvent = `click.discourse-preview-${id}-${triggeringLinkClass}`;
|
||||
|
@ -168,7 +173,7 @@ export default Mixin.create({
|
|||
},
|
||||
|
||||
_topicHeaderTrigger(username, target) {
|
||||
this.setProperties({ isFixed: true, isDocked: true });
|
||||
this.setProperties({ isDocked: true });
|
||||
return this._show(username, target);
|
||||
},
|
||||
|
||||
|
@ -188,110 +193,81 @@ export default Mixin.create({
|
|||
},
|
||||
|
||||
_previewClick($target) {
|
||||
this.set("isFixed", true);
|
||||
return this._show($target.text().replace(/^@/, ""), $target);
|
||||
},
|
||||
|
||||
_positionCard(target) {
|
||||
const rtl = $("html").css("direction") === "rtl";
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const width = $(this.element).width();
|
||||
const height = 175;
|
||||
const isFixed = this.isFixed;
|
||||
const isDocked = this.isDocked;
|
||||
|
||||
let verticalAdjustments = 0;
|
||||
|
||||
schedule("afterRender", () => {
|
||||
if (target) {
|
||||
if (!this.site.mobileView) {
|
||||
let position = target.offset();
|
||||
if (target.parents(".d-header").length > 0) {
|
||||
position.top = target.position().top;
|
||||
}
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (position) {
|
||||
position.bottom = "unset";
|
||||
if (this.site.desktopView) {
|
||||
const avatarOverflowSize = 44;
|
||||
this._popperReference = createPopper(target[0], this.element, {
|
||||
placement: "right",
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
padding: {
|
||||
top: headerOffset() + avatarOverflowSize,
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
left: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: "eventListeners", enabled: false },
|
||||
{ name: "offset", options: { offset: [10, 10] } },
|
||||
],
|
||||
});
|
||||
} else {
|
||||
document.querySelector(".card-cloak")?.classList.remove("hidden");
|
||||
this._popperReference = createPopper(target[0], this.element, {
|
||||
modifiers: [
|
||||
{ name: "eventListeners", enabled: false },
|
||||
{
|
||||
name: "computeStyles",
|
||||
enabled: true,
|
||||
fn({ state }) {
|
||||
// mimics our modal top of the screen positioning
|
||||
state.styles.popper = {
|
||||
...state.styles.popper,
|
||||
position: "fixed",
|
||||
left: `${
|
||||
(window.innerWidth - state.rects.popper.width) / 2
|
||||
}px`,
|
||||
top: "10%",
|
||||
transform: "translateY(-10%)",
|
||||
};
|
||||
|
||||
if (rtl) {
|
||||
// The site direction is rtl
|
||||
position.right = $(window).width() - position.left + 10;
|
||||
position.left = "auto";
|
||||
let overage = $(window).width() - 50 - (position.right + width);
|
||||
if (overage < 0) {
|
||||
position.right += overage;
|
||||
position.top += target.height() + 48;
|
||||
verticalAdjustments += target.height() + 48;
|
||||
}
|
||||
} else {
|
||||
// The site direction is ltr
|
||||
position.left += target.width() + 10;
|
||||
return state;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
let overage = $(window).width() - 50 - (position.left + width);
|
||||
if (overage < 0) {
|
||||
position.left += overage;
|
||||
position.top += target.height() + 48;
|
||||
verticalAdjustments += target.height() + 48;
|
||||
}
|
||||
}
|
||||
this.element.classList.toggle("docked-card", this.isDocked);
|
||||
|
||||
// It looks better to have the card aligned slightly higher
|
||||
position.top -= 24;
|
||||
|
||||
if (isFixed) {
|
||||
position.top -= $("html").scrollTop();
|
||||
//if content is fixed and will be cut off on the bottom, display it above...
|
||||
if (
|
||||
position.top + height + verticalAdjustments >
|
||||
$(window).height() - 50
|
||||
) {
|
||||
position.bottom =
|
||||
$(window).height() -
|
||||
(target.offset().top - $("html").scrollTop());
|
||||
if (verticalAdjustments > 0) {
|
||||
position.bottom += 48;
|
||||
}
|
||||
position.top = "unset";
|
||||
}
|
||||
}
|
||||
|
||||
const avatarOverflowSize = 44;
|
||||
if (isDocked && position.top < avatarOverflowSize) {
|
||||
position.top = avatarOverflowSize;
|
||||
}
|
||||
|
||||
$(this.element).css(position);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.site.mobileView) {
|
||||
$(".card-cloak").removeClass("hidden");
|
||||
let position = target.offset();
|
||||
position.top = "10%"; // match modal behaviour
|
||||
position.left = 0;
|
||||
$(this.element).css(position);
|
||||
}
|
||||
$(this.element).toggleClass("docked-card", isDocked);
|
||||
|
||||
// After the card is shown, focus on the first link
|
||||
//
|
||||
// note: we DO NOT use afterRender here cause _positionCard may
|
||||
// run afterwards, if we allowed this to happen the usercard
|
||||
// may be offscreen and we may scroll all the way to it on focus
|
||||
if (event.pointerId === -1) {
|
||||
discourseLater(() => {
|
||||
const firstLink = this.element.querySelector("a");
|
||||
firstLink && firstLink.focus();
|
||||
}, 350);
|
||||
}
|
||||
// After the card is shown, focus on the first link
|
||||
//
|
||||
// note: we DO NOT use afterRender here cause _positionCard may
|
||||
// run afterwards, if we allowed this to happen the usercard
|
||||
// may be offscreen and we may scroll all the way to it on focus
|
||||
if (event.pointerId === -1) {
|
||||
discourseLater(() => {
|
||||
this.element.querySelector("a")?.focus();
|
||||
}, 350);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@bind
|
||||
_hide() {
|
||||
this.element.dataset.popperPlacement = "";
|
||||
|
||||
if (!this.visible) {
|
||||
$(this.element).css({ left: -9999, top: -9999 });
|
||||
if (this.site.mobileView) {
|
||||
|
@ -307,7 +283,6 @@ export default Mixin.create({
|
|||
loading: null,
|
||||
cardTarget: null,
|
||||
post: null,
|
||||
isFixed: false,
|
||||
isDocked: false,
|
||||
});
|
||||
|
||||
|
|
|
@ -37,14 +37,17 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards
|
|||
color: var(--primary);
|
||||
background: var(--secondary) center center;
|
||||
background-size: cover;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
opacity: 0;
|
||||
outline: 2px solid transparent;
|
||||
@include transform(scale(0.9));
|
||||
&.show {
|
||||
opacity: 1;
|
||||
@include transform(scale(1));
|
||||
|
||||
&[data-popper-placement] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&[data-popper-placement]:not([data-popper-placement=""]) {
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 10px;
|
||||
background: rgba(var(--secondary-rgb), 0.85);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// shared styles for user and group cards
|
||||
.user-card,
|
||||
.group-card {
|
||||
position: absolute;
|
||||
z-index: z("usercard");
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
|
|
|
@ -3,7 +3,6 @@ $avatar_width: 120px;
|
|||
// shared styles for user and group cards
|
||||
.user-card,
|
||||
.group-card {
|
||||
position: fixed;
|
||||
// mobile cards should always be on top of everything - 1102
|
||||
z-index: z("mobile-composer") + 2;
|
||||
max-width: 95vw;
|
||||
|
|
Loading…
Reference in New Issue