Skip to content

fix(components): label icon-only controls for VoiceOver - #18

Open
romchornyi wants to merge 1 commit into
masterfrom
fix/accessibility-labels
Open

fix(components): label icon-only controls for VoiceOver#18
romchornyi wants to merge 1 commit into
masterfrom
fix/accessibility-labels

Conversation

@romchornyi

Copy link
Copy Markdown
Collaborator

Issue being fixed or feature implemented

Dash Wallet ticket 32072: "Please add support for screenreaders. The tabs at the bottom, as well as buttons, need to be labeled so that screenreaders can read them."

An accessibility audit of the wallet found that its Swap and Coinbase screens are ~88% accessible purely by inheriting this package's labels — the app-side files for those flows contain zero accessibility calls of their own. That cuts both ways: a gap here is inherited just as widely as a fix is.

A companion PR fixes the app's own shared components (dashpay/dashwallet-ios#…); this one closes the design-system half.

What was done?

Toast.swift — the icon-only dismiss button had no label.

ConverterArrowBadge.swift — the swap-direction badge announced its SF Symbol name ("diagonal-up-down"). Now "Swap direction". The non-interactive arrow-down variant is marked .accessibilityHidden(true), since it is decorative and was also announcing its asset name.

SwapAmountView.swift — the currency-select chevron had no label and, more importantly, never announced which currency is currently selected. A picker that does not say what it currently holds is unusable non-visually, so it now carries both an accessibilityLabel ("Select currency") and an accessibilityValue (the row's currency symbol, or "Dash" for a Dash row). Both call sites — the static primary row and the animated B row — are covered.

DualSwapAmountView.swift — the swap area was a bare container .onTapGesture: invisible to VoiceOver, no button trait, and no way to activate it at all. It is now a single element combining both amounts, with the .isButton trait, a hint describing what activation does, and an .accessibilityAction so double-tap actually swaps.

DashButton.swift — adds an optional accessibilityLabel. Constructing the button with an icon and text: nil produced a control whose only announcement was the asset name, with no way for a caller to supply a label. That is an API that makes accessibility impossible rather than merely absent, which is worse than a missing call.

Worth noting: the static rules the wallet now runs in CI do not catch the chevron — its label is DashIcon.Common.chevronDownCurrencySelect.image rather than a literal Image(...), so the rule sees no bare image. It was found by reading the code. The tooling under-reports; it is a ratchet, not a proof of correctness.

How Has This Been Tested?

  • swift build — Build complete. swift build --build-tests also passes; this is the real check that the package still compiles for its consumer.
  • The wallet's static accessibility audit (scripts/a11y_audit.py from dashwallet-ios, run against this package) reported 3 findings for A11Y004/A11Y012 before and 0 after. Its remaining findings here are all A11Y007 (Dynamic Type), which is separate work.
  • Each spoken result was derived by reading the resulting modifier chain:
    • Swap badge: "Swap direction, button"
    • Currency picker: "Select currency, $, button" (or "…, Dash, button" on a Dash row)
    • Dual-swap amounts: "1.5 DASH, 150.00 USD, button. Switches which currency you enter the amount in." — double-tap swaps
    • Toast close: "Close, button"
  • Not verified on a device with VoiceOver running. The utterances above are read off the code, not heard. Someone should confirm the dual-swap activation and the picker value on hardware before this is considered done.

One finding from the audit turned out not to be a real defect: the .onTapGesture reported at SwapAmountView sits inside #if DEBUG preview code and never ships. It was fixed anyway so the preview demonstrates the correct pattern.

Breaking Changes

None. One public API addition: DashButton.accessibilityLabel: String?, as both a property and an init parameter, defaulted to nil and placed before the trailing action: parameter — every existing call site, labelled or trailing-closure, compiles unchanged. No signatures were renamed or removed.

The package has no localization catalog (no .strings/.xcstrings; Package.swift declares defaultLocalization: "en"). New strings follow the existing convention of NSLocalizedString(_, bundle: .module, comment: "DashUIKit") relying on the key as its English fallback, exactly as NavigationBar, SearchBar and AddressFieldView already do. Keys introduced: "Select currency", "Swap direction", "Switches which currency you enter the amount in". If a catalog is ever added, these need entries.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

The Swap and Coinbase screens in the wallet are largely accessible only
because they inherit this package's labels — their own files contain no
accessibility calls at all. The gaps here are inherited just as widely.

Toast's dismiss button, ConverterArrowBadge and the currency-select chevron
announced their asset or SF Symbol names ("diagonal-up-down", "icon copy
outline"), which sounds like a working label in a quick VoiceOver pass but
tells a blind user nothing. Each now carries a proper label, and the chevron
also carries an accessibilityValue naming the currency it currently holds —
a picker that never says what it is set to is unusable non-visually.

DualSwapAmountView's swap area was a bare container .onTapGesture: no button
trait, no label, and no way to activate it. It is now a single element with
both amounts in its label and an accessibilityAction, so double-tap swaps.

DashButton gains an optional accessibilityLabel. Constructing it with an icon
and no text produced a button whose only announcement was the asset name, with
no way for a caller to override — an API that made accessibility impossible
rather than merely absent. The parameter defaults to nil and sits before
`action:`, so every existing call site compiles unchanged.

The static rules the wallet uses do not catch the chevron: its label is
DashIcon.Common.chevronDownCurrencySelect.image rather than a literal Image,
so the rule sees no bare image. Found by reading, not by tooling.

Verified with swift build and swift build --build-tests.
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b9f4bd0a-4af7-481a-9f1b-0344fd120007


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants