+
+;
+
+export default DHeadlessMenu;
diff --git a/app/assets/javascripts/float-kit/addon/components/d-headless-tooltip.gjs b/app/assets/javascripts/float-kit/addon/components/d-headless-tooltip.gjs
new file mode 100644
index 00000000000..e3ecc2b8c65
--- /dev/null
+++ b/app/assets/javascripts/float-kit/addon/components/d-headless-tooltip.gjs
@@ -0,0 +1,15 @@
+import { and } from "truth-helpers";
+import DInlineFloat from "float-kit/components/d-inline-float";
+
+const DHeadlessTooltip =
+
+;
+
+export default DHeadlessTooltip;
diff --git a/app/assets/javascripts/float-kit/addon/components/d-inline-float.gjs b/app/assets/javascripts/float-kit/addon/components/d-inline-float.gjs
index 74d8b76d932..032353a56bc 100644
--- a/app/assets/javascripts/float-kit/addon/components/d-inline-float.gjs
+++ b/app/assets/javascripts/float-kit/addon/components/d-inline-float.gjs
@@ -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}}
diff --git a/app/assets/javascripts/float-kit/addon/components/d-inline-menu.gjs b/app/assets/javascripts/float-kit/addon/components/d-inline-menu.gjs
deleted file mode 100644
index fba7bf704eb..00000000000
--- a/app/assets/javascripts/float-kit/addon/components/d-inline-menu.gjs
+++ /dev/null
@@ -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;
-
-
-
-
-
-
-}
diff --git a/app/assets/javascripts/float-kit/addon/components/d-inline-tooltip.gjs b/app/assets/javascripts/float-kit/addon/components/d-inline-tooltip.gjs
deleted file mode 100644
index 5b2ad497716..00000000000
--- a/app/assets/javascripts/float-kit/addon/components/d-inline-tooltip.gjs
+++ /dev/null
@@ -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;
-
-
-
-
-
-
-}
diff --git a/app/assets/javascripts/float-kit/addon/components/d-menu.gjs b/app/assets/javascripts/float-kit/addon/components/d-menu.gjs
index e4b157ecf53..81c86d82b28 100644
--- a/app/assets/javascripts/float-kit/addon/components/d-menu.gjs
+++ b/app/assets/javascripts/float-kit/addon/components/d-menu.gjs
@@ -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}}
diff --git a/app/assets/javascripts/float-kit/addon/components/d-menus.gjs b/app/assets/javascripts/float-kit/addon/components/d-menus.gjs
new file mode 100644
index 00000000000..ddc730277c8
--- /dev/null
+++ b/app/assets/javascripts/float-kit/addon/components/d-menus.gjs
@@ -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;
+
+
+
+
+ {{#each this.menu.registeredMenus key="id" as |menu|}}
+ {{#if menu.detachedTrigger}}
+
+ {{/if}}
+ {{/each}}
+
+}
diff --git a/app/assets/javascripts/float-kit/addon/components/d-tooltip.gjs b/app/assets/javascripts/float-kit/addon/components/d-tooltip.gjs
index 9b9090994bc..7bbbff7351f 100644
--- a/app/assets/javascripts/float-kit/addon/components/d-tooltip.gjs
+++ b/app/assets/javascripts/float-kit/addon/components/d-tooltip.gjs
@@ -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 {
{{#if (has-block)}}
{{yield this.componentArgs}}
diff --git a/app/assets/javascripts/float-kit/addon/components/d-tooltips.gjs b/app/assets/javascripts/float-kit/addon/components/d-tooltips.gjs
new file mode 100644
index 00000000000..8a199b87f16
--- /dev/null
+++ b/app/assets/javascripts/float-kit/addon/components/d-tooltips.gjs
@@ -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;
+
+
+
+
+ {{#each this.tooltip.registeredTooltips key="id" as |tooltip|}}
+ {{#if tooltip.detachedTrigger}}
+
+ {{/if}}
+ {{/each}}
+
+}
diff --git a/app/assets/javascripts/float-kit/addon/lib/d-menu-instance.js b/app/assets/javascripts/float-kit/addon/lib/d-menu-instance.js
index 4433fafd145..8a7ad299125 100644
--- a/app/assets/javascripts/float-kit/addon/lib/d-menu-instance.js
+++ b/app/assets/javascripts/float-kit/addon/lib/d-menu-instance.js
@@ -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 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();
}
}
diff --git a/app/assets/javascripts/float-kit/addon/lib/d-tooltip-instance.js b/app/assets/javascripts/float-kit/addon/lib/d-tooltip-instance.js
index f6dfbe0a9b0..43ab8ec57e1 100644
--- a/app/assets/javascripts/float-kit/addon/lib/d-tooltip-instance.js
+++ b/app/assets/javascripts/float-kit/addon/lib/d-tooltip-instance.js
@@ -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 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();
}
}
diff --git a/app/assets/javascripts/float-kit/addon/lib/float-kit-instance.js b/app/assets/javascripts/float-kit/addon/lib/float-kit-instance.js
index bebcbbeb174..a6cfe6bc215 100644
--- a/app/assets/javascripts/float-kit/addon/lib/float-kit-instance.js
+++ b/app/assets/javascripts/float-kit/addon/lib/float-kit-instance.js
@@ -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;
}
diff --git a/app/assets/javascripts/float-kit/addon/modifiers/close-on-click-outside.js b/app/assets/javascripts/float-kit/addon/modifiers/close-on-click-outside.js
deleted file mode 100644
index 219c9154b6f..00000000000
--- a/app/assets/javascripts/float-kit/addon/modifiers/close-on-click-outside.js
+++ /dev/null
@@ -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);
- }
-}
diff --git a/app/assets/javascripts/float-kit/addon/services/menu.js b/app/assets/javascripts/float-kit/addon/services/menu.js
index 182b4d447aa..f743d6883c5 100644
--- a/app/assets/javascripts/float-kit/addon/services/menu.js
+++ b/app/assets/javascripts/float-kit/addon/services/menu.js
@@ -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;
}
}
diff --git a/app/assets/javascripts/float-kit/addon/services/tooltip.js b/app/assets/javascripts/float-kit/addon/services/tooltip.js
index ba70cdd8fe7..c88ac3edd7d 100644
--- a/app/assets/javascripts/float-kit/addon/services/tooltip.js
+++ b/app/assets/javascripts/float-kit/addon/services/tooltip.js
@@ -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;
}
}
diff --git a/app/assets/javascripts/float-kit/app/components/d-headless-menu.js b/app/assets/javascripts/float-kit/app/components/d-headless-menu.js
new file mode 100644
index 00000000000..b56bfdcb16d
--- /dev/null
+++ b/app/assets/javascripts/float-kit/app/components/d-headless-menu.js
@@ -0,0 +1 @@
+export { default } from "float-kit/components/d-headless-menu";
diff --git a/app/assets/javascripts/float-kit/app/components/d-headless-tooltip.js b/app/assets/javascripts/float-kit/app/components/d-headless-tooltip.js
new file mode 100644
index 00000000000..39173d022d7
--- /dev/null
+++ b/app/assets/javascripts/float-kit/app/components/d-headless-tooltip.js
@@ -0,0 +1 @@
+export { default } from "float-kit/components/d-headless-tooltip";
diff --git a/app/assets/javascripts/float-kit/app/components/d-inline-menu.js b/app/assets/javascripts/float-kit/app/components/d-inline-menu.js
deleted file mode 100644
index 65c4b0de930..00000000000
--- a/app/assets/javascripts/float-kit/app/components/d-inline-menu.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "float-kit/components/d-inline-menu";
diff --git a/app/assets/javascripts/float-kit/app/components/d-inline-tooltip.js b/app/assets/javascripts/float-kit/app/components/d-inline-tooltip.js
deleted file mode 100644
index 173122c7b20..00000000000
--- a/app/assets/javascripts/float-kit/app/components/d-inline-tooltip.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "float-kit/components/d-inline-tooltip";
diff --git a/app/assets/javascripts/float-kit/app/components/d-menus.js b/app/assets/javascripts/float-kit/app/components/d-menus.js
new file mode 100644
index 00000000000..fd438bd86d2
--- /dev/null
+++ b/app/assets/javascripts/float-kit/app/components/d-menus.js
@@ -0,0 +1 @@
+export { default } from "float-kit/components/d-menus";
diff --git a/app/assets/javascripts/float-kit/app/components/d-tooltips.js b/app/assets/javascripts/float-kit/app/components/d-tooltips.js
new file mode 100644
index 00000000000..07aab3a9743
--- /dev/null
+++ b/app/assets/javascripts/float-kit/app/components/d-tooltips.js
@@ -0,0 +1 @@
+export { default } from "float-kit/components/d-tooltips";
diff --git a/app/assets/stylesheets/common/float-kit/d-tooltip.scss b/app/assets/stylesheets/common/float-kit/d-tooltip.scss
index 6d6418279dd..4c2d6146e98 100644
--- a/app/assets/stylesheets/common/float-kit/d-tooltip.scss
+++ b/app/assets/stylesheets/common/float-kit/d-tooltip.scss
@@ -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;
+ }
}
}
}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-reaction.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-message-reaction.gjs
index 4d75d2d3948..52cdfb1c5c7 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-message-reaction.gjs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-reaction.gjs
@@ -61,8 +61,6 @@ export default class ChatMessageReaction extends Component {
this.args.reaction.emoji,
this.args.reaction.reacted ? "remove" : "add"
);
-
- this.tooltip.close();
}
@cached
diff --git a/plugins/chat/test/javascripts/components/chat-channel-test.js b/plugins/chat/test/javascripts/components/chat-channel-test.js
index 69db4e461f9..8afcf5effa5 100644
--- a/plugins/chat/test/javascripts/components/chat-channel-test.js
+++ b/plugins/chat/test/javascripts/components/chat-channel-test.js
@@ -158,9 +158,7 @@ module(
});
test("it shows status tooltip", async function (assert) {
- await render(
- hbs``
- );
+ await render(hbs``);
await triggerEvent(statusSelector(mentionedUser.username), "mousemove");
assert.equal(
diff --git a/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js b/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js
index 1b1b45187a2..47af250c236 100644
--- a/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js
+++ b/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js
@@ -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)),
});
},
diff --git a/spec/system/user_tips_spec.rb b/spec/system/user_tips_spec.rb
index 7bb36f6ec33..db187c08c09 100644
--- a/spec/system/user_tips_spec.rb
+++ b/spec/system/user_tips_spec.rb
@@ -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