Skip to content

Commit c018ea1

Browse files
[WCAG 2.1 AA] Blaze budget VoiceOver update (#15701)
2 parents 17dff0c + 5f2d365 commit c018ea1

File tree

4 files changed

+103
-7
lines changed

4 files changed

+103
-7
lines changed

WooCommerce/Classes/ViewRelated/Blaze/BudgetSetting/BlazeBudgetSettingView.swift

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ private extension BlazeBudgetSettingView {
5353
.multilineTextAlignment(.center)
5454
.subheadlineStyle()
5555
}
56+
.accessibilityElement(children: .combine)
5657

5758
// Daily budget amount details
5859
VStack(spacing: Layout.dailyBudgetSectionSpacing) {
@@ -69,6 +70,9 @@ private extension BlazeBudgetSettingView {
6970
in: viewModel.dailyAmountSliderRange,
7071
step: BlazeBudgetSettingViewModel.Constants.dailyAmountSliderStep)
7172
}
73+
.accessibilityElement(children: .combine)
74+
.accessibilityLabel(Localization.dailySpend)
75+
.accessibilityValue(String(format: Localization.dailySpendValue, Int(viewModel.dailyAmount)))
7276

7377
// Schedule
7478
VStack(alignment: .leading) {
@@ -89,8 +93,11 @@ private extension BlazeBudgetSettingView {
8993
.tertiaryTitleStyle()
9094
}
9195
.frame(maxWidth: .infinity, alignment: .leading)
96+
.accessibilityElement(children: .combine)
97+
.accessibilityAddTraits(.isButton)
98+
.accessibilityHint(Localization.scheduleAccessibilityHint)
9299

93-
// Estimated impressions
100+
// Estimated impressions - grouped for accessibility
94101
VStack(alignment: .leading) {
95102
Button {
96103
showingImpressionInfo = true
@@ -102,12 +109,15 @@ private extension BlazeBudgetSettingView {
102109
.font(.subheadline)
103110
}
104111
.buttonStyle(.plain)
105-
.accessibilityHint(Localization.estimatedImpressionsAccessibilityHint)
106112
.renderedIf(viewModel.forecastedImpressionState != .failure)
107113

108114
forecastedImpressionsView
109115
}
110116
.frame(maxWidth: .infinity, alignment: .leading)
117+
.accessibilityElement(children: .combine)
118+
.accessibilityAddTraits(.isButton)
119+
.accessibilityHint(Localization.estimatedImpressionsAccessibilityHint)
120+
.accessibilityLabel(viewModel.impressionsSectionAccessibilityLabel)
111121
}
112122
}
113123

@@ -116,7 +126,7 @@ private extension BlazeBudgetSettingView {
116126
switch viewModel.forecastedImpressionState {
117127
case .loading:
118128
ActivityIndicator(isAnimating: .constant(true), style: .medium)
119-
case .result(let formattedResult):
129+
case .result(let formattedResult, _, _):
120130
Text(formattedResult)
121131
.fontWeight(.semibold)
122132
.tertiaryTitleStyle()
@@ -157,7 +167,7 @@ private extension BlazeBudgetSettingView {
157167
.bodyStyle()
158168
.padding(Layout.contentPadding)
159169
}
160-
.navigationTitle(Localization.title)
170+
.navigationTitle(Localization.impressions)
161171
.navigationBarTitleDisplayMode(.inline)
162172
.toolbar {
163173
ToolbarItem(placement: .confirmationAction) {
@@ -214,6 +224,11 @@ private extension BlazeBudgetSettingView {
214224
value: "Daily spend",
215225
comment: "Title label for the daily spend amount on the Blaze ads campaign budget settings screen."
216226
)
227+
static let dailySpendValue = NSLocalizedString(
228+
"blazeBudgetSettingView.dailySpendValue",
229+
value: "$%d",
230+
comment: "Value format for the daily spend amount on the Blaze ads campaign budget settings screen."
231+
)
217232
static let estimatedImpressions = NSLocalizedString(
218233
"blazeBudgetSettingView.estimatedTotalImpressions",
219234
value: "Estimated total impressions",
@@ -262,6 +277,11 @@ private extension BlazeBudgetSettingView {
262277
value: "Tap for more information about estimated impressions",
263278
comment: "Accessibility hint for the estimated impression button on the Blaze campaign budget setting screen"
264279
)
280+
static let scheduleAccessibilityHint = NSLocalizedString(
281+
"blazeBudgetSettingView.scheduleAccessibilityHint",
282+
value: "Opens campaign schedule settings",
283+
comment: "Accessibility hint for the schedule section on the Blaze budget setting screen"
284+
)
265285
}
266286
}
267287

WooCommerce/Classes/ViewRelated/Blaze/BudgetSetting/BlazeBudgetSettingViewModel.swift

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,28 @@ final class BlazeBudgetSettingViewModel: ObservableObject {
148148
locale: locale,
149149
result.totalImpressionsMin,
150150
result.totalImpressionsMax)
151-
forecastedImpressionState = .result(formattedResult: formattedImpressions)
151+
forecastedImpressionState = .result(
152+
formattedResult: formattedImpressions,
153+
minValue: result.totalImpressionsMin,
154+
maxValue: result.totalImpressionsMax
155+
)
152156
} catch {
153157
DDLogError("⛔️ Error fetching forecasted impression: \(error)")
154158
forecastedImpressionState = .failure
155159
}
156160
}
161+
162+
/// Provides a combined accessibility label for the entire impressions section
163+
var impressionsSectionAccessibilityLabel: String {
164+
switch forecastedImpressionState {
165+
case .result(_, let minValue, let maxValue):
166+
return String.localizedStringWithFormat(Localization.impressionsSectionAccessibility, minValue, maxValue)
167+
case .loading:
168+
return Localization.impressionsLoading
169+
case .failure:
170+
return Localization.impressionsFailure
171+
}
172+
}
157173
}
158174

159175
// MARK: - Private helpers
@@ -227,7 +243,7 @@ extension BlazeBudgetSettingViewModel {
227243

228244
enum ForecastedImpressionState: Equatable {
229245
case loading
230-
case result(formattedResult: String)
246+
case result(formattedResult: String, minValue: Int64, maxValue: Int64)
231247
case failure
232248
}
233249

@@ -286,5 +302,21 @@ extension BlazeBudgetSettingViewModel {
286302
comment: "The total amount for weekly spend on the Blaze budget setting screen. " +
287303
"Reads like: $35 USD weekly spend"
288304
)
305+
static let impressionsSectionAccessibility = NSLocalizedString(
306+
"blazeBudgetSettingViewModel.impressionsSectionAccessibility",
307+
value: "Estimated total impressions range is from %1$lld to %2$lld",
308+
comment: "The formatted estimated impression range for a Blaze campaign. " +
309+
"Reads like: Estimated total impressions range is from 26100 to 35300"
310+
)
311+
static let impressionsLoading = NSLocalizedString(
312+
"blazeBudgetSettingViewModel.impressionsLoading",
313+
value: "Loading...",
314+
comment: "The label for loading impressions"
315+
)
316+
static let impressionsFailure = NSLocalizedString(
317+
"blazeBudgetSettingViewModel.impressionsFailure",
318+
value: "Failed to load impressions",
319+
comment: "The label for failed to load impressions"
320+
)
289321
}
290322
}

