Skip to content

Commit 3d1812e

Browse files
mart-sessmanm2rt
andauthored
fix(tooltip): correct event handling on touch devices #348 (#350)
Co-authored-by: m2rt <mart@bitweb.ee>
1 parent f566c21 commit 3d1812e

3 files changed

Lines changed: 82 additions & 3 deletions

File tree

tedi/components/overlay/tooltip/tooltip-trigger/tooltip-trigger.component.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,54 @@ describe("TooltipTriggerComponent", () => {
9191
hostEl.dispatchEvent(new Event("mouseenter"));
9292
expect(tooltip.showTooltip).not.toHaveBeenCalled();
9393
});
94+
95+
it("should not call showTooltip after recent touchstart", () => {
96+
tooltip.openWith = jest.fn(() => "both");
97+
98+
hostEl.dispatchEvent(new Event("touchstart"));
99+
hostEl.dispatchEvent(new Event("mouseenter"));
100+
101+
expect(tooltip.showTooltip).not.toHaveBeenCalled();
102+
});
103+
});
104+
105+
describe("touch interaction", () => {
106+
it("should toggle tooltip on touchend regardless of openWith", () => {
107+
tooltip.openWith = jest.fn(() => "hover");
108+
109+
hostEl.dispatchEvent(new Event("touchstart"));
110+
hostEl.dispatchEvent(new Event("touchend"));
111+
112+
expect(tooltip.toggleTooltip).toHaveBeenCalledTimes(1);
113+
});
114+
115+
it("should not double-toggle on touch (click is ignored)", () => {
116+
tooltip.openWith = jest.fn(() => "both");
117+
118+
hostEl.dispatchEvent(new Event("touchstart"));
119+
hostEl.dispatchEvent(new Event("mouseenter"));
120+
hostEl.dispatchEvent(new Event("focusin"));
121+
hostEl.dispatchEvent(new Event("touchend"));
122+
hostEl.click();
123+
124+
expect(tooltip.showTooltip).not.toHaveBeenCalled();
125+
expect(tooltip.toggleTooltip).toHaveBeenCalledTimes(1);
126+
});
127+
128+
it("should reset isTouch flag after touchend timeout", () => {
129+
jest.useFakeTimers();
130+
tooltip.openWith = jest.fn(() => "both");
131+
132+
hostEl.dispatchEvent(new Event("touchstart"));
133+
hostEl.dispatchEvent(new Event("touchend"));
134+
135+
jest.advanceTimersByTime(300);
136+
137+
hostEl.dispatchEvent(new Event("mouseenter"));
138+
expect(tooltip.showTooltip).toHaveBeenCalled();
139+
140+
jest.useRealTimers();
141+
});
94142
});
95143

96144
describe("mouseleave", () => {
@@ -127,6 +175,16 @@ describe("TooltipTriggerComponent", () => {
127175
jest.advanceTimersByTime(100);
128176
expect(tooltip.hideTooltip).not.toHaveBeenCalled();
129177
});
178+
179+
it("should not call hideTooltip after recent touchstart", () => {
180+
tooltip.openWith = jest.fn(() => "both");
181+
182+
hostEl.dispatchEvent(new Event("touchstart"));
183+
hostEl.dispatchEvent(new Event("mouseleave"));
184+
185+
jest.advanceTimersByTime(100);
186+
expect(tooltip.hideTooltip).not.toHaveBeenCalled();
187+
});
130188
});
131189

132190
describe("focusin", () => {

tedi/components/overlay/tooltip/tooltip-trigger/tooltip-trigger.component.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export class TooltipTriggerComponent implements AfterContentChecked {
2626
readonly tooltip = inject(TooltipComponent);
2727
private interactiveElement = signal<HTMLElement | null>(null);
2828

29+
private isTouch = false;
30+
2931
constructor() {
3032
effect(() => {
3133
const element = this.interactiveElement();
@@ -35,8 +37,21 @@ export class TooltipTriggerComponent implements AfterContentChecked {
3537
});
3638
}
3739

40+
@HostListener("touchstart")
41+
onTouchStart() {
42+
this.isTouch = true;
43+
}
44+
45+
@HostListener("touchend")
46+
onTouchEnd() {
47+
this.tooltip.toggleTooltip();
48+
setTimeout(() => (this.isTouch = false), 300);
49+
}
50+
3851
@HostListener("click")
3952
onClick() {
53+
if (this.isTouch) return;
54+
4055
if (
4156
this.tooltip.openWith() === "both" ||
4257
this.tooltip.openWith() === "click"
@@ -47,6 +62,8 @@ export class TooltipTriggerComponent implements AfterContentChecked {
4762

4863
@HostListener("mouseenter")
4964
onMouseEnter() {
65+
if (this.isTouch) return;
66+
5067
if (
5168
this.tooltip.openWith() === "both" ||
5269
this.tooltip.openWith() === "hover"
@@ -57,6 +74,8 @@ export class TooltipTriggerComponent implements AfterContentChecked {
5774

5875
@HostListener("mouseleave")
5976
onMouseLeave() {
77+
if (this.isTouch) return;
78+
6079
if (
6180
this.tooltip.openWith() === "both" ||
6281
this.tooltip.openWith() === "hover"
@@ -71,6 +90,8 @@ export class TooltipTriggerComponent implements AfterContentChecked {
7190

7291
@HostListener("focusin")
7392
onFocusIn() {
93+
if (this.isTouch) return;
94+
7495
if (
7596
this.tooltip.openWith() === "both" ||
7697
this.tooltip.openWith() === "hover"
@@ -81,9 +102,8 @@ export class TooltipTriggerComponent implements AfterContentChecked {
81102

82103
@HostListener("focusout")
83104
onFocusOut() {
84-
if (this.tooltip.isContentHovered()) {
85-
return;
86-
}
105+
if (this.isTouch) return;
106+
if (this.tooltip.isContentHovered()) return;
87107

88108
if (
89109
this.tooltip.openWith() === "both" ||

tedi/components/overlay/tooltip/tooltip.component.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ tedi-tooltip {
1212
float-ui-content {
1313
.float-ui-container-tooltip {
1414
z-index: var(--z-index-tooltip);
15+
width: max-content;
1516
padding: 0;
1617
border: 0;
1718
border-radius: var(--popover-radius-rounded);

0 commit comments

Comments
 (0)