Skip to content

Commit d39be04

Browse files
committed
fix event dispatching and extend tests on it for focus by keyboard navigation
1 parent 3063efe commit d39be04

2 files changed

Lines changed: 54 additions & 16 deletions

File tree

src/components/Tooltip/Tooltip.test.tsx

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from "react";
22
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3+
import userEvent from "@testing-library/user-event";
34

45
import "@testing-library/jest-dom";
56

@@ -45,19 +46,53 @@ describe("Tooltip", () => {
4546
fireEvent.mouseEnter(container.getElementsByClassName(`${eccgui}-tooltip__wrapper--placeholder`)[0]);
4647
checkForPlaceholderClass(container, 1);
4748
await waitFor(() => {
48-
expect(screen.queryAllByText(TooltipStory.args.content)).toHaveLength(0);
4949
checkForPlaceholderClass(container, 0);
5050
});
51+
expect(screen.queryAllByText(TooltipStory.args.content as string)).toHaveLength(0);
5152
});
5253
it("should be displayed on two continues mouse hover when placeholder is used", async () => {
5354
const { container } = render(<Tooltip {...TooltipStory.args} usePlaceholder={true} />);
5455
fireEvent.mouseEnter(container.getElementsByClassName(`${eccgui}-tooltip__wrapper`)[0]);
5556
checkForPlaceholderClass(container, 1);
56-
await waitFor(async () => {
57-
expect(screen.queryAllByText(TooltipStory.args.content)).toHaveLength(0);
57+
await waitFor(() => {
58+
checkForPlaceholderClass(container, 0);
59+
});
60+
expect(screen.queryAllByText(TooltipStory.args.content as string)).toHaveLength(0);
61+
fireEvent.mouseEnter(container.getElementsByClassName(`${eccgui}-tooltip__wrapper`)[0]);
62+
expect(await screen.findByText(TooltipStory.args.content as string)).toBeVisible();
63+
});
64+
it("should be displayed on focus when no placeholder is used", async () => {
65+
// Blueprint ignores focus events with null relatedTarget (page-refocus guard), so we tab
66+
// from a preceding element to produce a non-null relatedTarget.
67+
render(
68+
<>
69+
<button>previous element</button>
70+
<Tooltip {...TooltipStory.args} usePlaceholder={false} />
71+
</>
72+
);
73+
const user = userEvent.setup();
74+
await user.tab(); // focuses "previous element"
75+
await user.tab(); // focuses tooltip target, relatedTarget is non-null → Blueprint opens
76+
expect(await screen.findByText(TooltipStory.args.content as string)).toBeVisible();
77+
});
78+
it("should be displayed after keyboard focus when placeholder is used", async () => {
79+
// Use a focusable button child so refocus() can call .focus() on it after the swap.
80+
// Tab from a preceding element so relatedTarget is non-null when Blueprint handles focus.
81+
const { container } = render(
82+
<>
83+
<button>previous element</button>
84+
<Tooltip {...TooltipStory.args} usePlaceholder={true}>
85+
<button>tooltip target</button>
86+
</Tooltip>
87+
</>
88+
);
89+
const user = userEvent.setup();
90+
await user.tab(); // focuses "previous element"
91+
await user.tab(); // focuses placeholder inner button, triggers focusin swap
92+
checkForPlaceholderClass(container, 1);
93+
await waitFor(() => {
5894
checkForPlaceholderClass(container, 0);
59-
fireEvent.mouseOver(container.getElementsByClassName(`${eccgui}-tooltip__wrapper`)[0]);
60-
expect(await screen.findByText(TooltipStory.args.content)).toBeVisible();
6195
});
96+
expect(await screen.findByText(TooltipStory.args.content as string)).toBeVisible();
6297
});
6398
});

src/components/Tooltip/Tooltip.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export interface TooltipProps extends Omit<BlueprintTooltipProps, "position"> {
5050
swapPlaceholderDelay?: number;
5151
}
5252

53-
export type TooltipSize = "small" | "medium" | "large"
53+
export type TooltipSize = "small" | "medium" | "large";
5454

5555
export const Tooltip = ({
5656
children,
@@ -100,16 +100,19 @@ export const Tooltip = ({
100100
}, swapDelayTime);
101101
if (placeholderRef.current !== null) {
102102
const eventType = ev.type === "focusin" ? "focusout" : "mouseleave";
103-
const innerFocusTarget = (placeholderRef.current as HTMLElement).querySelector("[tabindex='0']")?.children[0];
104-
(innerFocusTarget as HTMLElement).addEventListener(eventType, () => {
105-
if (
106-
(eventType === "focusout" && eventMemory.current === "afterfocus") ||
107-
(eventType === "mouseleave" && eventMemory.current === "afterhover")
108-
) {
109-
eventMemory.current = null;
110-
}
111-
clearTimeout(swapDelay.current as NodeJS.Timeout);
112-
});
103+
const innerFocusTarget = (placeholderRef.current as HTMLElement).querySelector("[tabindex='0']")
104+
?.children[0];
105+
if (innerFocusTarget) {
106+
(innerFocusTarget as HTMLElement).addEventListener(eventType, () => {
107+
if (
108+
(eventType === "focusout" && eventMemory.current === "afterfocus") ||
109+
(eventType === "mouseleave" && eventMemory.current === "afterhover")
110+
) {
111+
eventMemory.current = null;
112+
}
113+
clearTimeout(swapDelay.current as NodeJS.Timeout);
114+
});
115+
}
113116
}
114117
};
115118
(placeholderRef.current as HTMLElement).addEventListener("mouseenter", swap);

0 commit comments

Comments
 (0)