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:
Joffrey JAFFEUX 2023-01-30 14:15:10 +01:00 committed by GitHub
parent 18f7b47ecb
commit 335c3f4621
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 78 additions and 102 deletions

View File

@ -2,12 +2,13 @@ import { alias, match } from "@ember/object/computed";
import { schedule, throttle } from "@ember/runloop"; import { schedule, throttle } from "@ember/runloop";
import DiscourseURL from "discourse/lib/url"; import DiscourseURL from "discourse/lib/url";
import Mixin from "@ember/object/mixin"; import Mixin from "@ember/object/mixin";
import afterTransition from "discourse/lib/after-transition";
import { escapeExpression } from "discourse/lib/utilities"; import { escapeExpression } from "discourse/lib/utilities";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { wantsNewWindow } from "discourse/lib/intercept-click"; import { wantsNewWindow } from "discourse/lib/intercept-click";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import discourseLater from "discourse-common/lib/later"; import discourseLater from "discourse-common/lib/later";
import { createPopper } from "@popperjs/core";
import { headerOffset } from "discourse/lib/offset-calculator";
const DEFAULT_SELECTOR = "#main-outlet"; const DEFAULT_SELECTOR = "#main-outlet";
@ -27,6 +28,7 @@ export default Mixin.create({
elementId: null, //click detection added for data-{elementId} elementId: null, //click detection added for data-{elementId}
triggeringLinkClass: null, //the <a> classname where this card should appear 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. _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"), postStream: alias("topic.postStream"),
viewingTopic: match("router.currentRouteName", /^topic\./), viewingTopic: match("router.currentRouteName", /^topic\./),
@ -36,7 +38,6 @@ export default Mixin.create({
loading: null, loading: null,
cardTarget: null, cardTarget: null,
post: null, post: null,
isFixed: false,
isDocked: false, isDocked: false,
_show(username, target, event) { _show(username, target, event) {
@ -58,6 +59,10 @@ export default Mixin.create({
const currentUsername = this.username; const currentUsername = this.username;
if (username === currentUsername || this.loading === 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)); this._positionCard($(target));
return; return;
} }
@ -100,7 +105,7 @@ export default Mixin.create({
didInsertElement() { didInsertElement() {
this._super(...arguments); this._super(...arguments);
afterTransition($(this.element), this._hide);
const id = this.elementId; const id = this.elementId;
const triggeringLinkClass = this.triggeringLinkClass; const triggeringLinkClass = this.triggeringLinkClass;
const previewClickEvent = `click.discourse-preview-${id}-${triggeringLinkClass}`; const previewClickEvent = `click.discourse-preview-${id}-${triggeringLinkClass}`;
@ -168,7 +173,7 @@ export default Mixin.create({
}, },
_topicHeaderTrigger(username, target) { _topicHeaderTrigger(username, target) {
this.setProperties({ isFixed: true, isDocked: true }); this.setProperties({ isDocked: true });
return this._show(username, target); return this._show(username, target);
}, },
@ -188,92 +193,63 @@ export default Mixin.create({
}, },
_previewClick($target) { _previewClick($target) {
this.set("isFixed", true);
return this._show($target.text().replace(/^@/, ""), $target); return this._show($target.text().replace(/^@/, ""), $target);
}, },
_positionCard(target) { _positionCard(target) {
const rtl = $("html").css("direction") === "rtl"; schedule("afterRender", () => {
if (!target) { if (!target) {
return; 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 (position) {
position.bottom = "unset";
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;
let overage = $(window).width() - 50 - (position.left + width);
if (overage < 0) {
position.left += overage;
position.top += target.height() + 48;
verticalAdjustments += target.height() + 48;
}
}
// 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";
}
}
if (this.site.desktopView) {
const avatarOverflowSize = 44; const avatarOverflowSize = 44;
if (isDocked && position.top < avatarOverflowSize) { this._popperReference = createPopper(target[0], this.element, {
position.top = avatarOverflowSize; 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%)",
};
return state;
},
},
],
});
} }
$(this.element).css(position); this.element.classList.toggle("docked-card", this.isDocked);
}
}
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 // After the card is shown, focus on the first link
// //
@ -282,16 +258,16 @@ export default Mixin.create({
// may be offscreen and we may scroll all the way to it on focus // may be offscreen and we may scroll all the way to it on focus
if (event.pointerId === -1) { if (event.pointerId === -1) {
discourseLater(() => { discourseLater(() => {
const firstLink = this.element.querySelector("a"); this.element.querySelector("a")?.focus();
firstLink && firstLink.focus();
}, 350); }, 350);
} }
}
}); });
}, },
@bind @bind
_hide() { _hide() {
this.element.dataset.popperPlacement = "";
if (!this.visible) { if (!this.visible) {
$(this.element).css({ left: -9999, top: -9999 }); $(this.element).css({ left: -9999, top: -9999 });
if (this.site.mobileView) { if (this.site.mobileView) {
@ -307,7 +283,6 @@ export default Mixin.create({
loading: null, loading: null,
cardTarget: null, cardTarget: null,
post: null, post: null,
isFixed: false,
isDocked: false, isDocked: false,
}); });

View File

@ -37,14 +37,17 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards
color: var(--primary); color: var(--primary);
background: var(--secondary) center center; background: var(--secondary) center center;
background-size: cover; background-size: cover;
transition: opacity 0.2s, transform 0.2s;
opacity: 0;
outline: 2px solid transparent; outline: 2px solid transparent;
@include transform(scale(0.9));
&.show { &[data-popper-placement] {
opacity: 1; opacity: 0;
@include transform(scale(1));
} }
&[data-popper-placement]:not([data-popper-placement=""]) {
opacity: 1;
transition: opacity 0.5s;
}
.card-content { .card-content {
padding: 10px; padding: 10px;
background: rgba(var(--secondary-rgb), 0.85); background: rgba(var(--secondary-rgb), 0.85);

View File

@ -1,7 +1,6 @@
// shared styles for user and group cards // shared styles for user and group cards
.user-card, .user-card,
.group-card { .group-card {
position: absolute;
z-index: z("usercard"); z-index: z("usercard");
&.fixed { &.fixed {
position: fixed; position: fixed;

View File

@ -3,7 +3,6 @@ $avatar_width: 120px;
// shared styles for user and group cards // shared styles for user and group cards
.user-card, .user-card,
.group-card { .group-card {
position: fixed;
// mobile cards should always be on top of everything - 1102 // mobile cards should always be on top of everything - 1102
z-index: z("mobile-composer") + 2; z-index: z("mobile-composer") + 2;
max-width: 95vw; max-width: 95vw;