diff --git a/tedi/components/overlay/tooltip/tooltip-trigger/tooltip-trigger.component.spec.ts b/tedi/components/overlay/tooltip/tooltip-trigger/tooltip-trigger.component.spec.ts index 9246801b6..e919adabb 100644 --- a/tedi/components/overlay/tooltip/tooltip-trigger/tooltip-trigger.component.spec.ts +++ b/tedi/components/overlay/tooltip/tooltip-trigger/tooltip-trigger.component.spec.ts @@ -91,6 +91,54 @@ describe("TooltipTriggerComponent", () => { hostEl.dispatchEvent(new Event("mouseenter")); expect(tooltip.showTooltip).not.toHaveBeenCalled(); }); + + it("should not call showTooltip after recent touchstart", () => { + tooltip.openWith = jest.fn(() => "both"); + + hostEl.dispatchEvent(new Event("touchstart")); + hostEl.dispatchEvent(new Event("mouseenter")); + + expect(tooltip.showTooltip).not.toHaveBeenCalled(); + }); + }); + + describe("touch interaction", () => { + it("should toggle tooltip on touchend regardless of openWith", () => { + tooltip.openWith = jest.fn(() => "hover"); + + hostEl.dispatchEvent(new Event("touchstart")); + hostEl.dispatchEvent(new Event("touchend")); + + expect(tooltip.toggleTooltip).toHaveBeenCalledTimes(1); + }); + + it("should not double-toggle on touch (click is ignored)", () => { + tooltip.openWith = jest.fn(() => "both"); + + hostEl.dispatchEvent(new Event("touchstart")); + hostEl.dispatchEvent(new Event("mouseenter")); + hostEl.dispatchEvent(new Event("focusin")); + hostEl.dispatchEvent(new Event("touchend")); + hostEl.click(); + + expect(tooltip.showTooltip).not.toHaveBeenCalled(); + expect(tooltip.toggleTooltip).toHaveBeenCalledTimes(1); + }); + + it("should reset isTouch flag after touchend timeout", () => { + jest.useFakeTimers(); + tooltip.openWith = jest.fn(() => "both"); + + hostEl.dispatchEvent(new Event("touchstart")); + hostEl.dispatchEvent(new Event("touchend")); + + jest.advanceTimersByTime(300); + + hostEl.dispatchEvent(new Event("mouseenter")); + expect(tooltip.showTooltip).toHaveBeenCalled(); + + jest.useRealTimers(); + }); }); describe("mouseleave", () => { @@ -127,6 +175,16 @@ describe("TooltipTriggerComponent", () => { jest.advanceTimersByTime(100); expect(tooltip.hideTooltip).not.toHaveBeenCalled(); }); + + it("should not call hideTooltip after recent touchstart", () => { + tooltip.openWith = jest.fn(() => "both"); + + hostEl.dispatchEvent(new Event("touchstart")); + hostEl.dispatchEvent(new Event("mouseleave")); + + jest.advanceTimersByTime(100); + expect(tooltip.hideTooltip).not.toHaveBeenCalled(); + }); }); describe("focusin", () => { diff --git a/tedi/components/overlay/tooltip/tooltip-trigger/tooltip-trigger.component.ts b/tedi/components/overlay/tooltip/tooltip-trigger/tooltip-trigger.component.ts index 0ca55f61e..07beaa4cb 100644 --- a/tedi/components/overlay/tooltip/tooltip-trigger/tooltip-trigger.component.ts +++ b/tedi/components/overlay/tooltip/tooltip-trigger/tooltip-trigger.component.ts @@ -26,6 +26,8 @@ export class TooltipTriggerComponent implements AfterContentChecked { readonly tooltip = inject(TooltipComponent); private interactiveElement = signal(null); + private isTouch = false; + constructor() { effect(() => { const element = this.interactiveElement(); @@ -35,8 +37,21 @@ export class TooltipTriggerComponent implements AfterContentChecked { }); } + @HostListener("touchstart") + onTouchStart() { + this.isTouch = true; + } + + @HostListener("touchend") + onTouchEnd() { + this.tooltip.toggleTooltip(); + setTimeout(() => (this.isTouch = false), 300); + } + @HostListener("click") onClick() { + if (this.isTouch) return; + if ( this.tooltip.openWith() === "both" || this.tooltip.openWith() === "click" @@ -47,6 +62,8 @@ export class TooltipTriggerComponent implements AfterContentChecked { @HostListener("mouseenter") onMouseEnter() { + if (this.isTouch) return; + if ( this.tooltip.openWith() === "both" || this.tooltip.openWith() === "hover" @@ -57,6 +74,8 @@ export class TooltipTriggerComponent implements AfterContentChecked { @HostListener("mouseleave") onMouseLeave() { + if (this.isTouch) return; + if ( this.tooltip.openWith() === "both" || this.tooltip.openWith() === "hover" @@ -71,6 +90,8 @@ export class TooltipTriggerComponent implements AfterContentChecked { @HostListener("focusin") onFocusIn() { + if (this.isTouch) return; + if ( this.tooltip.openWith() === "both" || this.tooltip.openWith() === "hover" @@ -81,9 +102,8 @@ export class TooltipTriggerComponent implements AfterContentChecked { @HostListener("focusout") onFocusOut() { - if (this.tooltip.isContentHovered()) { - return; - } + if (this.isTouch) return; + if (this.tooltip.isContentHovered()) return; if ( this.tooltip.openWith() === "both" || diff --git a/tedi/components/overlay/tooltip/tooltip.component.scss b/tedi/components/overlay/tooltip/tooltip.component.scss index fec2e1add..4d138d827 100644 --- a/tedi/components/overlay/tooltip/tooltip.component.scss +++ b/tedi/components/overlay/tooltip/tooltip.component.scss @@ -12,6 +12,7 @@ tedi-tooltip { float-ui-content { .float-ui-container-tooltip { z-index: var(--z-index-tooltip); + width: max-content; padding: 0; border: 0; border-radius: var(--popover-radius-rounded);