Skip to content

Label pool deposit input, announce async results, surface specific wallet errors, fix hero stat drift - #317

Merged
chonilius merged 5 commits into
MergeFi:mainfrom
presidojay1:fix/pool-label-aria-live-wallet-error-hero-stat
Aug 25, 2026
Merged

Label pool deposit input, announce async results, surface specific wallet errors, fix hero stat drift#317
chonilius merged 5 commits into
MergeFi:mainfrom
presidojay1:fix/pool-label-aria-live-wallet-error-hero-stat

Conversation

@presidojay1

Copy link
Copy Markdown
Contributor

Summary

Four fixes across accessibility, wallet error handling, and a hardcoded stat:

Unlabeled deposit input (#236)PoolDepositButton's amount <input> had no <label>, aria-label, or aria-labelledby. A screen reader user tabbing to it heard only "number input, value 100" with no indication of what it represents — and the page renders one of these identical, unlabeled input/button pairs per maintenance pool. Added a visually-hidden <label htmlFor> matched to a per-pool id.

No aria-live on async results (#234)ConnectPanel, IssueActions, and MilestoneActions all conditionally render a <p> with the result of an async action (wallet connect, fund/claim/refund, milestone funding, pool deposit), but none had role="status"/role="alert"/aria-live. A sighted user sees the message the instant it appears; a screen reader user got no announcement at all. Added role="status" aria-live="polite" to success/notice messages and role="alert" to error messages across all three.

Generic wallet error swallows the specific reason (#235)WalletContext#connect already computes a specific, useful error (extension not installed, access denied, address unreadable) and stores it in its own state, but resolves to null on failure rather than rejecting. IssueActions/MilestoneActions never read that state, showing a generic fallback instead — and simply destructuring error from useWallet() wouldn't have worked either, since that value can still reflect a pre-await render by the time the awaited connect() call settles inside an async handler. Added a getError() accessor on WalletContext, backed by a ref kept in lockstep with every setError() call, so it's guaranteed current regardless of React's render/commit timing — and wired both components to prefer it over their generic fallback.

Hero stat drift (#237) — the homepage hero hardcoded "341 contributors" as a literal while a StatCard a few hundred lines later renders the same fact from platformStats.activeContributors — two independent sources of truth that only agreed because 341 happened to match today. Interpolated the same platformStats value into the hero sentence.

Changes

  • src/context/WalletContext.tsx / .test.tsx — new getError() accessor + test.
  • src/app/issues/[id]/IssueActions.tsx — aria-live roles + getError() usage.
  • src/app/milestones/MilestoneActions.tsx — input label + aria-live roles + getError() usage.
  • src/app/connect/ConnectPanel.tsx — aria-live role.
  • src/app/page.tsx — hero copy now derived from platformStats.activeContributors.

Test plan

  • npx tsc --noEmit — clean
  • npx eslint on all changed files — clean
  • npx jest (full suite) — 195/195 passing, no regressions

Closes #236
Closes #234
Closes #235
Closes #237

… synchronously

WalletContext#connect already computes a specific, useful error
("Wallet access was not granted.", "Unable to read wallet address.", a
Freighter-SDK message, or the "not installed" message) and stores it in
its own error state — but connect() resolves to null on failure rather
than rejecting, and a caller that awaits it and gets null back has no
reliable way to read that specific reason: destructuring `error` from
useWallet() at render time captures a value from whichever render
created that closure, not necessarily updated yet by the time the
awaited connect() call actually settles inside an async event handler.

Added an errorRef kept in lockstep with every setError() call (via a
new updateError() wrapper) and a getError() accessor that reads it
directly — a plain ref read has no dependency on React's render/commit
timing, so it's guaranteed to reflect connect()'s outcome the instant
its promise resolves, regardless of whether the consuming component has
re-rendered yet. Added a direct test exercising this exact pattern
(MergeFi#235).
… specific wallet errors

The notice/error <p> elements IssueActions renders after fund/claim/
refund actions had no role or aria-live attribute — a screen reader
user got no announcement when one of these real, often money-moving
actions completed, unlike a sighted user who sees the message appear
immediately. Added role="status" aria-live="polite" to the success
notice and role="alert" (implicitly assertive) to the error message
(MergeFi#234).

Also, withWallet() fell back to a generic "Connect a Stellar wallet to
continue." whenever connect() resolved to null, discarding the specific
reason WalletContext's own error state already computed (extension not
installed, access denied, address unreadable). Now prefers
getWalletError() — read synchronously right after the awaited
connect() call, so it's guaranteed current — over the generic fallback
(MergeFi#235).
…e specific wallet errors

PoolDepositButton's amount <input> had no <label>, aria-label, or
aria-labelledby — a screen reader user tabbing to it heard only "number
input, value 100" with no indication it's a deposit amount, and the
page renders one of these identical, unlabeled input/button pairs per
maintenance pool. Added a visually-hidden <label htmlFor> matched to a
per-pool id (closes MergeFi#236).

Also applied the same two fixes as IssueActions to both
MilestoneFundButton and PoolDepositButton: their error <p> elements got
role="alert" so screen reader users are notified when a fund/deposit
action fails (MergeFi#234), and handleFund()/handleDeposit() now prefer
getWalletError() — WalletContext's specific failure reason, read
synchronously right after the awaited connect() call — over their
generic fallback messages (MergeFi#235).
ConnectPanel's error <p> (shown when useWallet()'s connect() fails) had
no role or aria-live attribute, so a screen reader user attempting to
connect Freighter from this app's sole onboarding entry point got no
announcement when the attempt failed. Added role="alert" to match the
same fix applied to IssueActions/MilestoneActions' error messages
(MergeFi#234).
…ad of a literal

The hero copy hardcoded "341 contributors" as a literal, while a
StatCard a few hundred lines later on the same page renders the same
fact from platformStats.activeContributors.toLocaleString() — two
independent sources of truth for one number that only agreed because
341 happened to match today. Once activeContributors changes (or is
wired to a real fetched value), the hero copy has no relationship to it
and silently drifts, showing two different contributor counts within a
few screens of each other. Interpolated the same platformStats value
into the hero sentence so the two can't diverge (closes MergeFi#237).
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

@presidojay1 is attempting to deploy a commit to the chonilius' projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Aug 25, 2026

Copy link
Copy Markdown

@presidojay1 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@chonilius
chonilius merged commit 464c34b into MergeFi:main Aug 25, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment