Skip to content

Commit eaa33a0

Browse files
authored
[Woo POS][Historical Orders] VoiceOver and Accessibility (#16193)
2 parents b40ac0f + e7e407e commit eaa33a0

File tree

4 files changed

+250
-6
lines changed

4 files changed

+250
-6
lines changed

WooCommerce/Classes/POS/Presentation/Orders/POSOrderBadgeView.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ struct POSOrderBadgeView: View {
1717
.padding(.vertical, POSPadding.xSmall)
1818
.background(statusBackgroundColor)
1919
.clipShape(RoundedRectangle(cornerRadius: POSCornerRadiusStyle.small.value))
20+
.accessibilityLabel(Localization.badgeAccessibilityLabel(status: order.status.localizedName))
2021
}
2122

2223
private var statusBackgroundColor: Color {
@@ -34,3 +35,16 @@ struct POSOrderBadgeView: View {
3435
}
3536
}
3637
}
38+
39+
private extension POSOrderBadgeView {
40+
enum Localization {
41+
static func badgeAccessibilityLabel(status: String) -> String {
42+
let format = NSLocalizedString(
43+
"pos.orderBadgeView.accessibilityLabel",
44+
value: "Order status: %1$@",
45+
comment: "Accessibility label for order status badge. %1$@ is the status name (e.g., Completed, Failed, Processing)."
46+
)
47+
return String(format: format, status)
48+
}
49+
}
50+
}

WooCommerce/Classes/POS/Presentation/Orders/POSOrderDetailsView.swift

Lines changed: 185 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ private extension POSOrderDetailsView {
8383
Text(Localization.productsTitle)
8484
.font(.posBodyLargeBold)
8585
.foregroundStyle(Color.posOnSurface)
86+
.accessibilityAddTraits(.isHeader)
8687

8788
VStack(spacing: POSSpacing.small) {
8889
ForEach(Array(order.lineItems.enumerated()), id: \.element.itemID) { index, item in
@@ -105,6 +106,7 @@ private extension POSOrderDetailsView {
105106
Text(Localization.totalsTitle)
106107
.font(.posBodyLargeBold)
107108
.foregroundStyle(Color.posOnSurface)
109+
.accessibilityAddTraits(.isHeader)
108110

109111
VStack(spacing: POSSpacing.small) {
110112
productsSubtotalRow(order)
@@ -152,6 +154,20 @@ private extension POSOrderDetailsView {
152154
}
153155
.padding(.top, POSSpacing.xSmall)
154156
.multilineTextAlignment(.leading)
157+
.accessibilityElement(children: .combine)
158+
.accessibilityLabel(headerBottomContentAccessibilityLabel(for: order))
159+
}
160+
161+
private func headerBottomContentAccessibilityLabel(for order: POSOrder) -> String {
162+
let date = dateFormatter.string(from: order.dateCreated)
163+
let email = order.customerEmail
164+
let status = order.status.localizedName
165+
166+
return Localization.headerBottomContentAccessibilityLabel(
167+
date: date,
168+
email: email,
169+
status: status
170+
)
155171
}
156172

157173
}
@@ -167,6 +183,19 @@ private extension POSOrderDetailsView {
167183
Spacer()
168184
productTotalView(item: item)
169185
}
186+
.accessibilityElement(children: .combine)
187+
.accessibilityLabel(productRowAccessibilityLabel(for: item))
188+
}
189+
190+
private func productRowAccessibilityLabel(for item: POSOrderItem) -> String {
191+
let attributesText = item.attributes.isEmpty ? nil : item.attributes.map { "\($0.name): \($0.value)" }.joined(separator: ", ")
192+
return Localization.productRowAccessibilityLabel(
193+
name: item.name,
194+
attributes: attributesText,
195+
quantity: String(item.quantity.intValue),
196+
unitPrice: item.formattedPrice,
197+
total: item.formattedTotal
198+
)
170199
}
171200

172201
@ViewBuilder
@@ -219,7 +248,8 @@ private extension POSOrderDetailsView {
219248
func productsSubtotalRow(_ order: POSOrder) -> some View {
220249
totalsRow(
221250
title: Localization.productsLabel,
222-
amount: order.formattedSubtotal
251+
amount: order.formattedSubtotal,
252+
accessibilityLabel: Localization.subtotalAccessibilityLabel(order.formattedSubtotal)
223253
)
224254
}
225255

@@ -228,7 +258,8 @@ private extension POSOrderDetailsView {
228258
if let formattedDiscountTotal = order.formattedDiscountTotal {
229259
totalsRow(
230260
title: Localization.discountTotalLabel,
231-
amount: formattedDiscountTotal
261+
amount: formattedDiscountTotal,
262+
accessibilityLabel: Localization.discountAccessibilityLabel(formattedDiscountTotal)
232263
)
233264
}
234265
}
@@ -237,7 +268,8 @@ private extension POSOrderDetailsView {
237268
func taxTotalRow(_ order: POSOrder) -> some View {
238269
totalsRow(
239270
title: Localization.taxesLabel,
240-
amount: order.formattedTotalTax
271+
amount: order.formattedTotalTax,
272+
accessibilityLabel: Localization.taxAccessibilityLabel(order.formattedTotalTax)
241273
)
242274
}
243275

@@ -247,7 +279,8 @@ private extension POSOrderDetailsView {
247279
title: Localization.totalLabel,
248280
amount: order.formattedTotal,
249281
titleColor: .posOnSurface,
250-
titleFont: .posBodySmallBold
282+
titleFont: .posBodySmallBold,
283+
accessibilityLabel: Localization.totalAccessibilityLabel(order.formattedTotal)
251284
)
252285
}
253286

@@ -267,6 +300,13 @@ private extension POSOrderDetailsView {
267300
.foregroundStyle(Color.posOnSurfaceVariantHighest)
268301
}
269302
}
303+
.accessibilityElement(children: .combine)
304+
.accessibilityLabel(
305+
Localization.paidAccessibilityLabel(
306+
amount: order.formattedPaymentTotal,
307+
method: order.paymentMethodTitle
308+
)
309+
)
270310
}
271311

272312
@ViewBuilder
@@ -297,6 +337,8 @@ private extension POSOrderDetailsView {
297337
.foregroundStyle(Color.posOnSurfaceVariantHighest)
298338
}
299339
}
340+
.accessibilityElement(children: .combine)
341+
.accessibilityLabel(Localization.refundAccessibilityLabel(amount: refund.formattedTotal, reason: refund.reason))
300342
}
301343

