diff --git a/Sources/DashUIKit/Button/DashButton.swift b/Sources/DashUIKit/Button/DashButton.swift index c84b256..b90d981 100644 --- a/Sources/DashUIKit/Button/DashButton.swift +++ b/Sources/DashUIKit/Button/DashButton.swift @@ -133,6 +133,10 @@ public struct DashButton: View { public var size: DashButtonSize = .large public var style: DashButtonStyle = .filledBlue + /// VoiceOver label for a button whose content doesn't announce itself — + /// an icon-only button has no text, so without this it reads as the icon's + /// asset name. Ignored when nil; `text` then remains the announcement. + public var accessibilityLabel: String? = nil public var action: () -> Void = {} public init( @@ -144,6 +148,7 @@ public struct DashButton: View { fillsWidth: Bool = false, size: DashButtonSize, style: DashButtonStyle, + accessibilityLabel: String? = nil, action: @escaping () -> Void = {} ) { self.text = text @@ -154,10 +159,20 @@ public struct DashButton: View { self.fillsWidth = fillsWidth self.size = size self.style = style + self.accessibilityLabel = accessibilityLabel self.action = action } public var body: some View { + if let accessibilityLabel { + coreButton + .accessibilityLabel(Text(accessibilityLabel)) + } else { + coreButton + } + } + + private var coreButton: some View { Button(action: action) { styledContent } diff --git a/Sources/DashUIKit/Components/ConverterCard/ConverterArrowBadge.swift b/Sources/DashUIKit/Components/ConverterCard/ConverterArrowBadge.swift index 31168fa..a17cb86 100644 --- a/Sources/DashUIKit/Components/ConverterCard/ConverterArrowBadge.swift +++ b/Sources/DashUIKit/Components/ConverterCard/ConverterArrowBadge.swift @@ -39,8 +39,12 @@ struct ConverterArrowBadge: View { badge(iconName: "diagonal-up-down", iconRotation: rotation) } .buttonStyle(.plain) + .accessibilityLabel(Text(NSLocalizedString("Swap direction", bundle: .module, comment: "DashUIKit"))) } else { + // Decorative — the card's row order already conveys the direction, + // and without this the icon announces its asset name. badge(iconName: "arrow-down", iconRotation: 0) + .accessibilityHidden(true) } } .frame(height: 35) diff --git a/Sources/DashUIKit/Components/EnterAmount/DualSwapAmountView.swift b/Sources/DashUIKit/Components/EnterAmount/DualSwapAmountView.swift index 2830e6c..8e34914 100644 --- a/Sources/DashUIKit/Components/EnterAmount/DualSwapAmountView.swift +++ b/Sources/DashUIKit/Components/EnterAmount/DualSwapAmountView.swift @@ -64,6 +64,39 @@ internal struct DualSwapAmountView: View { .contentShape(Rectangle()) .onTapGesture { onSwap() } .dashPasteContextMenu(onPaste: onPaste) + // The swap lives on a bare tap gesture, so VoiceOver gets an explicit + // element: one button announcing both amounts, activated to swap. The + // chevron button inside survives `.combine` as a custom action. + .accessibilityElement(children: .combine) + .accessibilityLabel(Text(accessibilityDescription)) + .accessibilityAddTraits(.isButton) + .accessibilityHint(Text(NSLocalizedString( + "Switches which currency you enter the amount in", + bundle: .module, + comment: "DashUIKit" + ))) + .accessibilityAction { onSwap() } + } + + /// Both amounts with their currency names, primary first. + /// + /// That is the logical order, not necessarily the visual one: the animated + /// layout offsets the A and B rows past each other, so with + /// `isPrimaryLarge == false` the secondary amount is the one drawn on top. + /// Each amount carries its own currency name, so the announcement stays + /// unambiguous either way. + /// + /// The B row's error message stands in for the secondary amount when present, + /// mirroring what the view draws. + private var accessibilityDescription: String { + let primary = "\(primaryAmount.isEmpty ? "0" : primaryAmount) \(primaryCurrency.displayName)" + let secondary: String + if let secondaryErrorMessage { + secondary = secondaryErrorMessage + } else { + secondary = "\(secondaryAmount.isEmpty ? "0" : secondaryAmount) \(secondaryCurrency.displayName)" + } + return "\(primary), \(secondary)" } } diff --git a/Sources/DashUIKit/Components/EnterAmount/SwapAmountView.swift b/Sources/DashUIKit/Components/EnterAmount/SwapAmountView.swift index ec60f0a..5e89b88 100644 --- a/Sources/DashUIKit/Components/EnterAmount/SwapAmountView.swift +++ b/Sources/DashUIKit/Components/EnterAmount/SwapAmountView.swift @@ -177,6 +177,8 @@ public struct SwapAmountView: View { .frame(width: 10, height: 5) } .buttonStyle(.plain) + .accessibilityLabel(Text(NSLocalizedString("Select currency", bundle: .module, comment: "DashUIKit"))) + .accessibilityValue(Text(currencyAccessibilityValue)) } } .scaleToFitWidth() @@ -244,6 +246,13 @@ public struct SwapAmountView: View { if let first = s.first, first == "." || first == "," { return "0" + s } return s } + + /// What the currency picker currently holds, as VoiceOver reads it: + /// the row's symbol, or "Dash" when the row shows the Dash logo instead. + private var currencyAccessibilityValue: String { + if let sym = symbol, !sym.isEmpty { return sym } + return showDashLogo ? "Dash" : "" + } } // MARK: - AnimatedSwapLayout @@ -393,6 +402,8 @@ private struct AnimatedSwapLayout: View { .frame(width: chevronSize.width, height: chevronSize.height) } .buttonStyle(.plain) + .accessibilityLabel(Text(NSLocalizedString("Select currency", bundle: .module, comment: "DashUIKit"))) + .accessibilityValue(Text(secondaryCurrencyAccessibilityValue)) } } .foregroundColor(fontPrimary ? Color.dash.tertiaryText : Color.dash.primaryText) @@ -477,6 +488,13 @@ private struct AnimatedSwapLayout: View { if let first = s.first, first == "." || first == "," { return "0" + s } return s } + + /// What the B row's currency picker currently holds, as VoiceOver reads it: + /// the row's symbol, or "Dash" when the row shows the Dash logo instead. + private var secondaryCurrencyAccessibilityValue: String { + if let sym = secondarySymbol, !sym.isEmpty { return sym } + return showSecondaryDashLogo ? "Dash" : "" + } } // MARK: - Paste Context Menu @@ -660,6 +678,9 @@ private struct SwapAmountAnimatedPreview: View { ) .contentShape(Rectangle()) .onTapGesture { isPrimarySelected.toggle() } + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isButton) + .accessibilityAction { isPrimarySelected.toggle() } Button("Tap to swap") { isPrimarySelected.toggle() } .dashFont(.footnote) diff --git a/Sources/DashUIKit/Components/Toast.swift b/Sources/DashUIKit/Components/Toast.swift index 84dfbf7..b135e4b 100644 --- a/Sources/DashUIKit/Components/Toast.swift +++ b/Sources/DashUIKit/Components/Toast.swift @@ -104,6 +104,7 @@ public struct Toast: View { .background(Circle().fill(Color.dash.whiteAlpha10)) } .buttonStyle(.plain) + .accessibilityLabel(Text(NSLocalizedString("Close", bundle: .module, comment: "DashUIKit"))) } } .padding(.leading, 12)