From d1b929df36648d3f5ffff4481362a9cfb03d3bf6 Mon Sep 17 00:00:00 2001 From: "Adebanjo Abraham.I" Date: Sat, 29 Aug 2026 12:19:17 +0100 Subject: [PATCH] fix(a11y): give explorer links a descriptive aria-label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TransactionHistory already links each row to stellar.expert (added in a prior PR) and TransactionPanel already links the success hash, but neither surfaced that fact through an aria-label a screen reader user can rely on: - TransactionHistory's row aria-label described the transaction (hash/status/fee) but never mentioned it was also a link to an external explorer, or that it opens in a new tab. - TransactionPanel's two explorer links (the hash itself, and the 'View on Stellar Expert' badge link) had no aria-label at all. - TransactionHistory.tsx: append '— view on Stellar Expert (opens in a new tab)' to a row's aria-label only when it actually renders as a link (network unrecognized -> plain row, label unchanged). - TransactionPanel.tsx: add an aria-label to both explorer links, and mark the hash text inside the first one aria-hidden so the link's accessible name comes from the aria-label instead of being read twice. Closes #563 --- src/components/TransactionHistory.test.tsx | 40 ++++++++++++++++++++++ src/components/TransactionHistory.tsx | 8 ++++- src/components/TransactionPanel.test.tsx | 33 ++++++++++++++++++ src/components/TransactionPanel.tsx | 4 ++- 4 files changed, 83 insertions(+), 2 deletions(-) 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