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,110 +193,81 @@ 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";
if (!target) {
return;
}
const width = $(this.element).width();
const height = 175;
const isFixed = this.isFixed;
const isDocked = this.isDocked;
let verticalAdjustments = 0;
schedule("afterRender", () => { schedule("afterRender", () => {
if (target) { if (!target) {
if (!this.site.mobileView) { return;
let position = target.offset(); }
if (target.parents(".d-header").length > 0) {
position.top = target.position().top;
}
if (position) { if (this.site.desktopView) {
position.bottom = "unset"; 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) { return state;
// 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); this.element.classList.toggle("docked-card", this.isDocked);
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 // After the card is shown, focus on the first link
position.top -= 24; //
// note: we DO NOT use afterRender here cause _positionCard may
if (isFixed) { // run afterwards, if we allowed this to happen the usercard
position.top -= $("html").scrollTop(); // may be offscreen and we may scroll all the way to it on focus
//if content is fixed and will be cut off on the bottom, display it above... if (event.pointerId === -1) {
if ( discourseLater(() => {
position.top + height + verticalAdjustments > this.element.querySelector("a")?.focus();
$(window).height() - 50 }, 350);
) {
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);
}
} }
}); });
}, },
@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;