302344
@ViewBuilder
@@ -305,7 +347,8 @@ private extension POSOrderDetailsView {
305347
title: Localization.netPaymentLabel,
306348
amount: netAmount,
307349
titleColor: .posOnSurface,
308-
titleFont: .posBodySmallBold
350+
titleFont: .posBodySmallBold,
351+
accessibilityLabel: Localization.netPaymentAccessibilityLabel(netAmount)
309352
)
310353
}
311354

@@ -314,7 +357,8 @@ private extension POSOrderDetailsView {
314357
title: String,
315358
amount: String,
316359
titleColor: Color = .posOnSurfaceVariantHighest,
317-
titleFont: POSFontStyle = .posBodySmallRegular()
360+
titleFont: POSFontStyle = .posBodySmallRegular(),
361+
accessibilityLabel: String? = nil
318362
) -> some View {
319363
HStack {
320364
Text(title)
@@ -325,6 +369,8 @@ private extension POSOrderDetailsView {
325369
.font(.posBodySmallRegular())
326370
.foregroundStyle(Color.posOnSurface)
327371
}
372+
.accessibilityElement(children: .combine)
373+
.accessibilityLabel(accessibilityLabel ?? "\(title) \(amount)")
328374
}
329375
}
330376

@@ -371,11 +417,19 @@ private extension POSOrderDetailsView {
371417
.minimumScaleFactor(0.5)
372418
}
373419
.buttonStyle(POSFilledButtonStyle(size: .extraSmall))
420+
.accessibilityHint(accessibilityHint(for: action))
374421
}
375422
}
376423
Spacer()
377424
}
378425
}
426+
427+
private func accessibilityHint(for action: POSOrderDetailsAction) -> String {
428+
switch action {
429+
case .emailReceipt:
430+
return Localization.emailReceiptAccessibilityHint
431+
}
432+
}
379433
}
380434

