Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions packages/ui/src/features/canvas/components/ChannelNav.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,48 @@ describe("ChannelNav", () => {

const activity = screen.getByLabelText("Activity");
expect(activity).toBeEnabled();
expect(activity).not.toHaveAttribute("aria-haspopup");
await user.hover(activity);

await new Promise((resolve) => setTimeout(resolve, 400));
expect(screen.queryByText("Recent activity card")).not.toBeInTheDocument();
});

it("leaves no popover state on the bell after it navigates to Activity", async () => {
const user = userEvent.setup();
const { rerender } = render(<ChannelNav />);
const bell = () => screen.getByLabelText("Activity");

await user.hover(bell());
await screen.findByText("Recent activity card", {}, { timeout: 1_000 });
await user.click(bell());
mocks.view = { type: "activity" };
rerender(<ChannelNav />);

expect(screen.queryByText("Recent activity card")).not.toBeInTheDocument();
expect(bell()).not.toHaveAttribute("data-popup-open");
expect(bell()).not.toHaveAttribute("data-pressed");
});

it("neither resurfaces nor wedges the hover card once the bell has navigated", async () => {
const user = userEvent.setup();
const { rerender } = render(<ChannelNav />);
const bell = () => screen.getByLabelText("Activity");

await user.hover(bell());
await user.click(bell());
mocks.view = { type: "activity" };
rerender(<ChannelNav />);
await user.unhover(bell());

mocks.view = { type: "task-detail" };
rerender(<ChannelNav />);
await new Promise((resolve) => setTimeout(resolve, 400));
expect(screen.queryByText("Recent activity card")).not.toBeInTheDocument();

await user.hover(bell());
expect(
await screen.findByText("Recent activity card", {}, { timeout: 1_000 }),
).toBeInTheDocument();
});
});
93 changes: 53 additions & 40 deletions packages/ui/src/features/canvas/components/ChannelNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ import {
} from "@posthog/ui/router/navigationBridge";
import { useAppView } from "@posthog/ui/router/useAppView";
import { track } from "@posthog/ui/shell/analytics";
import { type ComponentPropsWithRef, type ReactNode, useState } from "react";
import {
type ComponentPropsWithRef,
type ReactElement,
type ReactNode,
useState,
} from "react";
import { ActivityHoverCard } from "./ActivityHoverCard";

const INBOX_REFETCH_INTERVAL_MS = 60_000;
Expand Down Expand Up @@ -133,10 +138,51 @@ function NavButton({
);
}

function ActivityHoverPopover({ trigger }: { trigger: ReactElement }) {
const [open, setOpen] = useState(false);

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
openOnHover
delay={300}
closeDelay={100}
onClick={(event) => event.preventBaseUIHandler()}
render={trigger}
/>
{open && (
<ActivityHoverCard side="bottom" onClose={() => setOpen(false)} />
)}
</Popover>
);
}

function ActivityNavItem({
isActive,
unreadCount,
onNavigate,
}: {
isActive: boolean;
unreadCount: number;
onNavigate: () => void;
}) {
const bell = (
<NavButton
icon={<BellIcon size={16} weight={isActive ? "fill" : "regular"} />}
label="Activity"
isActive={isActive}
onClick={onNavigate}
badge={<CountBadge count={unreadCount} className={ICON_BADGE_CLASS} />}
/>
);

if (isActive) return bell;
return <ActivityHoverPopover trigger={bell} />;
}

export function ChannelNav() {
const view = useAppView();
const loopsEnabled = useFeatureFlag(LOOPS_FLAG, import.meta.env.DEV);
const [activityOpen, setActivityOpen] = useState(false);

const { counts } = useInboxAllReports({
ignoreFilters: true,
Expand Down Expand Up @@ -178,44 +224,11 @@ export function ChannelNav() {
<CountBadge count={counts.pulls} className={ICON_BADGE_CLASS} />
}
/>
<Popover
open={!isActivity && activityOpen}
onOpenChange={(open) => setActivityOpen(!isActivity && open)}
>
<PopoverTrigger
openOnHover
delay={300}
closeDelay={100}
render={
<NavButton
icon={
<BellIcon
size={16}
weight={isActivity ? "fill" : "regular"}
/>
}
label="Activity"
isActive={isActivity}
onClick={() => {
setActivityOpen(false);
withTrack("activity", navigateToActivity)();
}}
badge={
<CountBadge
count={unseenActivity}
className={ICON_BADGE_CLASS}
/>
}
/>
}
/>
{!isActivity && activityOpen && (
<ActivityHoverCard
side="bottom"
onClose={() => setActivityOpen(false)}
/>
)}
</Popover>
<ActivityNavItem
isActive={isActivity}
unreadCount={unseenActivity}
onNavigate={withTrack("activity", navigateToActivity)}
/>
<NavIcon
icon={
<Lightning
Expand Down
Loading