Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/forty-bobcats-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ledger-live-desktop": minor
---

add History button in topbar
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import React, { useCallback } from "react";
import { IconButton, Tooltip, TooltipTrigger, TooltipContent } from "@ledgerhq/lumen-ui-react";
import {
Button,
IconButton,
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@ledgerhq/lumen-ui-react";
import type { TopBarAction } from "../types";

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

Expand All @@ -23,20 +30,34 @@ export function TopBarActionButton({
[onTooltipShow],
);

const control = cta ? (
<Button
appearance={appearance}
size="sm"
icon={icon}
onClick={onClick}
data-testid={testId}
disabled={!isInteractive}
className="rounded-full"
>
{cta}
</Button>
) : (
<IconButton
appearance={appearance}
size="sm"
aria-label={label}
icon={icon}
onClick={onClick}
data-testid={testId}
disabled={!isInteractive}
/>
);

return (
<div className="flex items-center gap-12">
<Tooltip onOpenChange={handleOpenChange}>
Comment on lines 57 to 59
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This MVVM component uses a raw <div> wrapper for layout. In src/mvvm/, UI code is expected to rely on the design system layout primitives (e.g., Lumen / shared layout components) rather than raw HTML containers so styling and accessibility stay consistent. Consider replacing this wrapper with an appropriate design-system layout component.

Copilot generated this review using guidance from repository custom instructions.
<TooltipTrigger asChild>
<IconButton
appearance={appearance}
size="sm"
aria-label={label}
icon={icon}
onClick={onClick}
data-testid={testId}
disabled={!isInteractive}
/>
</TooltipTrigger>
<TooltipTrigger asChild>{control}</TooltipTrigger>
{tooltip && (
<TooltipContent side="bottom" className={tooltipClassName}>
{tooltip}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from "react";
import { render, screen } from "tests/testSetup";
import { TopBarActionButton } from "../TopBarActionButton";
import { Eye, Clock } from "@ledgerhq/lumen-ui-react/symbols";

describe("TopBarActionButton", () => {
const defaultProps = {
label: "test-action",
isInteractive: true,
onClick: jest.fn(),
icon: Eye,
};

beforeEach(() => {
jest.clearAllMocks();
});

it("renders an IconButton when no cta is provided", () => {
render(<TopBarActionButton {...defaultProps} />);

const button = screen.getByTestId("topbar-action-button-test-action");
expect(button).toBeVisible();
expect(button).toHaveAttribute("aria-label", "test-action");
expect(screen.queryByText("History")).not.toBeInTheDocument();
});

it("renders a Button with text label when cta is provided", () => {
render(<TopBarActionButton {...defaultProps} icon={Clock} cta="History" />);

const button = screen.getByTestId("topbar-action-button-test-action");
expect(button).toBeVisible();
expect(screen.getByText("History")).toBeInTheDocument();
});

it("calls onClick when the cta button is clicked", async () => {
const handleClick = jest.fn();

const { user } = render(
<TopBarActionButton {...defaultProps} icon={Clock} cta="History" onClick={handleClick} />,
);

await user.click(screen.getByTestId("topbar-action-button-test-action"));
expect(handleClick).toHaveBeenCalledTimes(1);
});

it("disables the cta button when isInteractive is false", () => {
render(
<TopBarActionButton {...defaultProps} icon={Clock} cta="History" isInteractive={false} />,
);

const button = screen.getByTestId("topbar-action-button-test-action");
expect(button).toBeDisabled();
});

it("calls onClick when the icon button is clicked", async () => {
const handleClick = jest.fn();

const { user } = render(<TopBarActionButton {...defaultProps} onClick={handleClick} />);

await user.click(screen.getByTestId("topbar-action-button-test-action"));
expect(handleClick).toHaveBeenCalledTimes(1);
});

it("disables the icon button when isInteractive is false", () => {
render(<TopBarActionButton {...defaultProps} isInteractive={false} />);

const button = screen.getByTestId("topbar-action-button-test-action");
expect(button).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Clock } from "@ledgerhq/lumen-ui-react/symbols";
import { renderHook, act } from "tests/testSetup";
import { useHistory } from "../useHistory";
import { useNavigate, useLocation } from "react-router";

jest.mock("react-router", () => ({
...jest.requireActual("react-router"),
useNavigate: jest.fn(),
useLocation: jest.fn(),
}));

const mockNavigate = jest.fn();
const mockUseNavigate = jest.mocked(useNavigate);
const mockUseLocation = jest.mocked(useLocation);

const createLocation = (pathname: string) => ({
pathname,
state: null,
key: "default",
search: "",
hash: "",
});

describe("useHistory", () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseNavigate.mockReturnValue(mockNavigate);
mockUseLocation.mockReturnValue(createLocation("/history"));
});

it("returns handleHistory, historyIcon, tooltip, and cta", () => {
const { result } = renderHook(() => useHistory());

expect(result.current.handleHistory).toBeDefined();
expect(result.current.historyIcon).toBe(Clock);
expect(result.current.tooltip).toBeDefined();
expect(result.current.cta).toBeDefined();
});

it("navigates to /history and sets tracking source when not already on history page", () => {
mockUseLocation.mockReturnValueOnce(createLocation("/accounts"));

const { result } = renderHook(() => useHistory());

act(() => {
result.current.handleHistory();
});

expect(mockNavigate).toHaveBeenCalledWith("/history");
});

it("does not navigate when already on history page", () => {
const { result } = renderHook(() => useHistory());

act(() => {
result.current.handleHistory();
});

expect(mockNavigate).not.toHaveBeenCalled();
});
});
Loading
Loading