Skip to content

Commit bf447f5

Browse files
feat(desktop): add history button with cta support in topbar
1 parent 709b5a7 commit bf447f5

9 files changed

Lines changed: 384 additions & 180 deletions

File tree

.changeset/forty-bobcats-wonder.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ledger-live-desktop": minor
3+
---
4+
5+
add History button in topbar

apps/ledger-live-desktop/src/mvvm/components/TopBar/components/TopBarActionButton.tsx

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import React, { useCallback } from "react";
2-
import { IconButton, Tooltip, TooltipTrigger, TooltipContent } from "@ledgerhq/lumen-ui-react";
2+
import {
3+
Button,
4+
IconButton,
5+
Tooltip,
6+
TooltipTrigger,
7+
TooltipContent,
8+
} from "@ledgerhq/lumen-ui-react";
39
import type { TopBarAction } from "../types";
410

511
type TopBarActionButtonProps = TopBarAction;
@@ -13,6 +19,7 @@ export function TopBarActionButton({
1319
icon,
1420
appearance = "gray",
1521
onTooltipShow,
22+
cta,
1623
}: TopBarActionButtonProps) {
1724
const testId = `topbar-action-button-${label.replace(/\s+/g, "-").toLowerCase()}`;
1825

@@ -23,20 +30,34 @@ export function TopBarActionButton({
2330
[onTooltipShow],
2431
);
2532

33+
const control = cta ? (
34+
<Button
35+
appearance={appearance}
36+
size="sm"
37+
icon={icon}
38+
onClick={onClick}
39+
data-testid={testId}
40+
disabled={!isInteractive}
41+
className="rounded-full"
42+
>
43+
{cta}
44+
</Button>
45+
) : (
46+
<IconButton
47+
appearance={appearance}
48+
size="sm"
49+
aria-label={label}
50+
icon={icon}
51+
onClick={onClick}
52+
data-testid={testId}
53+
disabled={!isInteractive}
54+
/>
55+
);
56+
2657
return (
2758
<div className="flex items-center gap-12">
2859
<Tooltip onOpenChange={handleOpenChange}>
29-
<TooltipTrigger asChild>
30-
<IconButton
31-
appearance={appearance}
32-
size="sm"
33-
aria-label={label}
34-
icon={icon}
35-
onClick={onClick}
36-
data-testid={testId}
37-
disabled={!isInteractive}
38-
/>
39-
</TooltipTrigger>
60+
<TooltipTrigger asChild>{control}</TooltipTrigger>
4061
{tooltip && (
4162
<TooltipContent side="bottom" className={tooltipClassName}>
4263
{tooltip}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React from "react";
2+
import { render, screen } from "tests/testSetup";
3+
import { TopBarActionButton } from "../TopBarActionButton";
4+
import { Eye, Clock } from "@ledgerhq/lumen-ui-react/symbols";
5+
6+
describe("TopBarActionButton", () => {
7+
const defaultProps = {
8+
label: "test-action",
9+
isInteractive: true,
10+
onClick: jest.fn(),
11+
icon: Eye,
12+
};
13+
14+
beforeEach(() => {
15+
jest.clearAllMocks();
16+
});
17+
18+
it("renders an IconButton when no cta is provided", () => {
19+
render(<TopBarActionButton {...defaultProps} />);
20+
21+
const button = screen.getByTestId("topbar-action-button-test-action");
22+
expect(button).toBeVisible();
23+
expect(button).toHaveAttribute("aria-label", "test-action");
24+
expect(screen.queryByText("History")).not.toBeInTheDocument();
25+
});
26+
27+
it("renders a Button with text label when cta is provided", () => {
28+
render(<TopBarActionButton {...defaultProps} icon={Clock} cta="History" />);
29+
30+
const button = screen.getByTestId("topbar-action-button-test-action");
31+
expect(button).toBeVisible();
32+
expect(screen.getByText("History")).toBeInTheDocument();
33+
});
34+
35+
it("calls onClick when the cta button is clicked", async () => {
36+
const handleClick = jest.fn();
37+
38+
const { user } = render(
39+
<TopBarActionButton {...defaultProps} icon={Clock} cta="History" onClick={handleClick} />,
40+
);
41+
42+
await user.click(screen.getByTestId("topbar-action-button-test-action"));
43+
expect(handleClick).toHaveBeenCalledTimes(1);
44+
});
45+
46+
it("disables the cta button when isInteractive is false", () => {
47+
render(
48+
<TopBarActionButton {...defaultProps} icon={Clock} cta="History" isInteractive={false} />,
49+
);
50+
51+
const button = screen.getByTestId("topbar-action-button-test-action");
52+
expect(button).toBeDisabled();
53+
});
54+
55+
it("calls onClick when the icon button is clicked", async () => {
56+
const handleClick = jest.fn();
57+
58+
const { user } = render(<TopBarActionButton {...defaultProps} onClick={handleClick} />);
59+
60+
await user.click(screen.getByTestId("topbar-action-button-test-action"));
61+
expect(handleClick).toHaveBeenCalledTimes(1);
62+
});
63+
64+
it("disables the icon button when isInteractive is false", () => {
65+
render(<TopBarActionButton {...defaultProps} isInteractive={false} />);
66+
67+
const button = screen.getByTestId("topbar-action-button-test-action");
68+
expect(button).toBeDisabled();
69+
});
70+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Clock } from "@ledgerhq/lumen-ui-react/symbols";
2+
import { renderHook, act } from "tests/testSetup";
3+
import { useHistory } from "../useHistory";
4+
import { useNavigate, useLocation } from "react-router";
5+
6+
jest.mock("react-router", () => ({
7+
...jest.requireActual("react-router"),
8+
useNavigate: jest.fn(),
9+
useLocation: jest.fn(),
10+
}));
11+
12+
const mockNavigate = jest.fn();
13+
const mockUseNavigate = jest.mocked(useNavigate);
14+
const mockUseLocation = jest.mocked(useLocation);
15+
16+
const createLocation = (pathname: string) => ({
17+
pathname,
18+
state: null,
19+
key: "default",
20+
search: "",
21+
hash: "",
22+
});
23+
24+
describe("useHistory", () => {
25+
beforeEach(() => {
26+
jest.clearAllMocks();
27+
mockUseNavigate.mockReturnValue(mockNavigate);
28+
mockUseLocation.mockReturnValue(createLocation("/history"));
29+
});
30+
31+
it("returns handleHistory, historyIcon, tooltip, and cta", () => {
32+
const { result } = renderHook(() => useHistory());
33+
34+
expect(result.current.handleHistory).toBeDefined();
35+
expect(result.current.historyIcon).toBe(Clock);
36+
expect(result.current.tooltip).toBeDefined();
37+
expect(result.current.cta).toBeDefined();
38+
});
39+
40+
it("navigates to /history and sets tracking source when not already on history page", () => {
41+
mockUseLocation.mockReturnValueOnce(createLocation("/accounts"));
42+
43+
const { result } = renderHook(() => useHistory());
44+
45+
act(() => {
46+
result.current.handleHistory();
47+
});
48+
49+
expect(mockNavigate).toHaveBeenCalledWith("/history");
50+
});
51+
52+
it("does not navigate when already on history page", () => {
53+
const { result } = renderHook(() => useHistory());
54+
55+
act(() => {
56+
result.current.handleHistory();
57+
});
58+
59+
expect(mockNavigate).not.toHaveBeenCalled();
60+
});
61+
});

0 commit comments

Comments
 (0)