WooCommerce/Classes/ViewRelated/Blaze/BudgetSetting/BlazeScheduleSettingView.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ struct BlazeScheduleSettingView: View {
4343
AdaptiveStack(horizontalAlignment: .leading) {
4444
Text(Localization.startDate)
4545
.bodyStyle()
46+
.accessibilityHidden(true)
4647

4748
Spacer().renderedIf(sizeCategory.isAccessibilityCategory == false)
4849

@@ -58,6 +59,29 @@ struct BlazeScheduleSettingView: View {
5859
view.datePickerStyle(.compact)
5960
}
6061
}
62+
.accessibilityHint(Localization.startDateAccessibilityHint)
63+
.accessibilityLabel(
64+
String(
65+
format: Localization.startDateAccessibilityLabel,
66+
DateFormatter.localizedString(
67+
from: startDate,
68+
dateStyle: .medium,
69+
timeStyle: .none
70+
)
71+
)
72+
)
73+
// Apply accessibility grouping only for non-accessibility size categories.
74+
// For accessibility size categories, we use .graphical date picker style which is embedded
75+
// directly in the view and maintains its native "date picker" traits. For standard size
76+
// categories, we use .compact date picker style which acts as a popover, so we group the
77+
// elements with .combine and add .isButton trait to make the entire section actionable.
78+
// Applying .combine to the graphical date picker would override its native accessibility
79+
// traits and degrade the user experience.
80+
.if(!sizeCategory.isAccessibilityCategory) { view in
81+
view
82+
.accessibilityElement(children: .combine)
83+
.accessibilityAddTraits(.isButton)
84+
}
6185

6286
// Toggle to switch between evergreen and not. Hidden under a feature flag.
6387
Toggle(Localization.specifyDuration, isOn: $hasEndDate)
@@ -83,6 +107,9 @@ struct BlazeScheduleSettingView: View {
83107
in: dayCountSliderRange,
84108
step: Double(BlazeBudgetSettingViewModel.Constants.dayCountSliderStep))
85109
}
110+
.accessibilityElement(children: .combine)
111+
.accessibilityLabel(Localization.duration)
112+
.accessibilityValue(durationTextFormatter(duration).string)
86113
.renderedIf(hasEndDate)
87114

88115
Spacer()
@@ -154,6 +181,16 @@ private extension BlazeScheduleSettingView {
154181
value: "Cancel",
155182
comment: "Button to dismiss the Blaze schedule setting screen"
156183
)
184+
static let startDateAccessibilityLabel = NSLocalizedString(
185+
"blazeScheduleSettingView.startDateAccessibilityLabel",
186+
value: "Campaign start date is %@",
187+
comment: "Accessibility label for the start date picker on the Blaze campaign duration setting screen. %@ is replaced with the selected date."
188+
)
189+
static let startDateAccessibilityHint = NSLocalizedString(
190+
"blazeScheduleSettingView.startDateAccessibilityHint",
191+
value: "Double tap to edit the campaign start date",
192+
comment: "Accessibility hint for the start date picker on the Blaze campaign duration setting screen"
193+
)
157194
}
158195
}
159196

WooCommerce/WooCommerceTests/ViewRelated/Blaze/BlazeBudgetSettingViewModelTests.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,14 @@ final class BlazeBudgetSettingViewModelTests: XCTestCase {
150150
await viewModel.updateImpressions(startDate: .now, dayCount: 3, dailyBudget: 15)
151151

152152
// Then
153-
XCTAssertEqual(viewModel.forecastedImpressionState, .result(formattedResult: "1,000 - 5,000"))
153+
XCTAssertEqual(
154+
viewModel.forecastedImpressionState,
155+
.result(
156+
formattedResult: "1,000 - 5,000",
157+
minValue: 1000,
158+
maxValue: 5000
159+
)
160+
)
154161
}
155162

156163
@MainActor

0 commit comments

Comments
 (0)