From 7641259529322aa051ed026aacb91826d367b923 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:59:01 +0300 Subject: [PATCH 1/4] Add accessibility label to order badge view Introduces an accessibility label to POSOrderBadgeView using a localized string for improved accessibility. Adds a Localization helper to format the label with the order status. --- .../Presentation/Orders/POSOrderBadgeView.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/WooCommerce/Classes/POS/Presentation/Orders/POSOrderBadgeView.swift b/WooCommerce/Classes/POS/Presentation/Orders/POSOrderBadgeView.swift index 119f42ffb51..280bb764d2a 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/POSOrderBadgeView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/POSOrderBadgeView.swift @@ -17,6 +17,7 @@ struct POSOrderBadgeView: View { .padding(.vertical, POSPadding.xSmall) .background(statusBackgroundColor) .clipShape(RoundedRectangle(cornerRadius: POSCornerRadiusStyle.small.value)) + .accessibilityLabel(Localization.badgeAccessibilityLabel(status: order.status.localizedName)) } private var statusBackgroundColor: Color { @@ -34,3 +35,16 @@ struct POSOrderBadgeView: View { } } } + +private extension POSOrderBadgeView { + enum Localization { + static func badgeAccessibilityLabel(status: String) -> String { + let format = NSLocalizedString( + "pos.orderBadgeView.accessibilityLabel", + value: "Order status: %1$@", + comment: "Accessibility label for order status badge. %1$@ is the status name (e.g., Completed, Failed, Processing)." + ) + return String(format: format, status) + } + } +} From a2cbf0bff0dea0fbfb04a17fa413d787829331fa Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:59:32 +0300 Subject: [PATCH 2/4] 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. --- .../Orders/POSOrderDetailsView.swift | 191 +++++++++++++++++- 1 file changed, 185 insertions(+), 6 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/Orders/POSOrderDetailsView.swift b/WooCommerce/Classes/POS/Presentation/Orders/POSOrderDetailsView.swift index 0244ce864d7..7f4b67279c3 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/POSOrderDetailsView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/POSOrderDetailsView.swift @@ -83,6 +83,7 @@ private extension POSOrderDetailsView { Text(Localization.productsTitle) .font(.posBodyLargeBold) .foregroundStyle(Color.posOnSurface) + .accessibilityAddTraits(.isHeader) VStack(spacing: POSSpacing.small) { ForEach(Array(order.lineItems.enumerated()), id: \.element.itemID) { index, item in @@ -105,6 +106,7 @@ private extension POSOrderDetailsView { Text(Localization.totalsTitle) .font(.posBodyLargeBold) .foregroundStyle(Color.posOnSurface) + .accessibilityAddTraits(.isHeader) VStack(spacing: POSSpacing.small) { productsSubtotalRow(order) @@ -152,6 +154,20 @@ private extension POSOrderDetailsView { } .padding(.top, POSSpacing.xSmall) .multilineTextAlignment(.leading) + .accessibilityElement(children: .combine) + .accessibilityLabel(headerBottomContentAccessibilityLabel(for: order)) + } + + private func headerBottomContentAccessibilityLabel(for order: POSOrder) -> String { + let date = dateFormatter.string(from: order.dateCreated) + let email = order.customerEmail + let status = order.status.localizedName + + return Localization.headerBottomContentAccessibilityLabel( + date: date, + email: email, + status: status + ) } } @@ -167,6 +183,19 @@ private extension POSOrderDetailsView { Spacer() productTotalView(item: item) } + .accessibilityElement(children: .combine) + .accessibilityLabel(productRowAccessibilityLabel(for: item)) + } + + private func productRowAccessibilityLabel(for item: POSOrderItem) -> String { + let attributesText = item.attributes.isEmpty ? nil : item.attributes.map { "\($0.name): \($0.value)" }.joined(separator: ", ") + return Localization.productRowAccessibilityLabel( + name: item.name, + attributes: attributesText, + quantity: String(item.quantity.intValue), + unitPrice: item.formattedPrice, + total: item.formattedTotal + ) } @ViewBuilder @@ -219,7 +248,8 @@ private extension POSOrderDetailsView { func productsSubtotalRow(_ order: POSOrder) -> some View { totalsRow( title: Localization.productsLabel, - amount: order.formattedSubtotal + amount: order.formattedSubtotal, + accessibilityLabel: Localization.subtotalAccessibilityLabel(order.formattedSubtotal) ) } @@ -228,7 +258,8 @@ private extension POSOrderDetailsView { if let formattedDiscountTotal = order.formattedDiscountTotal { totalsRow( title: Localization.discountTotalLabel, - amount: formattedDiscountTotal + amount: formattedDiscountTotal, + accessibilityLabel: Localization.discountAccessibilityLabel(formattedDiscountTotal) ) } } @@ -237,7 +268,8 @@ private extension POSOrderDetailsView { func taxTotalRow(_ order: POSOrder) -> some View { totalsRow( title: Localization.taxesLabel, - amount: order.formattedTotalTax + amount: order.formattedTotalTax, + accessibilityLabel: Localization.taxAccessibilityLabel(order.formattedTotalTax) ) } @@ -247,7 +279,8 @@ private extension POSOrderDetailsView { title: Localization.totalLabel, amount: order.formattedTotal, titleColor: .posOnSurface, - titleFont: .posBodySmallBold + titleFont: .posBodySmallBold, + accessibilityLabel: Localization.totalAccessibilityLabel(order.formattedTotal) ) } @@ -267,6 +300,13 @@ private extension POSOrderDetailsView { .foregroundStyle(Color.posOnSurfaceVariantHighest) } } + .accessibilityElement(children: .combine) + .accessibilityLabel( + Localization.paidAccessibilityLabel( + amount: order.formattedPaymentTotal, + method: order.paymentMethodTitle + ) + ) } @ViewBuilder @@ -297,6 +337,8 @@ private extension POSOrderDetailsView { .foregroundStyle(Color.posOnSurfaceVariantHighest) } } + .accessibilityElement(children: .combine) + .accessibilityLabel(Localization.refundAccessibilityLabel(amount: refund.formattedTotal, reason: refund.reason)) } @ViewBuilder @@ -305,7 +347,8 @@ private extension POSOrderDetailsView { title: Localization.netPaymentLabel, amount: netAmount, titleColor: .posOnSurface, - titleFont: .posBodySmallBold + titleFont: .posBodySmallBold, + accessibilityLabel: Localization.netPaymentAccessibilityLabel(netAmount) ) } @@ -314,7 +357,8 @@ private extension POSOrderDetailsView { title: String, amount: String, titleColor: Color = .posOnSurfaceVariantHighest, - titleFont: POSFontStyle = .posBodySmallRegular() + titleFont: POSFontStyle = .posBodySmallRegular(), + accessibilityLabel: String? = nil ) -> some View { HStack { Text(title) @@ -325,6 +369,8 @@ private extension POSOrderDetailsView { .font(.posBodySmallRegular()) .foregroundStyle(Color.posOnSurface) } + .accessibilityElement(children: .combine) + .accessibilityLabel(accessibilityLabel ?? "\(title) \(amount)") } } @@ -371,11 +417,19 @@ private extension POSOrderDetailsView { .minimumScaleFactor(0.5) } .buttonStyle(POSFilledButtonStyle(size: .extraSmall)) + .accessibilityHint(accessibilityHint(for: action)) } } Spacer() } } + + private func accessibilityHint(for action: POSOrderDetailsAction) -> String { + switch action { + case .emailReceipt: + return Localization.emailReceiptAccessibilityHint + } + } } private extension POSOrderDetailsView { @@ -467,6 +521,131 @@ private enum Localization { value: "Email receipt", comment: "Label for email receipt action on order details view" ) + + static let emailReceiptAccessibilityHint = NSLocalizedString( + "pos.orderDetailsView.emailReceiptAction.accessibilityHint", + value: "Tap to send order receipt via email", + comment: "Accessibility hint for email receipt button on order details view" + ) + + static func headerBottomContentAccessibilityLabel(date: String, email: String?, status: String) -> String { + let baseFormat = NSLocalizedString( + "pos.orderDetailsView.headerBottomContent.accessibilityLabel", + value: "Order date: %1$@, Status: %2$@", + comment: "Accessibility label for order header bottom content. %1$@ is order date and time, %2$@ is order status." + ) + var label = String(format: baseFormat, date, status) + + if let email = email, email.isNotEmpty { + let emailFormat = NSLocalizedString( + "pos.orderDetailsView.headerBottomContent.accessibilityLabel.email", + value: "Customer email: %1$@", + comment: "Email portion of order header accessibility label. %1$@ is customer email address." + ) + label += ", " + String(format: emailFormat, email) + } + + return label + } + + static func productRowAccessibilityLabel(name: String, attributes: String?, quantity: String, unitPrice: String, total: String) -> String { + var label = name + if let attributes = attributes { + label += ", \(attributes)" + } + let format = NSLocalizedString( + "pos.orderDetailsView.productRow.accessibilityLabel", + value: "Quantity: %1$@ at %2$@ each, Total %3$@", + comment: "Accessibility label for product row. %1$@ is quantity, %2$@ is unit price, %3$@ is total price." + ) + label += ", " + String(format: format, quantity, unitPrice, total) + return label + } + + static func subtotalAccessibilityLabel(_ amount: String) -> String { + let format = NSLocalizedString( + "pos.orderDetailsView.subtotal.accessibilityLabel", + value: "Products subtotal: %1$@", + comment: "Accessibility label for products subtotal. %1$@ is the subtotal amount." + ) + return String(format: format, amount) + } + + static func discountAccessibilityLabel(_ amount: String) -> String { + let format = NSLocalizedString( + "pos.orderDetailsView.discount.accessibilityLabel", + value: "Discount total: %1$@", + comment: "Accessibility label for discount total. %1$@ is the discount amount." + ) + return String(format: format, amount) + } + + static func taxAccessibilityLabel(_ amount: String) -> String { + let format = NSLocalizedString( + "pos.orderDetailsView.tax.accessibilityLabel", + value: "Taxes: %1$@", + comment: "Accessibility label for taxes. %1$@ is the tax amount." + ) + return String(format: format, amount) + } + + static func totalAccessibilityLabel(_ amount: String) -> String { + let format = NSLocalizedString( + "pos.orderDetailsView.total.accessibilityLabel", + value: "Order total: %1$@", + comment: "Accessibility label for order total. %1$@ is the total amount." + ) + return String(format: format, amount) + } + + static func paidAccessibilityLabel(amount: String, method: String) -> String { + let baseFormat = NSLocalizedString( + "pos.orderDetailsView.paid.accessibilityLabel", + value: "Total paid: %1$@", + comment: "Accessibility label for total paid. %1$@ is the paid amount." + ) + var label = String(format: baseFormat, amount) + + if method.isNotEmpty { + let methodFormat = NSLocalizedString( + "pos.orderDetailsView.paid.accessibilityLabel.method", + value: "Payment method: %1$@", + comment: "Payment method portion of paid accessibility label. %1$@ is the payment method." + ) + label += ", " + String(format: methodFormat, method) + } + + return label + } + + static func refundAccessibilityLabel(amount: String, reason: String?) -> String { + let baseFormat = NSLocalizedString( + "pos.orderDetailsView.refund.accessibilityLabel", + value: "Refunded: %1$@", + comment: "Accessibility label for refunded amount. %1$@ is the refund amount." + ) + var label = String(format: baseFormat, amount) + + if let reason = reason, !reason.isEmpty { + let reasonFormat = NSLocalizedString( + "pos.orderDetailsView.refund.accessibilityLabel.reason", + value: "Reason: %1$@", + comment: "Reason portion of refund accessibility label. %1$@ is the refund reason." + ) + label += ", " + String(format: reasonFormat, reason) + } + + return label + } + + static func netPaymentAccessibilityLabel(_ amount: String) -> String { + let format = NSLocalizedString( + "pos.orderDetailsView.netPayment.accessibilityLabel", + value: "Net payment: %1$@", + comment: "Accessibility label for net payment. %1$@ is the net payment amount after refunds." + ) + return String(format: format, amount) + } } #if DEBUG From 03cc563dead15d58c93402eae6b5a8967c4feecf Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:59:40 +0300 Subject: [PATCH 3/4] Add accessibility labels and hints to order list views Introduces accessibility labels and hints for the search button and order rows in POSOrderListView to improve screen reader support. Ghost order rows are now hidden from accessibility. Localization keys for these labels and hints have been added. --- .../Orders/POSOrderListView.swift | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/WooCommerce/Classes/POS/Presentation/Orders/POSOrderListView.swift b/WooCommerce/Classes/POS/Presentation/Orders/POSOrderListView.swift index 894dda855ee..8f0e2959775 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/POSOrderListView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/POSOrderListView.swift @@ -44,6 +44,7 @@ struct POSOrderListView: View { analytics.track(event: WooAnalyticsEvent.PointOfSale.ordersListSearchButtonTapped()) setSearch(true) } + .accessibilityLabel(Localization.searchButtonAccessibilityLabel) .matchedGeometryEffect(id: Constants.searchControlID, in: searchTransition) .transition(.opacity.combined(with: .scale)) } @@ -218,6 +219,19 @@ private struct POSOrderRowView: View { .stroke(Color.posOnSurface, lineWidth: 2) } } + .accessibilityElement(children: .combine) + .accessibilityLabel(accessibilityLabel) + .accessibilityHint(POSOrderListView.Localization.orderRowAccessibilityHint) + } + + private var accessibilityLabel: String { + POSOrderListView.Localization.orderRowAccessibilityLabel( + orderNumber: order.number, + total: order.formattedTotal, + date: DateFormatter.dateAndTimeFormatter.string(from: order.dateCreated), + email: order.customerEmail, + status: order.status.localizedName + ) } @ViewBuilder @@ -281,6 +295,7 @@ private struct POSGhostOrderRowView: View { .background(Color.posSurfaceContainerLowest) .posItemCardBorderStyles() .geometryGroup() + .accessibilityHidden(true) } @ViewBuilder @@ -387,6 +402,39 @@ extension POSOrderListView { ) return String(format: format, orderNumber) } + + static func orderRowAccessibilityLabel(orderNumber: String, total: String, date: String, email: String?, status: String) -> String { + let baseFormat = NSLocalizedString( + "pos.orderListView.orderRow.accessibilityLabel", + value: "Order #%1$@, Total %2$@, %3$@, Status: %4$@", + comment: "Accessibility label for order row. %1$@ is order number, %2$@ is total amount, %3$@ is date and time, " + + "%4$@ is order status." + ) + var label = String(format: baseFormat, orderNumber, total, date, status) + + if let email = email, email.isNotEmpty { + let emailFormat = NSLocalizedString( + "pos.orderListView.orderRow.accessibilityLabel.email", + value: "Email: %1$@", + comment: "Email portion of order row accessibility label. %1$@ is customer email address." + ) + label += ", " + String(format: emailFormat, email) + } + + return label + } + + static let orderRowAccessibilityHint = NSLocalizedString( + "pos.orderListView.orderRow.accessibilityHint", + value: "Tap to view order details", + comment: "Accessibility hint for order row indicating the action when tapped." + ) + + static let searchButtonAccessibilityLabel = NSLocalizedString( + "pos.orderListView.searchButton.accessibilityLabel", + value: "Search orders", + comment: "Accessibility label for the search button in orders list." + ) } } From e7e407e8eb10cb3bed3be5c79971f61df2f7d213 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:34:01 +0300 Subject: [PATCH 4/4] Remove selected order when POSOrdersView disappears --- .../Classes/POS/Presentation/Orders/POSOrdersView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WooCommerce/Classes/POS/Presentation/Orders/POSOrdersView.swift b/WooCommerce/Classes/POS/Presentation/Orders/POSOrdersView.swift index 2ffb1edb147..e7887fe8f41 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/POSOrdersView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/POSOrdersView.swift @@ -56,6 +56,9 @@ struct POSOrdersView: View { .onAppear { analytics.track(event: WooAnalyticsEvent.PointOfSale.ordersListLoaded()) } + .onDisappear { + orderListModel.ordersController.selectOrder(nil) + } } }