Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 46fdc26

Browse files
authored
fix(canvas): fix the Activity bell's hover card resurfacing, and wedging, after it navigates (#4017)
1 parent 56d86a5 commit 46fdc26

2 files changed

Lines changed: 92 additions & 40 deletions

File tree

packages/ui/src/features/canvas/components/ChannelNav.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,48 @@ describe("ChannelNav", () => {
7474

7575
const activity = screen.getByLabelText("Activity");
7676
expect(activity).toBeEnabled();
77+
expect(activity).not.toHaveAttribute("aria-haspopup");
7778
await user.hover(activity);
7879

7980
await new Promise((resolve) => setTimeout(resolve, 400));
8081
expect(screen.queryByText("Recent activity card")).not.toBeInTheDocument();
8182
});
83+
84+
it("leaves no popover state on the bell after it navigates to Activity", async () => {
85+
const user = userEvent.setup();
86+
const { rerender } = render(<ChannelNav />);
87+
const bell = () => screen.getByLabelText("Activity");
88+
89+
await user.hover(bell());
90+
await screen.findByText("Recent activity card", {}, { timeout: 1_000 });
91+
await user.click(bell());
92+
mocks.view = { type: "activity" };
93+
rerender(<ChannelNav />);
94+
95+
expect(screen.queryByText("Recent activity card")).not.toBeInTheDocument();
96+
expect(bell()).not.toHaveAttribute("data-popup-open");
97+
expect(bell()).not.toHaveAttribute("data-pressed");
98+
});
99+
100+
it("neither resurfaces nor wedges the hover card once the bell has navigated", async () => {
101+
const user = userEvent.setup();
102+
const { rerender } = render(<ChannelNav />);
103+
const bell = () => screen.getByLabelText("Activity");
104+
105+
await user.hover(bell());
106+
await user.click(bell());
107+
mocks.view = { type: "activity" };
108+
rerender(<ChannelNav />);
109+
await user.unhover(bell());
110+
111+
mocks.view = { type: "task-detail" };
112+
rerender(<ChannelNav />);
113+
await new Promise((resolve) => setTimeout(resolve, 400));
114+
expect(screen.queryByText("Recent activity card")).not.toBeInTheDocument();
115+
116+
await user.hover(bell());
117+
expect(
118+
await screen.findByText("Recent activity card", {}, { timeout: 1_000 }),
119+
).toBeInTheDocument();
120+
});
82121
});

packages/ui/src/features/canvas/components/ChannelNav.tsx

Lines changed: 53 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ import {
3939
} from "@posthog/ui/router/navigationBridge";
4040
import { useAppView } from "@posthog/ui/router/useAppView";
4141
import { track } from "@posthog/ui/shell/analytics";
42-
import { type ComponentPropsWithRef, type ReactNode, useState } from "react";
42+
import {
43+
type ComponentPropsWithRef,
44+
type ReactElement,
45+
type ReactNode,
46+
useState,
47+
} from "react";
4348
import { ActivityHoverCard } from "./ActivityHoverCard";
4449

4550
const INBOX_REFETCH_INTERVAL_MS = 60_000;
@@ -133,10 +138,51 @@ function NavButton({
133138
);
134139
}
135140

141+
function ActivityHoverPopover({ trigger }: { trigger: ReactElement }) {
142+
const [open, setOpen] = useState(false);
143+
144+
return (
145+
<Popover open={open} onOpenChange={setOpen}>
146+
<PopoverTrigger
147+
openOnHover
148+
delay={300}
149+
closeDelay={100}
150+
onClick={(event) => event.preventBaseUIHandler()}
151+
render={trigger}
152+
/>
153+
{open && (
154+
<ActivityHoverCard side="bottom" onClose={() => setOpen(false)} />
155+
)}
156+
</Popover>
157+
);
158+
}
159+
160+
function ActivityNavItem({
161+
isActive,
162+
unreadCount,
163+
onNavigate,
164+
}: {
165+
isActive: boolean;
166+
unreadCount: number;
167+
onNavigate: () => void;
168+
}) {
169+
const bell = (
170+
<NavButton
171+
icon={<BellIcon size={16} weight={isActive ? "fill" : "regular"} />}
172+
label="Activity"
173+
isActive={isActive}
174+
onClick={onNavigate}
175+
badge={<CountBadge count={unreadCount} className={ICON_BADGE_CLASS} />}
176+
/>
177+
);
178+
179+
if (isActive) return bell;
180+
return <ActivityHoverPopover trigger={bell} />;
181+
}
182+
136183
export function ChannelNav() {
137184
const view = useAppView();
138185
const loopsEnabled = useFeatureFlag(LOOPS_FLAG, import.meta.env.DEV);
139-
const [activityOpen, setActivityOpen] = useState(false);
140186

141187
const { counts } = useInboxAllReports({
142188
ignoreFilters: true,
@@ -178,44 +224,11 @@ export function ChannelNav() {
178224
<CountBadge count={counts.pulls} className={ICON_BADGE_CLASS} />
179225
}
180226
/>
181-
<Popover
182-
open={!isActivity && activityOpen}
183-
onOpenChange={(open) => setActivityOpen(!isActivity && open)}
184-
>
185-
<PopoverTrigger
186-
openOnHover
187-
delay={300}
188-
closeDelay={100}
189-
render={
190-
<NavButton
191-
icon={
192-
<BellIcon
193-
size={16}
194-
weight={isActivity ? "fill" : "regular"}
195-
/>
196-
}
197-
label="Activity"
198-
isActive={isActivity}
199-
onClick={() => {
200-
setActivityOpen(false);
201-
withTrack("activity", navigateToActivity)();
202-
}}
203-
badge={
204-
<CountBadge
205-
count={unseenActivity}
206-
className={ICON_BADGE_CLASS}
207-
/>
208-
}
209-
/>
210-
}
211-
/>
212-
{!isActivity && activityOpen && (
213-
<ActivityHoverCard
214-
side="bottom"
215-
onClose={() => setActivityOpen(false)}
216-
/>
217-
)}
218-
</Popover>
227+
<ActivityNavItem
228+
isActive={isActivity}
229+
unreadCount={unseenActivity}
230+
onNavigate={withTrack("activity", navigateToActivity)}
231+
/>
219232
<NavIcon
220233
icon={
221234
<Lightning

0 commit comments

Comments
 (0)