381435
private extension POSOrderDetailsView {
@@ -467,6 +521,131 @@ private enum Localization {
467521
value: "Email receipt",
468522
comment: "Label for email receipt action on order details view"
469523
)
524+
525+
static let emailReceiptAccessibilityHint = NSLocalizedString(
526+
"pos.orderDetailsView.emailReceiptAction.accessibilityHint",
527+
value: "Tap to send order receipt via email",
528+
comment: "Accessibility hint for email receipt button on order details view"
529+
)
530+
531+
static func headerBottomContentAccessibilityLabel(date: String, email: String?, status: String) -> String {
532+
let baseFormat = NSLocalizedString(
533+
"pos.orderDetailsView.headerBottomContent.accessibilityLabel",
534+
value: "Order date: %1$@, Status: %2$@",
535+
comment: "Accessibility label for order header bottom content. %1$@ is order date and time, %2$@ is order status."
536+
)
537+
var label = String(format: baseFormat, date, status)
538+
539+
if let email = email, email.isNotEmpty {
540+
let emailFormat = NSLocalizedString(
541+
"pos.orderDetailsView.headerBottomContent.accessibilityLabel.email",
542+
value: "Customer email: %1$@",
543+
comment: "Email portion of order header accessibility label. %1$@ is customer email address."
544+
)
545+
label += ", " + String(format: emailFormat, email)
546+
}
547+
548+
return label
549+
}
550+
551+
static func productRowAccessibilityLabel(name: String, attributes: String?, quantity: String, unitPrice: String, total: String) -> String {
552+
var label = name
553+
if let attributes = attributes {
554+
label += ", \(attributes)"
555+
}
556+
let format = NSLocalizedString(
557+
"pos.orderDetailsView.productRow.accessibilityLabel",
558+
value: "Quantity: %1$@ at %2$@ each, Total %3$@",
559+
comment: "Accessibility label for product row. %1$@ is quantity, %2$@ is unit price, %3$@ is total price."
560+
)
561+
label += ", " + String(format: format, quantity, unitPrice, total)
562+
return label
563+
}
564+
565+
static func subtotalAccessibilityLabel(_ amount: String) -> String {
566+
let format = NSLocalizedString(
567+
"pos.orderDetailsView.subtotal.accessibilityLabel",
568+
value: "Products subtotal: %1$@",
569+
comment: "Accessibility label for products subtotal. %1$@ is the subtotal amount."
570+
)
571+
return String(format: format, amount)
572+
}
573+
574+
static func discountAccessibilityLabel(_ amount: String) -> String {
575+
let format = NSLocalizedString(
576+
"pos.orderDetailsView.discount.accessibilityLabel",
577+
value: "Discount total: %1$@",
578+
comment: "Accessibility label for discount total. %1$@ is the discount amount."
579+
)
580+
return String(format: format, amount)
581+
}
582+
583+
static func taxAccessibilityLabel(_ amount: String) -> String {
584+
let format = NSLocalizedString(
585+
"pos.orderDetailsView.tax.accessibilityLabel",
586+
value: "Taxes: %1$@",
587+
comment: "Accessibility label for taxes. %1$@ is the tax amount."
588+
)
589+
return String(format: format, amount)
590+
}
591+
592+
static func totalAccessibilityLabel(_ amount: String) -> String {
593+
let format = NSLocalizedString(
594+
"pos.orderDetailsView.total.accessibilityLabel",
595+
value: "Order total: %1$@",
596+
comment: "Accessibility label for order total. %1$@ is the total amount."
597+
)
598+
return String(format: format, amount)
599+
}
600+
601+
static func paidAccessibilityLabel(amount: String, method: String) -> String {
602+
let baseFormat = NSLocalizedString(
603+
"pos.orderDetailsView.paid.accessibilityLabel",
604+
value: "Total paid: %1$@",
605+
comment: "Accessibility label for total paid. %1$@ is the paid amount."
606+
)
607+
var label = String(format: baseFormat, amount)
608+
609+
if method.isNotEmpty {
610+
let methodFormat = NSLocalizedString(
611+
"pos.orderDetailsView.paid.accessibilityLabel.method",
612+
value: "Payment method: %1$@",
613+
comment: "Payment method portion of paid accessibility label. %1$@ is the payment method."
614+
)
615+
label += ", " + String(format: methodFormat, method)
616+
}
617+
618+
return label
619+
}
620+
621+
static func refundAccessibilityLabel(amount: String, reason: String?) -> String {
622+
let baseFormat = NSLocalizedString(
623+
"pos.orderDetailsView.refund.accessibilityLabel",
624+
value: "Refunded: %1$@",
625+
comment: "Accessibility label for refunded amount. %1$@ is the refund amount."
626+
)
627+
var label = String(format: baseFormat, amount)
628+
629+
if let reason = reason, !reason.isEmpty {
630+
let reasonFormat = NSLocalizedString(
631+
"pos.orderDetailsView.refund.accessibilityLabel.reason",
632+
value: "Reason: %1$@",
633+
comment: "Reason portion of refund accessibility label. %1$@ is the refund reason."
634+
)
635+
label += ", " + String(format: reasonFormat, reason)
636+
}
637+
638+
return label
639+
}
640+
641+
static func netPaymentAccessibilityLabel(_ amount: String) -> String {
642+
let format = NSLocalizedString(
643+
"pos.orderDetailsView.netPayment.accessibilityLabel",
644+
value: "Net payment: %1$@",
645+
comment: "Accessibility label for net payment. %1$@ is the net payment amount after refunds."
646+
)
647+
return String(format: format, amount)
648+
}
470649
}
471650

472651
#if DEBUG

0 commit comments

Comments
 (0)