Skip to content

Commit 4d4e2cd

Browse files
committed
Enhance accessibility in POSOrderDetailsView
Adds accessibility labels, traits, and hints to key UI elements in POSOrderDetailsView for improved VoiceOver support. Introduces helper methods and localized strings to provide descriptive accessibility information for headers, product rows, totals, payments, refunds, and actions.
1 parent 59b0ca2 commit 4d4e2cd

File tree

1 file changed

+185
-6
lines changed

1 file changed

+185
-6
lines changed

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)