From 961fcf67da45bce1425ca0a113eb9f1a7df5ae29 Mon Sep 17 00:00:00 2001 From: guerler Date: Fri, 16 May 2025 00:52:20 -0400 Subject: [PATCH 1/7] Switch from Bootstrap Popover wrapper to Popper, add interaction delay to allow navigation from reference to popper --- .../src/components/Common/LoginRequired.vue | 24 ++++++++++++++----- client/src/components/Popper/Popper.vue | 3 +++ client/src/components/Popper/usePopper.ts | 17 ++++++++++--- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/client/src/components/Common/LoginRequired.vue b/client/src/components/Common/LoginRequired.vue index c96c9b091491..5e9d2ef824c8 100644 --- a/client/src/components/Common/LoginRequired.vue +++ b/client/src/components/Common/LoginRequired.vue @@ -1,20 +1,32 @@ diff --git a/client/src/components/Popper/Popper.vue b/client/src/components/Popper/Popper.vue index d75b4488507d..64a5476ced3a 100644 --- a/client/src/components/Popper/Popper.vue +++ b/client/src/components/Popper/Popper.vue @@ -31,6 +31,7 @@ library.add(faTimesCircle); const props = defineProps({ arrow: { type: Boolean, default: true }, disabled: { type: Boolean, default: false }, + interactive: { type: Boolean, default: false }, mode: { type: String, default: "dark" }, placement: String as PropType, referenceEl: HTMLElement, @@ -43,6 +44,7 @@ const reference = props.referenceEl ? ref(props.referenceEl) : ref(); const popper = ref(); const { visible } = usePopper(reference, popper, { + interactive: props.interactive, placement: props.placement, trigger: props.trigger, }); @@ -64,6 +66,7 @@ defineExpose({ .popper-element { z-index: 9999; border-radius: $border-radius-large; + pointer-events: auto; } /** Available variants */ diff --git a/client/src/components/Popper/usePopper.ts b/client/src/components/Popper/usePopper.ts index b32502f17a7a..5615e8e43dca 100644 --- a/client/src/components/Popper/usePopper.ts +++ b/client/src/components/Popper/usePopper.ts @@ -5,17 +5,28 @@ export type Trigger = "click" | "hover" | "none"; const defaultTrigger: Trigger = "hover"; +const DELAY_CLOSE = 50; + export function usePopper( reference: Ref, popper: Ref, - options: { placement?: Placement; trigger?: Trigger } + options: { interactive?: boolean; placement?: Placement; trigger?: Trigger } ) { const instance = ref>(); const visible = ref(false); const listeners: Array<{ target: EventTarget; event: string; handler: EventListener }> = []; - const doOpen = () => (visible.value = true); - const doClose = () => (visible.value = false); + let closeHandler: ReturnType | null = null; + + const doOpen = () => { + closeHandler && clearTimeout(closeHandler); + visible.value = true; + }; + const doClose = () => { + const delay = options.interactive ? DELAY_CLOSE : 0; + closeHandler && clearTimeout(closeHandler); + closeHandler = setTimeout(() => (visible.value = false), delay); + }; const doCloseDocument = (e: Event) => { if (!reference.value?.contains(e.target as Node) && !popper.value?.contains(e.target as Node)) { visible.value = false; From fb720e6921a0b27446829fd226b81fc20f86bb6c Mon Sep 17 00:00:00 2001 From: guerler Date: Fri, 16 May 2025 01:11:17 -0400 Subject: [PATCH 2/7] Allow undefined for close handler --- client/src/components/Popper/usePopper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Popper/usePopper.ts b/client/src/components/Popper/usePopper.ts index 5615e8e43dca..0637b7de286e 100644 --- a/client/src/components/Popper/usePopper.ts +++ b/client/src/components/Popper/usePopper.ts @@ -16,7 +16,7 @@ export function usePopper( const visible = ref(false); const listeners: Array<{ target: EventTarget; event: string; handler: EventListener }> = []; - let closeHandler: ReturnType | null = null; + let closeHandler: ReturnType | undefined; const doOpen = () => { closeHandler && clearTimeout(closeHandler); From e75831f90d9b3b36e322f3780dabb894fa0da0e6 Mon Sep 17 00:00:00 2001 From: guerler Date: Fri, 16 May 2025 01:25:28 -0400 Subject: [PATCH 3/7] Use routerlink instead of anchor, avoid page reload --- client/src/components/Common/LoginRequired.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/components/Common/LoginRequired.vue b/client/src/components/Common/LoginRequired.vue index 5e9d2ef824c8..1ff012317af4 100644 --- a/client/src/components/Common/LoginRequired.vue +++ b/client/src/components/Common/LoginRequired.vue @@ -2,7 +2,6 @@ import { onMounted, ref } from "vue"; import { useUserStore } from "@/stores/userStore"; -import { withPrefix } from "@/utils/redirect"; import Popper from "@/components/Popper/Popper.vue"; @@ -27,6 +26,8 @@ onMounted(() => { :interactive="true" :reference-el="referenceEl">
{{ title }}
-
Please log in or register to use this feature.
+
+ Please log in or register to use this feature. +
From fe85c5200f586d47a177655a6610e79102ea3e8a Mon Sep 17 00:00:00 2001 From: guerler Date: Fri, 16 May 2025 10:09:20 -0400 Subject: [PATCH 4/7] Adjust jest test --- client/src/components/Popper/Popper.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/Popper/Popper.test.js b/client/src/components/Popper/Popper.test.js index f9115cde2139..d74111781a0c 100644 --- a/client/src/components/Popper/Popper.test.js +++ b/client/src/components/Popper/Popper.test.js @@ -103,6 +103,7 @@ describe("PopperComponent.vue", () => { await reference.trigger("mouseover"); expect(popperElement.isVisible()).toBe(true); await reference.trigger("mouseout"); + await new Promise(r => setTimeout(r, 100)); expect(popperElement.isVisible()).toBe(false); }); From efb2cfd0626cd8dbaa8ddc04836d4a02191bbd8e Mon Sep 17 00:00:00 2001 From: guerler Date: Fri, 16 May 2025 10:15:28 -0400 Subject: [PATCH 5/7] Add test for new popper prop --- client/src/components/Popper/Popper.test.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/client/src/components/Popper/Popper.test.js b/client/src/components/Popper/Popper.test.js index d74111781a0c..7290cc15f3a4 100644 --- a/client/src/components/Popper/Popper.test.js +++ b/client/src/components/Popper/Popper.test.js @@ -3,6 +3,8 @@ import { mount } from "@vue/test-utils"; import PopperComponent from "./Popper.vue"; +const DELAY = 100; + jest.mock("@popperjs/core", () => ({ createPopper: jest.fn(() => ({ destroy: jest.fn(), @@ -95,7 +97,7 @@ describe("PopperComponent.vue", () => { expect(wrapper.find(".popper-element").isVisible()).toBe(false); }); - test("shows and hides popper on hover trigger", async () => { + test("shows and hides popper on hover trigger over reference", async () => { const wrapper = mountTarget("hover"); const reference = wrapper.find("button"); const popperElement = wrapper.find(".popper-element"); @@ -103,7 +105,22 @@ describe("PopperComponent.vue", () => { await reference.trigger("mouseover"); expect(popperElement.isVisible()).toBe(true); await reference.trigger("mouseout"); - await new Promise(r => setTimeout(r, 100)); + await new Promise((r) => setTimeout(r, DELAY)); + expect(popperElement.isVisible()).toBe(false); + }); + + test("popper remains visible when hovering over popper", async () => { + const wrapper = mountTarget("hover"); + const reference = wrapper.find("button"); + const popperElement = wrapper.find(".popper-element"); + expect(popperElement.isVisible()).toBe(false); + await reference.trigger("mouseover"); + expect(popperElement.isVisible()).toBe(true); + await reference.trigger("mouseout"); + await popperElement.trigger("mouseover"); + await new Promise((r) => setTimeout(r, DELAY)); + await popperElement.trigger("mouseout"); + await new Promise((r) => setTimeout(r, DELAY)); expect(popperElement.isVisible()).toBe(false); }); From be9fe4ef34526c40a878f963bf6b2a6d2a3aae47 Mon Sep 17 00:00:00 2001 From: guerler Date: Fri, 16 May 2025 10:22:34 -0400 Subject: [PATCH 6/7] Extend test, apply interactive prop --- client/src/components/Popper/Popper.test.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/client/src/components/Popper/Popper.test.js b/client/src/components/Popper/Popper.test.js index 7290cc15f3a4..98b88952574d 100644 --- a/client/src/components/Popper/Popper.test.js +++ b/client/src/components/Popper/Popper.test.js @@ -3,7 +3,8 @@ import { mount } from "@vue/test-utils"; import PopperComponent from "./Popper.vue"; -const DELAY = 100; +// value from usePopper.ts +const DELAY_CLOSE = 50; jest.mock("@popperjs/core", () => ({ createPopper: jest.fn(() => ({ @@ -12,12 +13,13 @@ jest.mock("@popperjs/core", () => ({ })), })); -function mountTarget(trigger = "click") { +function mountTarget(trigger = "click", interactive = false) { return mount(PopperComponent, { propsData: { title: "Test Title", placement: "bottom", - trigger: trigger, + interactive, + trigger, }, slots: { reference: "", @@ -105,22 +107,24 @@ describe("PopperComponent.vue", () => { await reference.trigger("mouseover"); expect(popperElement.isVisible()).toBe(true); await reference.trigger("mouseout"); - await new Promise((r) => setTimeout(r, DELAY)); + await new Promise((r) => setTimeout(r, 0)); expect(popperElement.isVisible()).toBe(false); }); test("popper remains visible when hovering over popper", async () => { - const wrapper = mountTarget("hover"); + const wrapper = mountTarget("hover", true); const reference = wrapper.find("button"); const popperElement = wrapper.find(".popper-element"); expect(popperElement.isVisible()).toBe(false); await reference.trigger("mouseover"); expect(popperElement.isVisible()).toBe(true); await reference.trigger("mouseout"); + await new Promise((r) => setTimeout(r, DELAY_CLOSE / 2)); + expect(popperElement.isVisible()).toBe(true); await popperElement.trigger("mouseover"); - await new Promise((r) => setTimeout(r, DELAY)); + await new Promise((r) => setTimeout(r, DELAY_CLOSE * 2)); await popperElement.trigger("mouseout"); - await new Promise((r) => setTimeout(r, DELAY)); + await new Promise((r) => setTimeout(r, DELAY_CLOSE * 2)); expect(popperElement.isVisible()).toBe(false); }); From eace9f9a0052f601b9de316ff74bf1d16ae57ee9 Mon Sep 17 00:00:00 2001 From: guerler Date: Fri, 16 May 2025 10:36:40 -0400 Subject: [PATCH 7/7] Can directly close on click --- client/src/components/Popper/usePopper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Popper/usePopper.ts b/client/src/components/Popper/usePopper.ts index 0637b7de286e..08692912fffd 100644 --- a/client/src/components/Popper/usePopper.ts +++ b/client/src/components/Popper/usePopper.ts @@ -35,7 +35,7 @@ export function usePopper( const doCloseElement = (event: Event) => { const target = event.target as Element; if (target && target.closest(".popper-close")) { - doClose(); + visible.value = false; } }; const doCloseEscape = (event: Event) => {