kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(web): repair comment timestamp tooltip

Tin fbcf91d9 650418a5

+150 -8
+139
apps/web/src/components/activity/comment-card.test.tsx
··· 1 + import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 + import { fireEvent, render, screen } from "@testing-library/react"; 3 + import type { ReactElement, ReactNode } from "react"; 4 + import { cloneElement, isValidElement } from "react"; 5 + import { describe, expect, it, vi } from "vitest"; 6 + import CommentCard from "./comment-card"; 7 + 8 + vi.mock("@/components/activity/comment-editor", () => ({ 9 + default: ({ value }: { value: string }) => <div>{value}</div>, 10 + })); 11 + 12 + vi.mock("@/components/providers/auth-provider/hooks/use-auth", () => ({ 13 + useAuth: () => ({ user: null }), 14 + })); 15 + 16 + vi.mock("@/hooks/mutations/comment/use-update-comment", () => ({ 17 + default: () => ({ 18 + mutateAsync: vi.fn(), 19 + isPending: false, 20 + }), 21 + })); 22 + 23 + vi.mock("react-i18next", () => ({ 24 + useTranslation: () => ({ 25 + t: (key: string) => key, 26 + }), 27 + })); 28 + 29 + vi.mock("@/lib/format", () => ({ 30 + formatRelativeTime: () => "2 hours ago", 31 + formatDateTime: () => "Apr 5, 2026, 11:38 AM", 32 + })); 33 + 34 + vi.mock("@/components/ui/tooltip", async () => { 35 + const React = await import("react"); 36 + 37 + function Tooltip({ children }: { children: ReactNode }) { 38 + const [open, setOpen] = React.useState(false); 39 + return ( 40 + <div data-testid="tooltip-root"> 41 + {React.Children.map(children, (child) => 42 + isValidElement(child) 43 + ? cloneElement( 44 + child as ReactElement<{ 45 + open?: boolean; 46 + setOpen?: (open: boolean) => void; 47 + }>, 48 + { 49 + open, 50 + setOpen, 51 + }, 52 + ) 53 + : child, 54 + )} 55 + </div> 56 + ); 57 + } 58 + 59 + function TooltipTrigger({ 60 + children, 61 + setOpen, 62 + }: { 63 + children: ReactElement; 64 + setOpen?: (open: boolean) => void; 65 + }) { 66 + return cloneElement(children as ReactElement<Record<string, unknown>>, { 67 + onMouseEnter: () => setOpen?.(true), 68 + onMouseLeave: () => setOpen?.(false), 69 + onFocus: () => setOpen?.(true), 70 + onBlur: () => setOpen?.(false), 71 + }); 72 + } 73 + 74 + function TooltipContent({ 75 + children, 76 + open, 77 + }: { 78 + children: ReactNode; 79 + open?: boolean; 80 + }) { 81 + return open ? <div role="tooltip">{children}</div> : null; 82 + } 83 + 84 + function TooltipProvider({ children }: { children: ReactNode }) { 85 + return <>{children}</>; 86 + } 87 + 88 + return { 89 + Tooltip, 90 + TooltipContent, 91 + TooltipProvider, 92 + TooltipTrigger, 93 + }; 94 + }); 95 + 96 + function renderCommentCard() { 97 + const queryClient = new QueryClient({ 98 + defaultOptions: { 99 + queries: { retry: false }, 100 + mutations: { retry: false }, 101 + }, 102 + }); 103 + 104 + return render( 105 + <QueryClientProvider client={queryClient}> 106 + <CommentCard 107 + commentId="comment-1" 108 + taskId="task-1" 109 + content="Test comment" 110 + createdAt="2026-04-05T09:38:50.000Z" 111 + user={{ 112 + id: "user-1", 113 + name: "Tin", 114 + email: "tin@example.com", 115 + image: null, 116 + }} 117 + /> 118 + </QueryClientProvider>, 119 + ); 120 + } 121 + 122 + describe("CommentCard", () => { 123 + it("shows full date+short time in tooltip on hover/focus", async () => { 124 + renderCommentCard(); 125 + 126 + const trigger = screen.getByRole("button", { 127 + name: "Apr 5, 2026, 11:38 AM", 128 + }); 129 + 130 + fireEvent.mouseEnter(trigger); 131 + expect(await screen.findByText("Apr 5, 2026, 11:38 AM")).toBeVisible(); 132 + 133 + fireEvent.mouseLeave(trigger); 134 + expect(screen.queryByText("Apr 5, 2026, 11:38 AM")).not.toBeInTheDocument(); 135 + 136 + fireEvent.focus(trigger); 137 + expect(await screen.findByText("Apr 5, 2026, 11:38 AM")).toBeVisible(); 138 + }); 139 + });
+11 -8
apps/web/src/components/activity/comment-card.tsx
··· 57 57 const githubProfileUrl = 58 58 isFromGitHub && user?.name ? `https://github.com/${user.name}` : null; 59 59 const commentUrl = externalUrl || null; 60 + const fullTimestamp = formatDateTime(createdAt); 60 61 61 62 const handleEdit = useCallback(() => { 62 63 setEditedContent(content); ··· 163 164 164 165 <Tooltip> 165 166 <TooltipTrigger asChild> 166 - <time 167 - dateTime={new Date(createdAt).toISOString()} 168 - tabIndex={0} 169 - className="cursor-default text-xs text-muted-foreground/62 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" 167 + <button 168 + type="button" 169 + className="cursor-default text-xs text-muted-foreground/62 outline-none focus-visible:ring-2 focus-visible:ring-ring/70 focus-visible:ring-offset-2 focus-visible:ring-offset-background" 170 + aria-label={fullTimestamp} 171 + title={fullTimestamp} 170 172 > 171 - {formatRelativeTime(createdAt)} 172 - </time> 173 - </TooltipTrigger> 173 + <time dateTime={createdAt}> 174 + {formatRelativeTime(createdAt)} 175 + </time> 176 + </button> 174 177 </TooltipTrigger> 175 178 <TooltipContent> 176 - <p className="text-xs">{formatDateTime(createdAt)}</p> 179 + <p className="text-xs">{fullTimestamp}</p> 177 180 </TooltipContent> 178 181 </Tooltip> 179 182