DEV: allows for multiple menus/tooltips (#26823)
menus and tooltips are now appended to their own portals. The service are the only responsible for managing the instances, prior to this commit, services could manage one instance, but the DMenu and DTooltip components could also take over which could cause unexpected states. This change also allows nested menus/tooltips. Other notable changes: - few months ago core copied the CloseOnClickOutside modifier of float-kit without removing the float-kit one, this commit now only use the core one. - the close function is now trully async - the close function accepts an instance or an identifier as parameter
This commit is contained in:
parent
95302cc7ed
commit
fe16633a0c
|
@ -80,7 +80,7 @@ export default class PostTextSelection extends Component {
|
|||
super.willDestroy(...arguments);
|
||||
|
||||
cancel(this.debouncedSelectionChanged);
|
||||
this.menuInstance?.destroy();
|
||||
this.menuInstance?.close();
|
||||
}
|
||||
|
||||
@bind
|
||||
|
|
|
@ -41,7 +41,7 @@ export default class UserTip extends Component {
|
|||
buttonText = `${iconHTML(this.args.buttonIcon)} ${buttonText}`;
|
||||
}
|
||||
|
||||
instance = new DTooltipInstance(getOwner(this), trigger || element, {
|
||||
instance = new DTooltipInstance(getOwner(this), {
|
||||
identifier: "user-tip",
|
||||
interactive: true,
|
||||
closeOnScroll: false,
|
||||
|
@ -60,6 +60,8 @@ export default class UserTip extends Component {
|
|||
showSkipButton: this.args.showSkipButton,
|
||||
},
|
||||
});
|
||||
instance.trigger = trigger || element;
|
||||
instance.detachedTrigger = true;
|
||||
|
||||
this.tooltip.show(instance);
|
||||
|
||||
|
|
|
@ -22,6 +22,10 @@ export class UserStatusMessage {
|
|||
}
|
||||
|
||||
destroy() {
|
||||
if (this.tooltip.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tooltipInstance.destroy();
|
||||
}
|
||||
|
||||
|
|
|
@ -8,9 +8,13 @@ export default class CloseOnClickOutside extends Modifier {
|
|||
registerDestructor(this, (instance) => instance.cleanup());
|
||||
}
|
||||
|
||||
modify(element, [closeFn, { targetSelector, secondaryTargetSelector }]) {
|
||||
modify(
|
||||
element,
|
||||
[closeFn, { targetSelector, secondaryTargetSelector, target }]
|
||||
) {
|
||||
this.closeFn = closeFn;
|
||||
this.element = element;
|
||||
this.target = target;
|
||||
this.targetSelector = targetSelector;
|
||||
this.secondaryTargetSelector = secondaryTargetSelector;
|
||||
|
||||
|
@ -25,8 +29,10 @@ export default class CloseOnClickOutside extends Modifier {
|
|||
return;
|
||||
}
|
||||
|
||||
const target = this.target ?? document.querySelector(this.targetSelector);
|
||||
|
||||
if (
|
||||
document.querySelector(this.targetSelector)?.contains(event.target) ||
|
||||
target?.contains(event.target) ||
|
||||
(this.secondaryTargetSelector &&
|
||||
document
|
||||
.querySelector(this.secondaryTargetSelector)
|
||||
|
|
|
@ -126,6 +126,6 @@
|
|||
{{/if}}
|
||||
</DiscourseRoot>
|
||||
|
||||
<DInlineMenu />
|
||||
<DInlineTooltip />
|
||||
<DMenus />
|
||||
<DTooltips />
|
||||
<DToasts />
|
|
@ -1,3 +1,4 @@
|
|||
import { getOwner } from "@ember/application";
|
||||
import {
|
||||
click,
|
||||
find,
|
||||
|
@ -255,4 +256,13 @@ module("Integration | Component | FloatKit | d-menu", function (hooks) {
|
|||
|
||||
assert.dom(document.activeElement).hasClass("my-button");
|
||||
});
|
||||
|
||||
test("a menu can be closed by identifier", async function (assert) {
|
||||
await render(hbs`<DMenu @inline={{true}} @identifier="test">test</DMenu>`);
|
||||
await open();
|
||||
|
||||
await getOwner(this).lookup("service:menu").close("test");
|
||||
|
||||
assert.dom(".fk-d-menu__content.test-content").doesNotExist();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { getOwner } from "@ember/application";
|
||||
import {
|
||||
click,
|
||||
find,
|
||||
|
@ -40,7 +41,7 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
|
|||
);
|
||||
await hover();
|
||||
|
||||
assert.dom(".fk-d-tooltip").hasText("content");
|
||||
assert.dom(".fk-d-tooltip__content").hasText("content");
|
||||
});
|
||||
|
||||
test("@onRegisterApi", async function (assert) {
|
||||
|
@ -107,7 +108,7 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
|
|||
|
||||
await hover();
|
||||
|
||||
assert.dom(".fk-d-tooltip").hasAttribute("data-identifier", "tip");
|
||||
assert.dom(".fk-d-tooltip__content").hasAttribute("data-identifier", "tip");
|
||||
});
|
||||
|
||||
test("aria-expanded attribute", async function (assert) {
|
||||
|
@ -135,7 +136,7 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
|
|||
|
||||
await hover();
|
||||
|
||||
assert.dom(".fk-d-tooltip").hasText("content");
|
||||
assert.dom(".fk-d-tooltip__content").hasText("content");
|
||||
});
|
||||
|
||||
test("content role attribute", async function (assert) {
|
||||
|
@ -143,7 +144,7 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
|
|||
|
||||
await hover();
|
||||
|
||||
assert.dom(".fk-d-tooltip").hasAttribute("role", "tooltip");
|
||||
assert.dom(".fk-d-tooltip__content").hasAttribute("role", "tooltip");
|
||||
});
|
||||
|
||||
test("@component", async function (assert) {
|
||||
|
@ -155,11 +156,11 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
|
|||
|
||||
await hover();
|
||||
|
||||
assert.dom(".fk-d-tooltip").containsText("content");
|
||||
assert.dom(".fk-d-tooltip__content").containsText("content");
|
||||
|
||||
await click(".fk-d-tooltip .btn");
|
||||
await click(".fk-d-tooltip__content .btn");
|
||||
|
||||
assert.dom(".fk-d-tooltip").doesNotExist();
|
||||
assert.dom(".fk-d-tooltip__content").doesNotExist();
|
||||
});
|
||||
|
||||
test("content aria-labelledby attribute", async function (assert) {
|
||||
|
@ -169,7 +170,9 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
|
|||
|
||||
assert.strictEqual(
|
||||
document.querySelector(".fk-d-tooltip__trigger").id,
|
||||
document.querySelector(".fk-d-tooltip").getAttribute("aria-labelledby")
|
||||
document
|
||||
.querySelector(".fk-d-tooltip__content")
|
||||
.getAttribute("aria-labelledby")
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -180,7 +183,7 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
|
|||
await hover();
|
||||
await close();
|
||||
|
||||
assert.dom(".fk-d-tooltip").doesNotExist();
|
||||
assert.dom(".fk-d-tooltip__content").doesNotExist();
|
||||
|
||||
await render(
|
||||
hbs`<DTooltip @inline={{true}} @label="label" @closeOnEscape={{false}} />`
|
||||
|
@ -188,7 +191,7 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
|
|||
await hover();
|
||||
await close();
|
||||
|
||||
assert.dom(".fk-d-tooltip").exists();
|
||||
assert.dom(".fk-d-tooltip__content").exists();
|
||||
});
|
||||
|
||||
test("@closeOnClickOutside", async function (assert) {
|
||||
|
@ -198,7 +201,7 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
|
|||
await hover();
|
||||
await triggerEvent(".test", "pointerdown");
|
||||
|
||||
assert.dom(".fk-d-tooltip").doesNotExist();
|
||||
assert.dom(".fk-d-tooltip__content").doesNotExist();
|
||||
|
||||
await render(
|
||||
hbs`<span class="test">test</span><DTooltip @inline={{true}} @label="label" @closeOnClickOutside={{false}} />`
|
||||
|
@ -206,7 +209,7 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
|
|||
await hover();
|
||||
await triggerEvent(".test", "pointerdown");
|
||||
|
||||
assert.dom(".fk-d-tooltip").exists();
|
||||
assert.dom(".fk-d-tooltip__content").exists();
|
||||
});
|
||||
|
||||
test("@maxWidth", async function (assert) {
|
||||
|
@ -216,15 +219,32 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
|
|||
await hover();
|
||||
|
||||
assert.ok(
|
||||
find(".fk-d-tooltip").getAttribute("style").includes("max-width: 20px;")
|
||||
find(".fk-d-tooltip__content")
|
||||
.getAttribute("style")
|
||||
.includes("max-width: 20px;")
|
||||
);
|
||||
});
|
||||
|
||||
test("applies position", async function (assert) {
|
||||
await render(hbs`<DTooltip @inline={{true}} @label="label" />`);
|
||||
await render(hbs`<DTooltip @inline={{true}} @label="label" />`);
|
||||
await hover();
|
||||
|
||||
assert.ok(find(".fk-d-tooltip").getAttribute("style").includes("left: "));
|
||||
assert.ok(find(".fk-d-tooltip").getAttribute("style").includes("top: "));
|
||||
assert.ok(
|
||||
find(".fk-d-tooltip__content").getAttribute("style").includes("left: ")
|
||||
);
|
||||
assert.ok(
|
||||
find(".fk-d-tooltip__content").getAttribute("style").includes("top: ")
|
||||
);
|
||||
});
|
||||
|
||||
test("a tooltip can be closed by identifier", async function (assert) {
|
||||
await render(
|
||||
hbs`<DTooltip @inline={{true}} @label="label" @identifier="test">test</DTooltip>`
|
||||
);
|
||||
await open();
|
||||
|
||||
await getOwner(this).lookup("service:tooltip").close("test");
|
||||
|
||||
assert.dom(".fk-d-tooltip__content.test-content").doesNotExist();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -125,7 +125,7 @@ module("Integration | Component | user-info", function (hooks) {
|
|||
this.currentUser.status = { emoji: "tooth", description: "off to dentist" };
|
||||
|
||||
await render(
|
||||
hbs`<UserInfo @user={{this.currentUser}} @showStatus={{true}} /><DInlineTooltip />`
|
||||
hbs`<UserInfo @user={{this.currentUser}} @showStatus={{true}} /><DTooltips />`
|
||||
);
|
||||
await triggerEvent(query(".user-status-message"), "mousemove");
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ module("Integration | Component | user-status-message", function (hooks) {
|
|||
this.status.ends_at = "2100-02-01T12:30:00.000Z";
|
||||
|
||||
await render(
|
||||
hbs`<UserStatusMessage @status={{this.status}} /><DInlineTooltip />`
|
||||
hbs`<UserStatusMessage @status={{this.status}} /><DTooltips />`
|
||||
);
|
||||
await mouseenter();
|
||||
|
||||
|
@ -66,7 +66,7 @@ module("Integration | Component | user-status-message", function (hooks) {
|
|||
this.status.ends_at = "2100-02-02T12:30:00.000Z";
|
||||
|
||||
await render(
|
||||
hbs`<UserStatusMessage @status={{this.status}} /><DInlineTooltip />`
|
||||
hbs`<UserStatusMessage @status={{this.status}} /><DTooltips />`
|
||||
);
|
||||
await mouseenter();
|
||||
|
||||
|
@ -84,7 +84,7 @@ module("Integration | Component | user-status-message", function (hooks) {
|
|||
this.status.ends_at = null;
|
||||
|
||||
await render(
|
||||
hbs`<UserStatusMessage @status={{this.status}} /><DInlineTooltip />`
|
||||
hbs`<UserStatusMessage @status={{this.status}} /><DTooltips />`
|
||||
);
|
||||
await mouseenter();
|
||||
|
||||
|
@ -97,7 +97,7 @@ module("Integration | Component | user-status-message", function (hooks) {
|
|||
|
||||
test("it shows tooltip by default", async function (assert) {
|
||||
await render(
|
||||
hbs`<UserStatusMessage @status={{this.status}} /><DInlineTooltip />`
|
||||
hbs`<UserStatusMessage @status={{this.status}} /><DTooltips />`
|
||||
);
|
||||
await mouseenter();
|
||||
|
||||
|
|
|
@ -576,7 +576,7 @@ module("Integration | Component | Widget | post", function (hooks) {
|
|||
this.set("args", { canManage: true });
|
||||
|
||||
await render(
|
||||
hbs`<MountWidget @widget="post" @args={{this.args}} /><DInlineMenu />`
|
||||
hbs`<MountWidget @widget="post" @args={{this.args}} /><DMenus />`
|
||||
);
|
||||
|
||||
assert
|
||||
|
@ -597,7 +597,7 @@ module("Integration | Component | Widget | post", function (hooks) {
|
|||
this.set("permanentlyDeletePost", () => (this.deleted = true));
|
||||
|
||||
await render(
|
||||
hbs`<MountWidget @widget="post" @args={{this.args}} @permanentlyDeletePost={{this.permanentlyDeletePost}} /><DInlineMenu />`
|
||||
hbs`<MountWidget @widget="post" @args={{this.args}} @permanentlyDeletePost={{this.permanentlyDeletePost}} /><DMenus />`
|
||||
);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
|
@ -616,7 +616,7 @@ module("Integration | Component | Widget | post", function (hooks) {
|
|||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @permanentlyDeletePost={{this.permanentlyDeletePost}} />
|
||||
<DInlineMenu />
|
||||
<DMenus />
|
||||
`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
|
@ -637,7 +637,7 @@ module("Integration | Component | Widget | post", function (hooks) {
|
|||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @togglePostType={{this.togglePostType}} />
|
||||
<DInlineMenu />
|
||||
<DMenus />
|
||||
`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
|
@ -657,7 +657,7 @@ module("Integration | Component | Widget | post", function (hooks) {
|
|||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @rebakePost={{this.rebakePost}} />
|
||||
<DInlineMenu />
|
||||
<DMenus />
|
||||
`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
|
@ -678,7 +678,7 @@ module("Integration | Component | Widget | post", function (hooks) {
|
|||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @unhidePost={{this.unhidePost}} />
|
||||
<DInlineMenu />
|
||||
<DMenus />
|
||||
`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
|
@ -701,7 +701,7 @@ module("Integration | Component | Widget | post", function (hooks) {
|
|||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @changePostOwner={{this.changePostOwner}} />
|
||||
<DInlineMenu />
|
||||
<DMenus />
|
||||
`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { concat } from "@ember/helper";
|
||||
import { concat, hash } from "@ember/helper";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { modifier as modifierFn } from "ember-modifier";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import closeOnClickOutside from "discourse/modifiers/close-on-click-outside";
|
||||
import TrapTab from "discourse/modifiers/trap-tab";
|
||||
import DFloatPortal from "float-kit/components/d-float-portal";
|
||||
import { getScrollParent } from "float-kit/lib/get-scroll-parent";
|
||||
import FloatKitApplyFloatingUi from "float-kit/modifiers/apply-floating-ui";
|
||||
import FloatKitCloseOnClickOutside from "float-kit/modifiers/close-on-click-outside";
|
||||
import FloatKitCloseOnEscape from "float-kit/modifiers/close-on-escape";
|
||||
|
||||
export default class DFloatBody extends Component {
|
||||
|
@ -38,7 +38,11 @@ export default class DFloatBody extends Component {
|
|||
}
|
||||
|
||||
get trigger() {
|
||||
return this.args.instance.trigger;
|
||||
return this.args.instance?.trigger;
|
||||
}
|
||||
|
||||
get content() {
|
||||
return this.args.instance?.content;
|
||||
}
|
||||
|
||||
get options() {
|
||||
|
@ -48,7 +52,7 @@ export default class DFloatBody extends Component {
|
|||
<template>
|
||||
<DFloatPortal
|
||||
@inline={{@inline}}
|
||||
@portalOutletElement={{@portalOutletElement}}
|
||||
@portalOutletElement={{@instance.portalOutletElement}}
|
||||
>
|
||||
<div
|
||||
class={{concatClass
|
||||
|
@ -66,7 +70,9 @@ export default class DFloatBody extends Component {
|
|||
{{(if @trapTab (modifier TrapTab autofocus=this.options.autofocus))}}
|
||||
{{(if
|
||||
this.supportsCloseOnClickOutside
|
||||
(modifier FloatKitCloseOnClickOutside this.trigger @instance.close)
|
||||
(modifier
|
||||
closeOnClickOutside @instance.close (hash target=this.content)
|
||||
)
|
||||
)}}
|
||||
{{(if
|
||||
this.supportsCloseOnEscape
|
||||
|
|
|
@ -10,7 +10,7 @@ export default class DFloatPortal extends Component {
|
|||
{{#if this.inline}}
|
||||
{{yield}}
|
||||
{{else}}
|
||||
{{#in-element @portalOutletElement}}
|
||||
{{#in-element @portalOutletElement insertBefore=null}}
|
||||
{{yield}}
|
||||
{{/in-element}}
|
||||
{{/if}}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import DInlineFloat from "float-kit/components/d-inline-float";
|
||||
|
||||
const DHeadlessMenu = <template>
|
||||
<DInlineFloat
|
||||
@instance={{@menu}}
|
||||
@trapTab={{@menu.options.trapTab}}
|
||||
@mainClass="fk-d-menu"
|
||||
@innerClass="fk-d-menu__inner-content"
|
||||
@role="dialog"
|
||||
@inline={{@inline}}
|
||||
/>
|
||||
</template>;
|
||||
|
||||
export default DHeadlessMenu;
|
|
@ -0,0 +1,15 @@
|
|||
import { and } from "truth-helpers";
|
||||
import DInlineFloat from "float-kit/components/d-inline-float";
|
||||
|
||||
const DHeadlessTooltip = <template>
|
||||
<DInlineFloat
|
||||
@instance={{@tooltip}}
|
||||
@trapTab={{and @tooltip.options.interactive @tooltip.options.trapTab}}
|
||||
@mainClass="fk-d-tooltip__content"
|
||||
@innerClass="fk-d-tooltip__inner-content"
|
||||
@role="tooltip"
|
||||
@inline={{@inline}}
|
||||
/>
|
||||
</template>;
|
||||
|
||||
export default DHeadlessTooltip;
|
|
@ -32,7 +32,7 @@ export default class DInlineFloat extends Component {
|
|||
@mainClass={{@mainClass}}
|
||||
@innerClass={{@innerClass}}
|
||||
@role={{@role}}
|
||||
@portalOutletElement={{@portalOutletElement}}
|
||||
@portalOutletElement={{@instance.portalOutletElement}}
|
||||
@inline={{@inline}}
|
||||
>
|
||||
{{#if @instance.options.component}}
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import Component from "@glimmer/component";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { service } from "@ember/service";
|
||||
import DInlineFloat from "float-kit/components/d-inline-float";
|
||||
import { MENU } from "float-kit/lib/constants";
|
||||
|
||||
export default class DInlineMenu extends Component {
|
||||
@service menu;
|
||||
|
||||
<template>
|
||||
<div
|
||||
id={{MENU.portalOutletId}}
|
||||
{{didInsert this.menu.registerPortalOutletElement}}
|
||||
></div>
|
||||
|
||||
<DInlineFloat
|
||||
@instance={{this.menu.activeMenu}}
|
||||
@portalOutletElement={{this.menu.portalOutletElement}}
|
||||
@trapTab={{this.menu.activeMenu.options.trapTab}}
|
||||
@mainClass="fk-d-menu"
|
||||
@innerClass="fk-d-menu__inner-content"
|
||||
@role="dialog"
|
||||
@inline={{@inline}}
|
||||
/>
|
||||
</template>
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import Component from "@glimmer/component";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { service } from "@ember/service";
|
||||
import { and } from "truth-helpers";
|
||||
import DInlineFloat from "float-kit/components/d-inline-float";
|
||||
import { TOOLTIP } from "float-kit/lib/constants";
|
||||
|
||||
export default class DInlineTooltip extends Component {
|
||||
@service tooltip;
|
||||
|
||||
<template>
|
||||
<div
|
||||
id={{TOOLTIP.portalOutletId}}
|
||||
{{didInsert this.tooltip.registerPortalOutletElement}}
|
||||
></div>
|
||||
|
||||
<DInlineFloat
|
||||
@instance={{this.tooltip.activeTooltip}}
|
||||
@portalOutletElement={{this.tooltip.portalOutletElement}}
|
||||
@trapTab={{and
|
||||
this.tooltip.activeTooltip.options.interactive
|
||||
this.tooltip.activeTooltip.options.trapTab
|
||||
}}
|
||||
@mainClass="fk-d-tooltip"
|
||||
@innerClass="fk-d-tooltip__inner-content"
|
||||
@role="tooltip"
|
||||
@inline={{@inline}}
|
||||
/>
|
||||
</template>
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { cached } from "@glimmer/tracking";
|
||||
import { getOwner } from "@ember/application";
|
||||
import { concat } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { next } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { modifier } from "ember-modifier";
|
||||
import { and } from "truth-helpers";
|
||||
|
@ -18,33 +19,25 @@ export default class DMenu extends Component {
|
|||
@service menu;
|
||||
@service site;
|
||||
|
||||
@tracked menuInstance = null;
|
||||
registerTrigger = modifier((element) => {
|
||||
if (!this.menuInstance.trigger) {
|
||||
next(() => {
|
||||
this.menuInstance.trigger = element;
|
||||
this.options.onRegisterApi?.(this.menuInstance);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
registerTrigger = modifier((element, [properties]) => {
|
||||
const options = {
|
||||
...properties,
|
||||
@cached
|
||||
get menuInstance() {
|
||||
return new DMenuInstance(getOwner(this), {
|
||||
...this.allowedProperties(),
|
||||
...{
|
||||
autoUpdate: true,
|
||||
listeners: true,
|
||||
beforeTrigger: () => {
|
||||
this.menu.close();
|
||||
},
|
||||
},
|
||||
};
|
||||
const instance = new DMenuInstance(getOwner(this), element, options);
|
||||
|
||||
this.menuInstance = instance;
|
||||
|
||||
this.options.onRegisterApi?.(this.menuInstance);
|
||||
|
||||
return () => {
|
||||
instance.destroy();
|
||||
|
||||
if (this.isDestroying) {
|
||||
this.menuInstance = null;
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get menuId() {
|
||||
return `d-menu-${this.menuInstance.id}`;
|
||||
|
@ -88,7 +81,7 @@ export default class DMenu extends Component {
|
|||
@translatedTitle={{@title}}
|
||||
@disabled={{@disabled}}
|
||||
aria-expanded={{if this.menuInstance.expanded "true" "false"}}
|
||||
{{this.registerTrigger (this.allowedProperties)}}
|
||||
{{this.registerTrigger}}
|
||||
...attributes
|
||||
>
|
||||
{{#if (has-block "trigger")}}
|
||||
|
@ -126,12 +119,12 @@ export default class DMenu extends Component {
|
|||
@trapTab={{this.options.trapTab}}
|
||||
@mainClass={{concatClass
|
||||
"fk-d-menu"
|
||||
"fk-d-menu__content"
|
||||
(concat this.options.identifier "-content")
|
||||
}}
|
||||
@innerClass="fk-d-menu__inner-content"
|
||||
@role="dialog"
|
||||
@inline={{this.options.inline}}
|
||||
@portalOutletElement={{this.menu.portalOutletElement}}
|
||||
>
|
||||
{{#if (has-block)}}
|
||||
{{yield this.componentArgs}}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DHeadlessMenu from "float-kit/components/d-headless-menu";
|
||||
|
||||
export default class DMenus extends Component {
|
||||
@service menu;
|
||||
|
||||
<template>
|
||||
<div id="d-menu-portals"></div>
|
||||
|
||||
{{#each this.menu.registeredMenus key="id" as |menu|}}
|
||||
{{#if menu.detachedTrigger}}
|
||||
<DHeadlessMenu @menu={{menu}} />
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</template>
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { cached } from "@glimmer/tracking";
|
||||
import { getOwner } from "@ember/application";
|
||||
import { concat } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { next } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { modifier } from "ember-modifier";
|
||||
import { and } from "truth-helpers";
|
||||
|
@ -15,34 +17,23 @@ export default class DTooltip extends Component {
|
|||
@service tooltip;
|
||||
@service internalTooltip;
|
||||
|
||||
@tracked tooltipInstance = null;
|
||||
|
||||
registerTrigger = modifier((element, [properties]) => {
|
||||
const options = {
|
||||
...properties,
|
||||
...{
|
||||
listeners: true,
|
||||
beforeTrigger: (instance) => {
|
||||
this.internalTooltip.activeTooltip?.close?.();
|
||||
this.internalTooltip.activeTooltip = instance;
|
||||
},
|
||||
},
|
||||
};
|
||||
const instance = new DTooltipInstance(getOwner(this), element, options);
|
||||
|
||||
this.tooltipInstance = instance;
|
||||
|
||||
this.options.onRegisterApi?.(instance);
|
||||
|
||||
return () => {
|
||||
instance.destroy();
|
||||
|
||||
if (this.isDestroying) {
|
||||
this.tooltipInstance = null;
|
||||
}
|
||||
};
|
||||
registerTrigger = modifier((element) => {
|
||||
if (!this.tooltipInstance?.trigger) {
|
||||
next(() => {
|
||||
this.tooltipInstance.trigger = element;
|
||||
this.options.onRegisterApi?.(this.tooltipInstance);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@cached
|
||||
get tooltipInstance() {
|
||||
return new DTooltipInstance(getOwner(this), {
|
||||
...this.allowedProperties(),
|
||||
...{ autoUpdate: true, listeners: true },
|
||||
});
|
||||
}
|
||||
|
||||
get options() {
|
||||
return this.tooltipInstance?.options;
|
||||
}
|
||||
|
@ -98,11 +89,13 @@ export default class DTooltip extends Component {
|
|||
<DFloatBody
|
||||
@instance={{this.tooltipInstance}}
|
||||
@trapTab={{and this.options.interactive this.options.trapTab}}
|
||||
@mainClass="fk-d-tooltip"
|
||||
@mainClass={{concatClass
|
||||
"fk-d-tooltip__content"
|
||||
(concat this.options.identifier "-content")
|
||||
}}
|
||||
@innerClass="fk-d-tooltip__inner-content"
|
||||
@role="tooltip"
|
||||
@inline={{this.options.inline}}
|
||||
@portalOutletElement={{this.tooltip.portalOutletElement}}
|
||||
>
|
||||
{{#if (has-block)}}
|
||||
{{yield this.componentArgs}}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DHeadlessTooltip from "float-kit/components/d-headless-tooltip";
|
||||
|
||||
export default class DTooltips extends Component {
|
||||
@service tooltip;
|
||||
|
||||
<template>
|
||||
<div id="d-tooltip-portals"></div>
|
||||
|
||||
{{#each this.tooltip.registeredTooltips key="id" as |tooltip|}}
|
||||
{{#if tooltip.detachedTrigger}}
|
||||
<DHeadlessTooltip @tooltip={{tooltip}} />
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</template>
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { setOwner } from "@ember/application";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { getOwner, setOwner } from "@ember/application";
|
||||
import { action } from "@ember/object";
|
||||
import { guidFor } from "@ember/object/internals";
|
||||
import { service } from "@ember/service";
|
||||
|
@ -10,54 +11,97 @@ export default class DMenuInstance extends FloatKitInstance {
|
|||
@service site;
|
||||
@service modal;
|
||||
|
||||
constructor(owner, trigger, options = {}) {
|
||||
/**
|
||||
* Indicates whether the menu is expanded or not.
|
||||
* @property {boolean} expanded - Tracks the state of menu expansion, initially set to false.
|
||||
*/
|
||||
@tracked expanded = false;
|
||||
|
||||
/**
|
||||
* Specifies whether the trigger for opening/closing the menu is detached from the menu itself.
|
||||
* This is the case when a menu is trigger programmaticaly instead of through the <DMenu /> component.
|
||||
* @property {boolean} detachedTrigger - Tracks whether the trigger is detached, initially set to false.
|
||||
*/
|
||||
@tracked detachedTrigger = false;
|
||||
|
||||
/**
|
||||
* Configuration options for the DMenuInstance.
|
||||
* @property {Object} options - Options object that configures the menu behavior and display.
|
||||
*/
|
||||
@tracked options;
|
||||
|
||||
@tracked _trigger;
|
||||
|
||||
constructor(owner, options = {}) {
|
||||
super(...arguments);
|
||||
|
||||
setOwner(this, owner);
|
||||
this.options = { ...MENU.options, ...options };
|
||||
this.id = trigger.id || guidFor(trigger);
|
||||
this.trigger = trigger;
|
||||
}
|
||||
|
||||
get portalOutletElement() {
|
||||
return document.getElementById("d-menu-portals");
|
||||
}
|
||||
|
||||
get trigger() {
|
||||
return this._trigger;
|
||||
}
|
||||
|
||||
set trigger(element) {
|
||||
this._trigger = element;
|
||||
this.id = element.id || guidFor(element);
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
@action
|
||||
close() {
|
||||
async close() {
|
||||
if (getOwner(this).isDestroying) {
|
||||
return;
|
||||
}
|
||||
|
||||
await super.close(...arguments);
|
||||
|
||||
if (this.site.mobileView && this.options.modalForMobile) {
|
||||
this.modal.close();
|
||||
await this.modal.close();
|
||||
}
|
||||
|
||||
super.close(...arguments);
|
||||
await this.menu.close(this);
|
||||
}
|
||||
|
||||
@action
|
||||
onMouseMove(event) {
|
||||
if (this.trigger.contains(event.target) && this.expanded) {
|
||||
async show() {
|
||||
await super.show(...arguments);
|
||||
await this.menu.show(this);
|
||||
}
|
||||
|
||||
@action
|
||||
async onMouseMove(event) {
|
||||
if (this.expanded && this.trigger.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onTrigger(event);
|
||||
await this.onTrigger(event);
|
||||
}
|
||||
|
||||
@action
|
||||
onClick(event) {
|
||||
async onClick(event) {
|
||||
if (this.expanded && this.untriggers.includes("click")) {
|
||||
this.onUntrigger(event);
|
||||
return;
|
||||
return await this.onUntrigger(event);
|
||||
}
|
||||
|
||||
this.onTrigger(event);
|
||||
await this.onTrigger(event);
|
||||
}
|
||||
|
||||
@action
|
||||
onMouseLeave(event) {
|
||||
async onMouseLeave(event) {
|
||||
if (this.untriggers.includes("hover")) {
|
||||
this.onUntrigger(event);
|
||||
await this.onUntrigger(event);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async onTrigger() {
|
||||
this.options.beforeTrigger?.(this);
|
||||
await this.options.beforeTrigger?.(this);
|
||||
await this.show();
|
||||
}
|
||||
|
||||
|
@ -67,8 +111,7 @@ export default class DMenuInstance extends FloatKitInstance {
|
|||
}
|
||||
|
||||
@action
|
||||
async destroy() {
|
||||
await this.close();
|
||||
destroy() {
|
||||
this.tearDownListeners();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import { setOwner } from "@ember/application";
|
||||
import { action } from "@ember/object";
|
||||
import { guidFor } from "@ember/object/internals";
|
||||
|
@ -8,45 +9,89 @@ import FloatKitInstance from "float-kit/lib/float-kit-instance";
|
|||
export default class DTooltipInstance extends FloatKitInstance {
|
||||
@service tooltip;
|
||||
|
||||
constructor(owner, trigger, options = {}) {
|
||||
/**
|
||||
* Indicates whether the tooltip is expanded or not.
|
||||
* @property {boolean} expanded - Tracks the state of tooltip expansion, initially set to false.
|
||||
*/
|
||||
@tracked expanded = false;
|
||||
|
||||
/**
|
||||
* Specifies whether the trigger for opening/closing the tooltip is detached from the tooltip itself.
|
||||
* This is the case when a tooltip is trigger programmaticaly instead of through the <DTooltip /> component.
|
||||
* @property {boolean} detachedTrigger - Tracks whether the trigger is detached, initially set to false.
|
||||
*/
|
||||
@tracked detachedTrigger = false;
|
||||
|
||||
/**
|
||||
* Configuration options for the DTooltipInstance.
|
||||
* @property {Object} options - Options object that configures the tooltip behavior and display.
|
||||
*/
|
||||
@tracked options;
|
||||
|
||||
@tracked _trigger;
|
||||
|
||||
constructor(owner, options = {}) {
|
||||
super(...arguments);
|
||||
|
||||
setOwner(this, owner);
|
||||
this.options = { ...TOOLTIP.options, ...options };
|
||||
this.id = trigger.id || guidFor(trigger);
|
||||
this.trigger = trigger;
|
||||
}
|
||||
|
||||
get trigger() {
|
||||
return this._trigger;
|
||||
}
|
||||
|
||||
set trigger(element) {
|
||||
this._trigger = element;
|
||||
this.id = element.id || guidFor(element);
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
@action
|
||||
onMouseMove(event) {
|
||||
if (this.trigger.contains(event.target) && this.expanded) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onTrigger(event);
|
||||
get portalOutletElement() {
|
||||
return document.getElementById("d-tooltip-portals");
|
||||
}
|
||||
|
||||
@action
|
||||
onClick(event) {
|
||||
async show() {
|
||||
await this.tooltip.show(this);
|
||||
await super.show(...arguments);
|
||||
}
|
||||
|
||||
@action
|
||||
async close() {
|
||||
await this.tooltip.close(this);
|
||||
|
||||
await super.close(...arguments);
|
||||
}
|
||||
|
||||
@action
|
||||
async onMouseMove(event) {
|
||||
if (this.expanded && this.trigger.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.onTrigger(event);
|
||||
}
|
||||
|
||||
@action
|
||||
async onClick(event) {
|
||||
if (this.expanded && this.untriggers.includes("click")) {
|
||||
this.onUntrigger(event);
|
||||
return;
|
||||
return await this.onUntrigger(event);
|
||||
}
|
||||
|
||||
this.onTrigger(event);
|
||||
await this.onTrigger(event);
|
||||
}
|
||||
|
||||
@action
|
||||
onMouseLeave(event) {
|
||||
async onMouseLeave(event) {
|
||||
if (this.untriggers.includes("hover")) {
|
||||
this.onUntrigger(event);
|
||||
await this.onUntrigger(event);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async onTrigger() {
|
||||
this.options.beforeTrigger?.(this);
|
||||
await this.options.beforeTrigger?.(this);
|
||||
await this.show();
|
||||
}
|
||||
|
||||
|
@ -57,7 +102,6 @@ export default class DTooltipInstance extends FloatKitInstance {
|
|||
|
||||
@action
|
||||
destroy() {
|
||||
this.close();
|
||||
this.tearDownListeners();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { cancel, next } from "@ember/runloop";
|
||||
import { cancel } from "@ember/runloop";
|
||||
import { makeArray } from "discourse-common/lib/helpers";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
@ -13,48 +13,44 @@ function cancelEvent(event) {
|
|||
}
|
||||
|
||||
export default class FloatKitInstance {
|
||||
@tracked expanded = false;
|
||||
@tracked id = null;
|
||||
|
||||
trigger = null;
|
||||
content = null;
|
||||
|
||||
@action
|
||||
show() {
|
||||
this.expanded = true;
|
||||
|
||||
next(() => {
|
||||
this.options.onShow?.();
|
||||
});
|
||||
async show() {
|
||||
await this.options.onShow?.();
|
||||
}
|
||||
|
||||
@action
|
||||
close() {
|
||||
this.expanded = false;
|
||||
|
||||
next(() => {
|
||||
this.options.onClose?.();
|
||||
});
|
||||
async close() {
|
||||
await this.options.onClose?.();
|
||||
}
|
||||
|
||||
@action
|
||||
onFocus(event) {
|
||||
this.onTrigger(event);
|
||||
async onFocus(event) {
|
||||
await this.onTrigger(event);
|
||||
}
|
||||
|
||||
@action
|
||||
onBlur(event) {
|
||||
this.onTrigger(event);
|
||||
async onBlur(event) {
|
||||
await this.onTrigger(event);
|
||||
}
|
||||
|
||||
@action
|
||||
onFocusIn(event) {
|
||||
this.onTrigger(event);
|
||||
async onFocusIn(event) {
|
||||
await this.onTrigger(event);
|
||||
}
|
||||
|
||||
@action
|
||||
onFocusOut(event) {
|
||||
this.onTrigger(event);
|
||||
async onFocusOut(event) {
|
||||
await this.onTrigger(event);
|
||||
}
|
||||
|
||||
@action
|
||||
trapPointerDown(event) {
|
||||
// this is done to avoid trigger on click outside when you click on your own trigger
|
||||
// given trigger and content are not in the same div, we can't just check if target is
|
||||
// inside the menu
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -105,7 +101,11 @@ export default class FloatKitInstance {
|
|||
}
|
||||
|
||||
tearDownListeners() {
|
||||
if (!this.options.listeners) {
|
||||
if (typeof this.trigger.addEventListener === "function") {
|
||||
this.trigger.removeEventListener("pointerdown", this.trapPointerDown);
|
||||
}
|
||||
|
||||
if (!this.options?.listeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -141,7 +141,11 @@ export default class FloatKitInstance {
|
|||
}
|
||||
|
||||
setupListeners() {
|
||||
if (!this.options.listeners) {
|
||||
if (typeof this.trigger.addEventListener === "function") {
|
||||
this.trigger.addEventListener("pointerdown", this.trapPointerDown);
|
||||
}
|
||||
|
||||
if (!this.options?.listeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
import { registerDestructor } from "@ember/destroyable";
|
||||
import Modifier from "ember-modifier";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
export default class FloatKitCloseOnClickOutside extends Modifier {
|
||||
constructor(owner, args) {
|
||||
super(owner, args);
|
||||
registerDestructor(this, (instance) => instance.cleanup());
|
||||
}
|
||||
|
||||
modify(element, [trigger, closeFn]) {
|
||||
this.closeFn = closeFn;
|
||||
this.trigger = trigger;
|
||||
this.element = element;
|
||||
|
||||
document.addEventListener("pointerdown", this.check, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
check(event) {
|
||||
if (this.element.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.trigger instanceof HTMLElement &&
|
||||
this.trigger.contains(event.target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeFn();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
document.removeEventListener("pointerdown", this.check);
|
||||
}
|
||||
}
|
|
@ -1,14 +1,12 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import { getOwner } from "@ember/application";
|
||||
import { action } from "@ember/object";
|
||||
import { guidFor } from "@ember/object/internals";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import Service from "@ember/service";
|
||||
import DMenuInstance from "float-kit/lib/d-menu-instance";
|
||||
import { updatePosition } from "float-kit/lib/update-position";
|
||||
|
||||
export default class Menu extends Service {
|
||||
@tracked activeMenu;
|
||||
@tracked portalOutletElement;
|
||||
@tracked registeredMenus = [];
|
||||
|
||||
/**
|
||||
* Render a menu
|
||||
|
@ -34,36 +32,49 @@ export default class Menu extends Service {
|
|||
if (arguments[0] instanceof DMenuInstance) {
|
||||
instance = arguments[0];
|
||||
|
||||
if (this.activeMenu === instance && this.activeMenu.expanded) {
|
||||
if (instance.expanded) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const trigger = arguments[0];
|
||||
if (
|
||||
this.activeMenu &&
|
||||
this.activeMenu.id ===
|
||||
(trigger?.id?.length ? trigger.id : guidFor(trigger)) &&
|
||||
this.activeMenu.expanded
|
||||
) {
|
||||
this.activeMenu?.close();
|
||||
return;
|
||||
}
|
||||
instance = this.registeredMenus.find(
|
||||
(registeredMenu) => registeredMenu.trigger === arguments[0]
|
||||
);
|
||||
|
||||
instance = new DMenuInstance(getOwner(this), trigger, arguments[1]);
|
||||
if (!instance) {
|
||||
instance = new DMenuInstance(getOwner(this), arguments[1]);
|
||||
instance.trigger = arguments[0];
|
||||
instance.detachedTrigger = true;
|
||||
}
|
||||
}
|
||||
|
||||
await this.replace(instance);
|
||||
instance.expanded = true;
|
||||
return instance;
|
||||
}
|
||||
if (instance.options.identifier) {
|
||||
for (const menu of this.registeredMenus) {
|
||||
if (
|
||||
menu.options.identifier === instance.options.identifier &&
|
||||
menu !== instance
|
||||
) {
|
||||
await this.close(menu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces any active menu-
|
||||
*/
|
||||
@action
|
||||
async replace(menu) {
|
||||
await this.activeMenu?.close();
|
||||
this.activeMenu = menu;
|
||||
if (instance.expanded) {
|
||||
return await this.close(instance);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => {
|
||||
if (!this.registeredMenus.includes(instance)) {
|
||||
this.registeredMenus = this.registeredMenus.concat(instance);
|
||||
}
|
||||
|
||||
instance.expanded = true;
|
||||
|
||||
schedule("afterRender", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -72,26 +83,27 @@ export default class Menu extends Service {
|
|||
*/
|
||||
@action
|
||||
async close(menu) {
|
||||
if (this.activeMenu && menu && this.activeMenu.id !== menu.id) {
|
||||
if (typeof menu === "string") {
|
||||
menu = this.registeredMenus.find(
|
||||
(registeredMenu) => registeredMenu.options.identifier === menu
|
||||
);
|
||||
}
|
||||
|
||||
if (!menu) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.activeMenu?.close();
|
||||
this.activeMenu = null;
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
menu.expanded = false;
|
||||
|
||||
/**
|
||||
* Update the menu position
|
||||
* @param {DMenuInstance} [menu] - the menu to update, if not provider will update any active menu
|
||||
*/
|
||||
@action
|
||||
async update(menu) {
|
||||
const instance = menu || this.activeMenu;
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
await updatePosition(instance.trigger, instance.content, instance.options);
|
||||
await instance.show();
|
||||
this.registeredMenus = this.registeredMenus.filter(
|
||||
(registeredMenu) => menu.id !== registeredMenu.id
|
||||
);
|
||||
|
||||
schedule("afterRender", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -104,17 +116,12 @@ export default class Menu extends Service {
|
|||
*/
|
||||
@action
|
||||
register(trigger, options = {}) {
|
||||
return new DMenuInstance(getOwner(this), trigger, {
|
||||
const instance = new DMenuInstance(getOwner(this), {
|
||||
...options,
|
||||
listeners: true,
|
||||
beforeTrigger: async (menu) => {
|
||||
await this.replace(menu);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
registerPortalOutletElement(element) {
|
||||
this.portalOutletElement = element;
|
||||
instance.trigger = trigger;
|
||||
instance.detachedTrigger = true;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import { getOwner } from "@ember/application";
|
||||
import { action } from "@ember/object";
|
||||
import { guidFor } from "@ember/object/internals";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import Service from "@ember/service";
|
||||
import DTooltipInstance from "float-kit/lib/d-tooltip-instance";
|
||||
import { updatePosition } from "float-kit/lib/update-position";
|
||||
|
||||
export default class Tooltip extends Service {
|
||||
@tracked activeTooltip;
|
||||
@tracked portalOutletElement;
|
||||
@tracked registeredTooltips = [];
|
||||
|
||||
/**
|
||||
* Render a tooltip
|
||||
|
@ -34,36 +32,48 @@ export default class Tooltip extends Service {
|
|||
if (arguments[0] instanceof DTooltipInstance) {
|
||||
instance = arguments[0];
|
||||
|
||||
if (this.activeTooltip === instance && this.activeTooltip.expanded) {
|
||||
if (instance.expanded) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const trigger = arguments[0];
|
||||
if (
|
||||
this.activeTooltip &&
|
||||
this.activeTooltip.id ===
|
||||
(trigger?.id?.length ? trigger.id : guidFor(trigger)) &&
|
||||
this.activeTooltip.expanded
|
||||
) {
|
||||
this.activeTooltip?.close();
|
||||
return;
|
||||
instance = this.registeredTooltips.find(
|
||||
(registeredTooltips) => registeredTooltips.trigger === arguments[0]
|
||||
);
|
||||
if (!instance) {
|
||||
instance = new DTooltipInstance(getOwner(this), arguments[1]);
|
||||
instance.trigger = arguments[0];
|
||||
instance.detachedTrigger = true;
|
||||
}
|
||||
|
||||
instance = new DTooltipInstance(getOwner(this), trigger, arguments[1]);
|
||||
}
|
||||
|
||||
await this.replace(instance);
|
||||
instance.expanded = true;
|
||||
return instance;
|
||||
}
|
||||
if (instance.options.identifier) {
|
||||
for (const tooltip of this.registeredTooltips) {
|
||||
if (
|
||||
tooltip.options.identifier === instance.options.identifier &&
|
||||
tooltip !== instance
|
||||
) {
|
||||
await this.close(tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces any active tooltip
|
||||
*/
|
||||
@action
|
||||
async replace(tooltip) {
|
||||
await this.activeTooltip?.close();
|
||||
this.activeTooltip = tooltip;
|
||||
if (instance.expanded) {
|
||||
return await this.close(instance);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => {
|
||||
if (!this.registeredTooltips.includes(instance)) {
|
||||
this.registeredTooltips = this.registeredTooltips.concat(instance);
|
||||
}
|
||||
|
||||
instance.expanded = true;
|
||||
|
||||
schedule("afterRender", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -72,26 +82,27 @@ export default class Tooltip extends Service {
|
|||
*/
|
||||
@action
|
||||
async close(tooltip) {
|
||||
if (this.activeTooltip && tooltip && this.activeTooltip.id !== tooltip.id) {
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = this.registeredTooltips.find(
|
||||
(registeredTooltip) => registeredTooltip.options.identifier === tooltip
|
||||
);
|
||||
}
|
||||
|
||||
if (!tooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.activeTooltip?.close();
|
||||
this.activeTooltip = null;
|
||||
}
|
||||
tooltip.expanded = false;
|
||||
|
||||
/**
|
||||
* Update the tooltip position
|
||||
* @param {DTooltipInstance} [tooltip] - the tooltip to update, if not provider will update any active tooltip
|
||||
*/
|
||||
@action
|
||||
async update(tooltip) {
|
||||
const instance = tooltip || this.activeTooltip;
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
await updatePosition(instance.trigger, instance.content, instance.options);
|
||||
await instance.show();
|
||||
await new Promise((resolve) => {
|
||||
this.registeredTooltips = this.registeredTooltips.filter(
|
||||
(registeredTooltips) => tooltip.id !== registeredTooltips.id
|
||||
);
|
||||
|
||||
schedule("afterRender", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -104,17 +115,12 @@ export default class Tooltip extends Service {
|
|||
*/
|
||||
@action
|
||||
register(trigger, options = {}) {
|
||||
return new DTooltipInstance(getOwner(this), trigger, {
|
||||
const instance = new DTooltipInstance(getOwner(this), {
|
||||
...options,
|
||||
listeners: true,
|
||||
beforeTrigger: async (tooltip) => {
|
||||
await this.replace(tooltip);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
registerPortalOutletElement(element) {
|
||||
this.portalOutletElement = element;
|
||||
instance.trigger = trigger;
|
||||
instance.detachedTrigger = true;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { default } from "float-kit/components/d-headless-menu";
|
|
@ -0,0 +1 @@
|
|||
export { default } from "float-kit/components/d-headless-tooltip";
|
|
@ -1 +0,0 @@
|
|||
export { default } from "float-kit/components/d-inline-menu";
|
|
@ -1 +0,0 @@
|
|||
export { default } from "float-kit/components/d-inline-tooltip";
|
|
@ -0,0 +1 @@
|
|||
export { default } from "float-kit/components/d-menus";
|
|
@ -0,0 +1 @@
|
|||
export { default } from "float-kit/components/d-tooltips";
|
|
@ -9,17 +9,6 @@
|
|||
}
|
||||
|
||||
.fk-d-tooltip {
|
||||
background-color: var(--secondary);
|
||||
border-radius: var(--d-border-radius);
|
||||
border: 1px solid var(--primary-low);
|
||||
box-shadow: var(--shadow-menu-panel);
|
||||
z-index: z("max");
|
||||
width: max-content;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex !important;
|
||||
padding: 0;
|
||||
|
||||
&__trigger {
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
|
@ -33,26 +22,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.-animated {
|
||||
animation: d-tooltip-opening 0.15s ease-in;
|
||||
|
||||
&[data-placement^="bottom"] {
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
&[data-placement^="top"] {
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
&[data-placement^="right"] {
|
||||
transform-origin: center left;
|
||||
}
|
||||
|
||||
&[data-placement^="left"] {
|
||||
transform-origin: center right;
|
||||
}
|
||||
}
|
||||
|
||||
&__inner-content {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
@ -61,41 +30,74 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
&__content {
|
||||
background-color: var(--secondary);
|
||||
border-radius: var(--d-border-radius);
|
||||
border: 1px solid var(--primary-low);
|
||||
box-shadow: var(--shadow-menu-panel);
|
||||
z-index: z("max");
|
||||
width: max-content;
|
||||
position: absolute;
|
||||
}
|
||||
top: 0;
|
||||
display: flex !important;
|
||||
padding: 0;
|
||||
|
||||
&[data-placement^="top"] {
|
||||
.arrow {
|
||||
bottom: -10px;
|
||||
rotate: 180deg;
|
||||
&.-animated {
|
||||
animation: d-tooltip-opening 0.15s ease-in;
|
||||
|
||||
&[data-placement^="bottom"] {
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
&[data-placement^="top"] {
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
&[data-placement^="right"] {
|
||||
transform-origin: center left;
|
||||
}
|
||||
|
||||
&[data-placement^="left"] {
|
||||
transform-origin: center right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-placement^="top-start"] {
|
||||
.arrow {
|
||||
margin-left: 10px;
|
||||
z-index: z("max");
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-placement^="bottom"] {
|
||||
.arrow {
|
||||
top: -10px;
|
||||
&[data-placement^="top"] {
|
||||
.arrow {
|
||||
bottom: -10px;
|
||||
rotate: 180deg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-placement^="right"] {
|
||||
.arrow {
|
||||
rotate: -90deg;
|
||||
left: -10px;
|
||||
&[data-placement^="top-start"] {
|
||||
.arrow {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-placement^="left"] {
|
||||
.arrow {
|
||||
rotate: 90deg;
|
||||
right: -10px;
|
||||
&[data-placement^="bottom"] {
|
||||
.arrow {
|
||||
top: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-placement^="right"] {
|
||||
.arrow {
|
||||
rotate: -90deg;
|
||||
left: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-placement^="left"] {
|
||||
.arrow {
|
||||
rotate: 90deg;
|
||||
right: -10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,8 +61,6 @@ export default class ChatMessageReaction extends Component {
|
|||
this.args.reaction.emoji,
|
||||
this.args.reaction.reacted ? "remove" : "add"
|
||||
);
|
||||
|
||||
this.tooltip.close();
|
||||
}
|
||||
|
||||
@cached
|
||||
|
|
|
@ -158,9 +158,7 @@ module(
|
|||
});
|
||||
|
||||
test("it shows status tooltip", async function (assert) {
|
||||
await render(
|
||||
hbs`<ChatChannel @channel={{this.channel}} /><DInlineTooltip />`
|
||||
);
|
||||
await render(hbs`<ChatChannel @channel={{this.channel}} /><DTooltips />`);
|
||||
await triggerEvent(statusSelector(mentionedUser.username), "mousemove");
|
||||
|
||||
assert.equal(
|
||||
|
|
|
@ -361,7 +361,7 @@ export default {
|
|||
},
|
||||
]);
|
||||
|
||||
return tooltip.close();
|
||||
return tooltip.close("local-date");
|
||||
}
|
||||
|
||||
if (!event?.target?.classList?.contains("discourse-local-date")) {
|
||||
|
@ -370,6 +370,7 @@ export default {
|
|||
|
||||
const siteSettings = this.container.lookup("service:site-settings");
|
||||
return tooltip.show(event.target, {
|
||||
identifier: "local-date",
|
||||
content: htmlSafe(buildHtmlPreview(event.target, siteSettings)),
|
||||
});
|
||||
},
|
||||
|
|
|
@ -14,7 +14,7 @@ describe "Homepage", type: :system do
|
|||
sign_in user
|
||||
visit "/"
|
||||
|
||||
expect(page).to have_no_css(".fk-d-tooltip .user-tip__title")
|
||||
expect(page).to have_no_css(".fk-d-tooltip__content .user-tip__title")
|
||||
end
|
||||
|
||||
it "does not show the boostrapping tip to an admin user" do
|
||||
|
@ -22,7 +22,7 @@ describe "Homepage", type: :system do
|
|||
sign_in admin
|
||||
visit "/"
|
||||
|
||||
expect(page).to have_no_css(".fk-d-tooltip .user-tip__title")
|
||||
expect(page).to have_no_css(".fk-d-tooltip__content .user-tip__title")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -35,20 +35,23 @@ describe "Homepage", type: :system do
|
|||
|
||||
visit "/"
|
||||
|
||||
expect(page).to have_css(".fk-d-tooltip .user-tip__title", text: "Your first notification!")
|
||||
expect(page).to have_css(
|
||||
".fk-d-tooltip__content .user-tip__title",
|
||||
text: "Your first notification!",
|
||||
)
|
||||
|
||||
find(".d-header #current-user").click
|
||||
find(".d-header").click
|
||||
|
||||
# Clicking outside element dismisses the tip
|
||||
expect(page).to have_no_css(
|
||||
".fk-d-tooltip .user-tip__title",
|
||||
".fk-d-tooltip__content .user-tip__title",
|
||||
text: "Your first notification!",
|
||||
)
|
||||
|
||||
page.refresh
|
||||
|
||||
expect(page).to have_no_css(
|
||||
".fk-d-tooltip .user-tip__title",
|
||||
".fk-d-tooltip__content .user-tip__title",
|
||||
text: "Your first notification!",
|
||||
)
|
||||
end
|
||||
|
@ -57,25 +60,25 @@ describe "Homepage", type: :system do
|
|||
sign_in user
|
||||
visit "/"
|
||||
|
||||
find(".fk-d-tooltip .user-tip__buttons .btn-primary").click
|
||||
expect(page).to have_no_css(".fk-d-tooltip .user-tip__title")
|
||||
find(".fk-d-tooltip__content .user-tip__buttons .btn-primary").click
|
||||
expect(page).to have_no_css(".fk-d-tooltip__content .user-tip__title")
|
||||
|
||||
discovery.topic_list.visit_topic(topics[0])
|
||||
expect(page).to have_css(".fk-d-tooltip .user-tip__title", text: "Topic timeline")
|
||||
expect(page).to have_css(".fk-d-tooltip__content .user-tip__title", text: "Topic timeline")
|
||||
|
||||
find(".fk-d-tooltip .user-tip__buttons .btn-primary").click
|
||||
expect(page).to have_css(".fk-d-tooltip .user-tip__title", text: "Keep reading!")
|
||||
find(".fk-d-tooltip__content .user-tip__buttons .btn-primary").click
|
||||
expect(page).to have_css(".fk-d-tooltip__content .user-tip__title", text: "Keep reading!")
|
||||
end
|
||||
|
||||
it "can skip all tips" do
|
||||
sign_in user
|
||||
visit "/"
|
||||
|
||||
find(".fk-d-tooltip .user-tip__buttons .btn", text: "Skip tips").click
|
||||
expect(page).to have_no_css(".fk-d-tooltip .user-tip__title")
|
||||
find(".fk-d-tooltip__content .user-tip__buttons .btn", text: "Skip tips").click
|
||||
expect(page).to have_no_css(".fk-d-tooltip__content .user-tip__title")
|
||||
|
||||
discovery.topic_list.visit_topic(topics[0])
|
||||
expect(page).to have_no_css(".fk-d-tooltip .user-tip__title")
|
||||
expect(page).to have_no_css(".fk-d-tooltip__content .user-tip__title")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue