diff --git a/src/components/TransactionHistory.test.tsx b/src/components/TransactionHistory.test.tsx index 0da1b1b..e3f3857 100644 --- a/src/components/TransactionHistory.test.tsx +++ b/src/components/TransactionHistory.test.tsx @@ -420,6 +420,46 @@ describe("TransactionHistory", () => { expect(row.tagName).toBe("DIV"); expect(row).not.toHaveAttribute("href"); }); + + it("mentions the explorer link in the row's aria-label when it is one (#563)", async () => { + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + address: ADDRESS, + isConnected: true, + network: { name: "testnet" } as ReturnType["network"], + }), + ); + const tx = makeTx(0); + mockGetHistory([tx], 1); + + render(); + act(() => { vi.advanceTimersByTime(0); }); + + await waitFor(() => screen.getByRole("article")); + expect(screen.getByRole("article")).toHaveAccessibleName( + expect.stringMatching(/stellar expert.*opens in a new tab/i), + ); + }); + + it("does not mention an explorer link in the aria-label for a non-link row (#563)", async () => { + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + address: ADDRESS, + isConnected: true, + network: { name: "futurenet" } as ReturnType["network"], + }), + ); + const tx = makeTx(0); + mockGetHistory([tx], 1); + + render(); + act(() => { vi.advanceTimersByTime(0); }); + + await waitFor(() => screen.getByRole("article")); + expect(screen.getByRole("article")).not.toHaveAccessibleName( + expect.stringMatching(/stellar expert/i), + ); + }); }); describe("status and date range filtering (#350, #352)", () => { diff --git a/src/components/TransactionHistory.tsx b/src/components/TransactionHistory.tsx index 00381e4..64eed8f 100644 --- a/src/components/TransactionHistory.tsx +++ b/src/components/TransactionHistory.tsx @@ -160,11 +160,17 @@ export const TxRow = memo(function TxRow({ ? { href: explorerUrl, target: "_blank", rel: "noopener noreferrer" } : {}; + const rowLabel = `Transaction ${truncateAddress(tx.hash, 10, 6)} — ${tx.successful ? "Success" : "Failed"} — Fee: ${tx.feePaid} stroops`; + return ( )} role="article" - aria-label={`Transaction ${truncateAddress(tx.hash, 10, 6)} — ${tx.successful ? "Success" : "Failed"} — Fee: ${tx.feePaid} stroops`} + aria-label={ + explorerUrl + ? `${rowLabel} — view on Stellar Expert (opens in a new tab)` + : rowLabel + } className="flex items-center justify-between px-5 py-3.5 border-b border-line last:border-0 gap-4 hover:bg-surface-2 transition-colors cursor-pointer" >
diff --git a/src/components/TransactionPanel.test.tsx b/src/components/TransactionPanel.test.tsx index d109556..d4e7e13 100644 --- a/src/components/TransactionPanel.test.tsx +++ b/src/components/TransactionPanel.test.tsx @@ -415,6 +415,39 @@ describe("TransactionPanel", () => { "https://stellar.expert/explorer/testnet/tx/txhash123", ); }); + + it("gives both explorer links an accessible aria-label (#563)", async () => { + const mockSubmit = vi + .fn() + .mockResolvedValue({ data: { hash: "txhash123", ledger: 100 }, error: null }); + mockGetClient(mockSubmit); + vi.mocked(useSorokit).mockReturnValue({ + address: "GABC", + isConnected: true, + network: { name: "testnet", passphrase: "x", rpcUrl: "x", horizonUrl: "x" }, + client: getClient(), + } as unknown as ReturnType); + + render(); + + const validDest = "GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + fireEvent.change(screen.getByLabelText("Destination Address"), { target: { value: validDest } }); + fireEvent.change(screen.getByLabelText("Amount (XLM)"), { target: { value: "10" } }); + + await reviewAndConfirm(); + await screen.findByText("Transaction submitted"); + + // Scoped to the two links this change touches — TransactionStatusTracker + // renders its own separate explorer link lower in the panel. + const hashLink = screen.getByText("txhash123").closest("a")!; + const badgeLink = screen.getByRole("link", { + name: /view on stellar expert/i, + }); + for (const link of [hashLink, badgeLink]) { + expect(link).toHaveAccessibleName(expect.stringContaining("txhash123")); + expect(link).toHaveAccessibleName(expect.stringMatching(/opens in a new tab/i)); + } + }); }); describe("default prop pre-fill (#351)", () => { diff --git a/src/components/TransactionPanel.tsx b/src/components/TransactionPanel.tsx index 9dfc9f6..d8eb31c 100644 --- a/src/components/TransactionPanel.tsx +++ b/src/components/TransactionPanel.tsx @@ -268,9 +268,10 @@ export function TransactionPanel({ href={explorerUrl} target="_blank" rel="noopener noreferrer" + aria-label={`View transaction ${result.hash} on Stellar Expert (opens in a new tab)`} className="break-all leading-relaxed text-brand hover:underline inline-flex items-start gap-1.5" > - {result.hash} + ) : ( @@ -287,6 +288,7 @@ export function TransactionPanel({ href={explorerUrl} target="_blank" rel="noopener noreferrer" + aria-label={`View on Stellar Expert: transaction ${result.hash} (opens in a new tab)`} className="text-[11px] text-brand hover:underline" > View on Stellar Expert