FIX: uses tippy for popover (#15409)

Note this commit also introduce a new {{d-popover}} component, example usage:

```hbs
{{#d-popover |state|}}
  {{d-button label="foo.things" class="d-popover-trigger"}}

  <div class="d-popover-content">
    Some content
  <div>
{{/d-popover}}
```
This commit is contained in:
Joffrey JAFFEUX
2022-05-02 17:10:26 +02:00
committed by GitHub
parent 4885a2535a
commit fad94160c7
25 changed files with 2801 additions and 307 deletions
@@ -25,6 +25,10 @@ define("@popperjs/core", ["exports"], function (__exports__) {
__exports__.popperGenerator = window.Popper.popperGenerator;
});
define("tippy.js", ["exports"], function (__exports__) {
__exports__.default = window.tippy;
});
define("@uppy/core", ["exports"], function (__exports__) {
__exports__.default = window.Uppy.Core;
__exports__.BasePlugin = window.Uppy.Core.BasePlugin;
@@ -0,0 +1,61 @@
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
import tippy from "tippy.js";
import { guidFor } from "@ember/object/internals";
export default class DiscoursePopover extends Component {
tagName = "";
isExpanded = false;
options = null;
didInsertElement() {
this._super(...arguments);
this._setupTippy();
}
get componentId() {
return guidFor(this);
}
_setupTippy() {
const baseOptions = {
trigger: "click",
zIndex: 1400,
arrow: iconHTML("tippy-rounded-arrow"),
interactive: true,
allowHTML: false,
appendTo: "parent",
content:
this.options?.content ||
document
.getElementById(this.componentId)
.querySelector(
":scope > .d-popover-content, :scope > div, :scope > ul"
),
onShow: () => {
if (this.isDestroyed || this.isDestroying) {
return;
}
this.set("isExpanded", true);
},
onHide: () => {
if (this.isDestroyed || this.isDestroying) {
return;
}
this.set("isExpanded", false);
},
};
tippy(
document
.getElementById(this.componentId)
.querySelector(
':scope > .d-popover-trigger, :scope > .btn, :scope > [role="button"]'
),
Object.assign({}, baseOptions, this.options || {})
);
}
}
@@ -1,20 +1,19 @@
import {
POPOVER_SELECTORS,
hidePopover,
showPopover,
} from "discourse/lib/d-popover";
import { showPopover } from "discourse/lib/d-popover";
export default {
name: "d-popover",
initialize(container) {
const router = container.lookup("router:main");
router.on("routeWillChange", hidePopover);
$("#main")
.on("click.d-popover mouseenter.d-popover", POPOVER_SELECTORS, (e) =>
showPopover(e)
)
.on("mouseleave.d-popover", POPOVER_SELECTORS, (e) => hidePopover(e));
initialize() {
["click", "mouseover"].forEach((eventType) => {
document.addEventListener(eventType, (e) => {
if (e.target.dataset.tooltip || e.target.dataset.popover) {
showPopover(e, {
interactive: false,
content: (reference) =>
reference.dataset.tooltip || reference.dataset.popover,
});
}
});
});
},
};
@@ -1,168 +1,48 @@
import { siteDir } from "discourse/lib/text-direction";
import { isLegacyEmber } from "discourse-common/config/environment";
import { run } from "@ember/runloop";
import tippy from "tippy.js";
import { iconHTML } from "discourse-common/lib/icon-library";
const D_POPOVER_ID = "d-popover";
const D_POPOVER_TEMPLATE = `
<div id="${D_POPOVER_ID}" class="is-under">
<div class="d-popover-arrow d-popover-top-arrow"></div>
<div class="d-popover-content">
<div class="spinner small"></div>
</div>
<div class="d-popover-arrow d-popover-bottom-arrow"></div>
</div>
`;
const D_ARROW_HEIGHT = 10;
const D_HORIZONTAL_MARGIN = 5;
export const POPOVER_SELECTORS = "[data-popover], [data-tooltip]";
export function hidePopover() {
getPopover().fadeOut().remove();
return getPopover();
export function hidePopover(event) {
if (event?.target?._tippy) {
showPopover(event);
}
}
// options accepts all tippy.js options as defined in their documentation
// https://atomiks.github.io/tippyjs/v6/all-props/
export function showPopover(event, options = {}) {
let $enteredElement = $(event.target).closest(POPOVER_SELECTORS).first();
const tippyOptions = Object.assign(
{
arrow: iconHTML("tippy-rounded-arrow"),
content: options.textContent || options.htmlContent,
allowHTML: options?.htmlContent?.length,
trigger: "mouseenter click",
hideOnClick: true,
zIndex: 1400,
},
options
);
if (!$enteredElement.length) {
$enteredElement = $(event.target);
// legacy support
delete tippyOptions.textContent;
delete tippyOptions.htmlContent;
const instance = event.target._tippy
? event.target._tippy
: tippy(event.target, tippyOptions);
// hangs on legacy ember
if (!isLegacyEmber) {
run.begin();
instance.popper.addEventListener("transitionend", run.end, {
once: true,
});
}
if (isRetina()) {
getPopover().addClass("retina");
}
if (!getPopover().length) {
$("body").append($(D_POPOVER_TEMPLATE));
}
setPopoverHtmlContent($enteredElement, options.htmlContent);
setPopoverTextContent($enteredElement, options.textContent);
getPopover().fadeIn();
positionPopover($enteredElement);
return {
html: (content) => replaceHtmlContent($enteredElement, content),
text: (content) => replaceTextContent($enteredElement, content),
hide: hidePopover,
};
}
function setPopoverHtmlContent($enteredElement, content) {
replaceHtmlContent($enteredElement, content);
}
function setPopoverTextContent($enteredElement, content) {
content =
content ||
$enteredElement.attr("data-popover") ||
$enteredElement.attr("data-tooltip");
replaceTextContent($enteredElement, content);
}
function replaceTextContent($enteredElement, content) {
if (content) {
getPopover().find(".d-popover-content").text(content);
window.requestAnimationFrame(() => positionPopover($enteredElement));
}
}
function replaceHtmlContent($enteredElement, content) {
if (content) {
getPopover().find(".d-popover-content").html(content);
window.requestAnimationFrame(() => positionPopover($enteredElement));
}
}
function positionPopover($element) {
const $popover = getPopover();
$popover.removeClass("is-above is-under is-left-aligned is-right-aligned");
const $dHeader = $(".d-header");
const windowRect = {
left: 0,
top: $dHeader.length ? $dHeader[0].getBoundingClientRect().bottom : 0,
width: $(window).width(),
height: $(window).height(),
};
const popoverRect = {
width: $popover.width(),
height: $popover.height(),
left: null,
right: null,
};
if (popoverRect.width > windowRect.width - D_HORIZONTAL_MARGIN * 2) {
popoverRect.width = windowRect.width - D_HORIZONTAL_MARGIN * 2;
$popover.width(popoverRect.width);
}
const targetRect = $element[0].getBoundingClientRect();
const underSpace = windowRect.height - targetRect.bottom - D_ARROW_HEIGHT;
const topSpace = targetRect.top - windowRect.top - D_ARROW_HEIGHT;
if (
underSpace > popoverRect.height + D_HORIZONTAL_MARGIN ||
underSpace > topSpace
) {
$popover
.css("top", targetRect.bottom + window.pageYOffset + D_ARROW_HEIGHT)
.addClass("is-under");
if (instance.state.isShown) {
instance.hide();
} else {
$popover
.css(
"top",
targetRect.top +
window.pageYOffset -
popoverRect.height -
D_ARROW_HEIGHT
)
.addClass("is-above");
instance.show();
}
const leftSpace = targetRect.left + targetRect.width / 2;
if (siteDir() === "ltr") {
if (leftSpace > popoverRect.width / 2 + D_HORIZONTAL_MARGIN) {
popoverRect.left = leftSpace - popoverRect.width / 2;
$popover.css("left", popoverRect.left);
} else {
popoverRect.left = D_HORIZONTAL_MARGIN;
$popover.css("left", popoverRect.left).addClass("is-left-aligned");
}
} else {
const rightSpace = windowRect.width - targetRect.right;
if (rightSpace > popoverRect.width / 2 + D_HORIZONTAL_MARGIN) {
popoverRect.left = leftSpace - popoverRect.width / 2;
$popover.css("left", popoverRect.left);
} else {
popoverRect.left =
windowRect.width - popoverRect.width - D_HORIZONTAL_MARGIN * 2;
$popover.css("left", popoverRect.left).addClass("is-right-aligned");
}
}
let arrowPosition;
if (siteDir() === "ltr") {
arrowPosition = Math.abs(targetRect.left - popoverRect.left);
} else {
arrowPosition = targetRect.left - popoverRect.left + targetRect.width / 2;
}
$popover.find(".d-popover-arrow").css("left", arrowPosition);
}
function isRetina() {
return window.devicePixelRatio && window.devicePixelRatio > 1;
}
function getPopover() {
return $(document.getElementById(D_POPOVER_ID));
}
@@ -0,0 +1,3 @@
<div id={{componentId}} class="d-popover {{if isExpanded "is-expanded"}}">
{{yield (hash isExpanded=isExpanded)}}
</div>
@@ -23,6 +23,7 @@
"@ember/test-helpers": "^2.2.0",
"@glimmer/component": "^1.0.4",
"@glimmer/tracking": "^1.0.4",
"tippy.js": "^6.3.7",
"@popperjs/core": "2.10.2",
"@uppy/aws-s3": "^2.0.8",
"@uppy/aws-s3-multipart": "^2.2.1",
@@ -0,0 +1,76 @@
import componentTest, {
setupRenderingTest,
} from "discourse/tests/helpers/component-test";
import {
discourseModule,
exists,
query,
} from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import { showPopover } from "discourse/lib/d-popover";
import { click } from "@ember/test-helpers";
discourseModule("Integration | Component | d-popover", function (hooks) {
setupRenderingTest(hooks);
componentTest("show/hide popover from lib", {
template: hbs`{{d-button translatedLabel="test" action=onButtonClick forwardEvent=true}}`,
beforeEach() {
this.set("onButtonClick", (_, event) => {
showPopover(event, { content: "test", trigger: "click", duration: 0 });
});
},
async test(assert) {
assert.notOk(document.querySelector("div[data-tippy-root]"));
await click(".btn");
assert.equal(
document.querySelector("div[data-tippy-root]").innerText.trim(),
"test"
);
await click(".btn");
assert.notOk(document.querySelector("div[data-tippy-root]"));
},
});
componentTest("show/hide popover from component", {
template: hbs`{{#d-popover}}{{d-button icon="chevron-down"}}<ul><li class="test">foo</li></ul>{{/d-popover}}`,
async test(assert) {
assert.notOk(exists(".d-popover.is-expanded"));
assert.notOk(exists(".test"));
await click(".btn");
assert.ok(exists(".d-popover.is-expanded"));
assert.equal(query(".test").innerText.trim(), "foo");
},
});
componentTest("using options with component", {
template: hbs`{{#d-popover options=(hash content="bar")}}{{d-button icon="chevron-down"}}{{/d-popover}}`,
async test(assert) {
await click(".btn");
assert.equal(query(".tippy-content").innerText.trim(), "bar");
},
});
componentTest("d-popover component accepts a block", {
template: hbs`{{#d-popover as |state|}}{{d-button icon=(if state.isExpanded "chevron-up" "chevron-down")}}{{/d-popover}}`,
async test(assert) {
assert.ok(exists(".d-icon-chevron-down"));
await click(".btn");
assert.ok(exists(".d-icon-chevron-up"));
},
});
});
+1
View File
@@ -6,6 +6,7 @@
//= require Markdown.Converter.js
//= require bootbox.js
//= require popper.js
//= require tippy.umd.js
//= require bootstrap-modal.js
//= require caret_position
//= require itsatrap.js
+4
View File
@@ -5,6 +5,10 @@ define("@popperjs/core", ["exports"], function (__exports__) {
__exports__.popperGenerator = window.Popper.popperGenerator;
});
define("tippy.js", ["exports"], function (__exports__) {
__exports__.default = window.tippy;
});
define("@uppy/core", ["exports"], function (__exports__) {
__exports__.default = window.Uppy.Core;
__exports__.BasePlugin = window.Uppy.Core.BasePlugin;
+1
View File
@@ -6,4 +6,5 @@
//= require virtual-dom
//= require virtual-dom-amd
//= require popper.js
//= require tippy.umd.js
//= require wizard-shims
+12
View File
@@ -2120,6 +2120,11 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590"
integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==
"@popperjs/core@^2.9.0":
version "2.11.0"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.0.tgz#6734f8ebc106a0860dff7f92bf90df193f0935d7"
integrity sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ==
"@simple-dom/interface@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@simple-dom/interface/-/interface-1.4.0.tgz#e8feea579232017f89b0138e2726facda6fbb71f"
@@ -12905,6 +12910,13 @@ tiny-lr@^2.0.0:
object-assign "^4.1.0"
qs "^6.4.0"
tippy.js@^6.3.7:
version "6.3.7"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==
dependencies:
"@popperjs/core" "^2.9.0"
tmp@0.0.28:
version "0.0.28"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120"
+2
View File
@@ -2,6 +2,8 @@
@import "vendor/normalize";
@import "vendor/normalize-ext";
@import "vendor/pikaday";
@import "vendor/tippy";
@import "vendor/svg-arrow";
@import "common/foundation/helpers";
@import "common/foundation/base";
@import "common/select-kit/_index";
+22 -104
View File
@@ -1,112 +1,30 @@
$d-popover-background: var(--secondary);
$d-popover-border: var(--primary-medium);
@-webkit-keyframes popoverFadeIn {
from {
opacity: 0;
.tippy-box {
color: var(--primary);
background: $d-popover-background;
box-shadow: shadow("card");
border: 1px solid $d-popover-border;
}
.tippy-box[data-placement^="top"] .tippy-svg-arrow > svg {
top: 12px;
}
.tippy-box[data-placement^="bottom"] .tippy-svg-arrow > svg {
top: -10px;
}
.tippy-rounded-arrow {
fill: $d-popover-background;
.svg-arrow {
fill: $d-popover-border;
}
to {
opacity: 1;
.svg-content {
fill: $d-popover-background;
}
}
@keyframes popoverFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
#d-popover {
background-color: $d-popover-background;
position: absolute;
z-index: z("modal", "tooltip");
border-color: $d-popover-border;
border-style: solid;
border-width: 1px;
max-width: 300px;
-webkit-animation: popoverFadeIn 0.5s;
animation: popoverFadeIn 0.5s;
background-clip: padding-box;
display: block;
box-shadow: shadow("dropdown");
border-radius: 2px;
&.is-under {
margin-top: 0px;
.d-popover-top-arrow {
display: block;
}
.d-popover-bottom-arrow {
display: none;
}
}
&.is-above {
margin-top: 0px;
.d-popover-bottom-arrow {
display: block;
}
.d-popover-top-arrow {
display: none;
}
}
&.retina {
border-width: 0.5px;
}
.d-popover-content {
padding: 0.5em;
font-size: $font-down-1;
overflow-wrap: break-word;
-webkit-animation: popoverFadeIn 0.5s;
animation: popoverFadeIn 0.5s;
}
.d-popover-arrow {
border-style: solid;
color: transparent;
content: "";
position: absolute;
z-index: z("tooltip") - 100;
}
.d-popover-top-arrow {
border-color: transparent transparent $d-popover-border;
top: -8px;
border-width: 0 8px 8px;
&:after {
border-color: transparent transparent $d-popover-background;
border-style: solid;
border-width: 0 7px 7px;
bottom: -8.5px;
margin-left: -7px;
position: absolute;
content: "";
}
}
.d-popover-bottom-arrow {
border-color: $d-popover-border transparent transparent;
top: 100%;
border-width: 8px 8px 0;
&:after {
position: absolute;
content: "";
border-color: $d-popover-background transparent transparent;
border-style: solid;
border-width: 7px 7px 0;
bottom: 1.5px;
transform: translate(-7px, 0);
}
}
[data-tooltip] > *,
[data-popover] > * {
pointer-events: none;
}
@@ -2,7 +2,7 @@
margin-top: 2em;
}
#d-popover .download-calendar {
div[data-tippy-root] .download-calendar {
color: var(--primary-med-or-secondary-med);
}
+1
View File
@@ -0,0 +1 @@
.tippy-box[data-placement^=top]>.tippy-svg-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-svg-arrow:after,.tippy-box[data-placement^=top]>.tippy-svg-arrow>svg{top:16px;transform:rotate(180deg)}.tippy-box[data-placement^=bottom]>.tippy-svg-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-svg-arrow>svg{bottom:16px}.tippy-box[data-placement^=left]>.tippy-svg-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-svg-arrow:after,.tippy-box[data-placement^=left]>.tippy-svg-arrow>svg{transform:rotate(90deg);top:calc(50% - 3px);left:11px}.tippy-box[data-placement^=right]>.tippy-svg-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-svg-arrow:after,.tippy-box[data-placement^=right]>.tippy-svg-arrow>svg{transform:rotate(-90deg);top:calc(50% - 3px);right:11px}.tippy-svg-arrow{width:16px;height:16px;fill:#333;text-align:initial}.tippy-svg-arrow,.tippy-svg-arrow>svg{position:absolute}
+1
View File
@@ -0,0 +1 @@
.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}