From 0c0469ead42d4e5e86389298d28c5d7207a69538 Mon Sep 17 00:00:00 2001 From: Romet Pastak Date: Tue, 4 Nov 2025 10:03:19 +0200 Subject: [PATCH] fix(tooltip): add openWith input, fix tooltip opening #182 --- .../tooltip-trigger.component.spec.ts | 130 +++++++++++++++--- .../tooltip-trigger.component.ts | 41 ++++-- .../overlay/tooltip/tooltip.component.ts | 7 + .../overlay/tooltip/tooltip.stories.ts | 19 ++- 4 files changed, 172 insertions(+), 25 deletions(-) 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 dc89a91a9..8199f3ef0 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 @@ -12,6 +12,7 @@ class MockTooltipComponent { showTooltip = jest.fn(); hideTooltip = jest.fn(); toggleTooltip = jest.fn(); + openWith = jest.fn(() => "both" as "hover" | "click" | "both"); } @Component({ @@ -52,30 +53,127 @@ describe("TooltipTriggerComponent", () => { }); describe("Event listeners", () => { - it("should call toggleTooltip on click", () => { - hostEl.click(); - expect(tooltip.toggleTooltip).toHaveBeenCalled(); + describe("click", () => { + it("should call toggleTooltip when openWith is 'click'", () => { + tooltip.openWith = jest.fn(() => "click"); + hostEl.click(); + expect(tooltip.toggleTooltip).toHaveBeenCalled(); + }); + + it("should call toggleTooltip when openWith is 'both'", () => { + tooltip.openWith = jest.fn(() => "both"); + hostEl.click(); + expect(tooltip.toggleTooltip).toHaveBeenCalled(); + }); + + it("should not call toggleTooltip when openWith is 'hover'", () => { + tooltip.openWith = jest.fn(() => "hover"); + hostEl.click(); + expect(tooltip.toggleTooltip).not.toHaveBeenCalled(); + }); }); - it("should call showTooltip on mouseenter", () => { - hostEl.dispatchEvent(new Event("mouseenter")); - expect(tooltip.showTooltip).toHaveBeenCalled(); + describe("mouseenter", () => { + it("should call showTooltip when openWith is 'hover'", () => { + tooltip.openWith = jest.fn(() => "hover"); + hostEl.dispatchEvent(new Event("mouseenter")); + expect(tooltip.showTooltip).toHaveBeenCalled(); + }); + + it("should call showTooltip when openWith is 'both'", () => { + tooltip.openWith = jest.fn(() => "both"); + hostEl.dispatchEvent(new Event("mouseenter")); + expect(tooltip.showTooltip).toHaveBeenCalled(); + }); + + it("should not call showTooltip when openWith is 'click'", () => { + tooltip.openWith = jest.fn(() => "click"); + hostEl.dispatchEvent(new Event("mouseenter")); + expect(tooltip.showTooltip).not.toHaveBeenCalled(); + }); }); - it("should call showTooltip on focusin", () => { - hostEl.dispatchEvent(new Event("focusin")); - expect(tooltip.showTooltip).toHaveBeenCalled(); + describe("mouseleave", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("should call hideTooltip after delay when openWith is 'hover'", () => { + tooltip.openWith = jest.fn(() => "hover"); + hostEl.dispatchEvent(new Event("mouseleave")); + + expect(tooltip.hideTooltip).not.toHaveBeenCalled(); + jest.advanceTimersByTime(100); + expect(tooltip.hideTooltip).toHaveBeenCalled(); + }); + + it("should call hideTooltip after delay when openWith is 'both'", () => { + tooltip.openWith = jest.fn(() => "both"); + hostEl.dispatchEvent(new Event("mouseleave")); + + expect(tooltip.hideTooltip).not.toHaveBeenCalled(); + jest.advanceTimersByTime(100); + expect(tooltip.hideTooltip).toHaveBeenCalled(); + }); + + it("should not call hideTooltip when openWith is 'click'", () => { + tooltip.openWith = jest.fn(() => "click"); + hostEl.dispatchEvent(new Event("mouseleave")); + + jest.advanceTimersByTime(100); + expect(tooltip.hideTooltip).not.toHaveBeenCalled(); + }); }); - it("should call hideTooltip on focusout when not hovering content", () => { - hostEl.dispatchEvent(new Event("focusout")); - expect(tooltip.hideTooltip).toHaveBeenCalled(); + describe("focusin", () => { + it("should call showTooltip when openWith is 'hover'", () => { + tooltip.openWith = jest.fn(() => "hover"); + hostEl.dispatchEvent(new Event("focusin")); + expect(tooltip.showTooltip).toHaveBeenCalled(); + }); + + it("should call showTooltip when openWith is 'both'", () => { + tooltip.openWith = jest.fn(() => "both"); + hostEl.dispatchEvent(new Event("focusin")); + expect(tooltip.showTooltip).toHaveBeenCalled(); + }); + + it("should not call showTooltip when openWith is 'click'", () => { + tooltip.openWith = jest.fn(() => "click"); + hostEl.dispatchEvent(new Event("focusin")); + expect(tooltip.showTooltip).not.toHaveBeenCalled(); + }); }); - it("should not hideTooltip on focusout when content is hovered", () => { - tooltip.isContentHovered = jest.fn(() => true); - hostEl.dispatchEvent(new Event("focusout")); - expect(tooltip.hideTooltip).not.toHaveBeenCalled(); + describe("focusout", () => { + it("should call hideTooltip on focusout when not hovering content and openWith is 'hover'", () => { + tooltip.openWith = jest.fn(() => "hover"); + hostEl.dispatchEvent(new Event("focusout")); + expect(tooltip.hideTooltip).toHaveBeenCalled(); + }); + + it("should call hideTooltip on focusout when not hovering content and openWith is 'both'", () => { + tooltip.openWith = jest.fn(() => "both"); + hostEl.dispatchEvent(new Event("focusout")); + expect(tooltip.hideTooltip).toHaveBeenCalled(); + }); + + it("should not call hideTooltip on focusout when openWith is 'click'", () => { + tooltip.openWith = jest.fn(() => "click"); + hostEl.dispatchEvent(new Event("focusout")); + expect(tooltip.hideTooltip).not.toHaveBeenCalled(); + }); + + it("should not hideTooltip on focusout when content is hovered", () => { + tooltip.openWith = jest.fn(() => "hover"); + tooltip.isContentHovered = jest.fn(() => true); + hostEl.dispatchEvent(new Event("focusout")); + expect(tooltip.hideTooltip).not.toHaveBeenCalled(); + }); }); }); 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 2020b080e..86c946442 100644 --- a/tedi/components/overlay/tooltip/tooltip-trigger/tooltip-trigger.component.ts +++ b/tedi/components/overlay/tooltip/tooltip-trigger/tooltip-trigger.component.ts @@ -29,26 +29,46 @@ export class TooltipTriggerComponent implements AfterContentChecked { @HostListener("click") onClick() { - this.tooltip.toggleTooltip(); + if ( + this.tooltip.openWith() === "both" || + this.tooltip.openWith() === "click" + ) { + this.tooltip.toggleTooltip(); + } } @HostListener("mouseenter") onMouseEnter() { - this.tooltip.showTooltip(); + if ( + this.tooltip.openWith() === "both" || + this.tooltip.openWith() === "hover" + ) { + this.tooltip.showTooltip(); + } } @HostListener("mouseleave") onMouseLeave() { - clearTimeout(this.tooltip.hideTimeout); + if ( + this.tooltip.openWith() === "both" || + this.tooltip.openWith() === "hover" + ) { + clearTimeout(this.tooltip.hideTimeout); - this.tooltip.hideTimeout = setTimeout(() => { - this.tooltip.hideTooltip(); - }, this.tooltip.timeoutDelay()); + this.tooltip.hideTimeout = setTimeout(() => { + this.tooltip.hideTooltip(); + }, this.tooltip.timeoutDelay()); + } } @HostListener("focusin") onFocusIn() { - this.tooltip.showTooltip(); + if ( + this.tooltip.openWith() === "both" || + this.tooltip.openWith() === "hover" + ) { + this.tooltip.showTooltip(); + } } @HostListener("focusout") @@ -57,7 +77,12 @@ export class TooltipTriggerComponent implements AfterContentChecked { return; } - this.tooltip.hideTooltip(); + if ( + this.tooltip.openWith() === "both" || + this.tooltip.openWith() === "hover" + ) { + this.tooltip.hideTooltip(); + } } ngAfterContentChecked(): void { diff --git a/tedi/components/overlay/tooltip/tooltip.component.ts b/tedi/components/overlay/tooltip/tooltip.component.ts index 7906fb754..32f761970 100644 --- a/tedi/components/overlay/tooltip/tooltip.component.ts +++ b/tedi/components/overlay/tooltip/tooltip.component.ts @@ -16,6 +16,7 @@ import { import { TooltipTriggerComponent } from "./tooltip-trigger/tooltip-trigger.component"; export type TooltipPosition = `${NgxFloatUiPlacements}`; +export type TooltipOpenWith = "hover" | "click" | "both"; @Component({ standalone: true, @@ -39,6 +40,12 @@ export class TooltipComponent implements AfterContentChecked { */ readonly preventOverflow = input(true); + /** + * How tooltip can opened? + * @default both + */ + readonly openWith = input("both"); + /** * Append floating element to given selector. * Use 'body' to append at the end of DOM or empty string to append next to trigger element. diff --git a/tedi/components/overlay/tooltip/tooltip.stories.ts b/tedi/components/overlay/tooltip/tooltip.stories.ts index 3037a4f71..6359754ce 100644 --- a/tedi/components/overlay/tooltip/tooltip.stories.ts +++ b/tedi/components/overlay/tooltip/tooltip.stories.ts @@ -30,6 +30,7 @@ const POSITIONS: TooltipPosition[] = [ "left-start", "left-end", ]; +const OPEN_WITH = ["hover", "click", "both"]; /** * Figma ↗
@@ -71,6 +72,21 @@ export default { }, }, }, + openWith: { + control: "radio", + description: "How tooltip can opened?", + options: OPEN_WITH, + table: { + category: "tooltip", + type: { + summary: "TooltipOpenWith", + detail: "hover \nclick \nboth", + }, + defaultValue: { + summary: "both", + }, + }, + }, preventOverflow: { control: "boolean", description: @@ -144,11 +160,12 @@ export const Default: Story = { appendTo: "body", timeoutDelay: 100, maxWidth: "medium", + openWith: "both", }, render: (args) => ({ props: args, template: ` - +