Skip to content

Commit d78ef58

Browse files
Handle part of missing VoiceOver accessibility in Shipping Labels flow (#15912)
2 parents 1aef4ef + aca7a6b commit d78ef58

File tree

13 files changed

+213
-43
lines changed

13 files changed

+213
-43
lines changed

RELEASE-NOTES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
22.9
55
-----
6+
- [*] Shipping Labels: Empty dimensions are no longer presented for product cards [https://github.com/woocommerce/woocommerce-ios/pull/15912]
7+
- [*] Shipping Labels: Improved VoiceOver accessibility [https://github.com/woocommerce/woocommerce-ios/pull/15912]
68
- [**] Order Details: Update Shipping Labels section for stores with Woo Shipping extension [https://github.com/woocommerce/woocommerce-ios/pull/15889]
79
- [*] Order List: New orders made through Point of Sale are now filterable via the Order List filters menu [https://github.com/woocommerce/woocommerce-ios/pull/15910]
810
- [*] Shipping Labels: Display base rate on selected shipping service cards [https://github.com/woocommerce/woocommerce-ios/pull/15916]

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/ShipmentDetails/WooShippingShipmentDetailsView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ struct WooShippingShipmentDetailsView: View {
1818

1919
WooShippingItems(itemsCountLabel: viewModel.itemsCountLabel,
2020
itemsDetailLabel: viewModel.itemsDetailLabel,
21-
items: viewModel.itemsRowViewModels)
21+
items: viewModel.itemsRowViewModels,
22+
itemsSummaryAccessibilityValue: viewModel.itemsSummaryAccessibilityValue)
2223

2324
WooShippingHazmatRow(selectedCategory: $viewModel.hazmatCategory,
2425
enabled: !viewModel.canViewLabel)

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/ShipmentDetails/WooShippingShipmentDetailsViewModel.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,18 @@ final class WooShippingShipmentDetailsViewModel: ObservableObject {
263263
}
264264
}
265265

266+
/// Accessor for manual collapsed product items section accessibility label
267+
extension WooShippingShipmentDetailsViewModel {
268+
var itemsSummaryAccessibilityValue: String {
269+
return String.localizedStringWithFormat(
270+
Localization.itemsSummaryAccessibilityFormat,
271+
shipment.quantity,
272+
shipment.weight,
273+
shipment.price
274+
)
275+
}
276+
}
277+
266278
private extension WooShippingShipmentDetailsViewModel {
267279
func observeAddresses(originAddressPublisher: AnyPublisher<WooShippingAddress?, Never>,
268280
destinationAddressPublisher: AnyPublisher<WooShippingAddress?, Never>) {
@@ -562,5 +574,13 @@ private extension WooShippingShipmentDetailsViewModel {
562574
comment: "Label for row showing the additional cost to require Saturday delivery " +
563575
"on the shipping label creation screen"
564576
)
577+
static let itemsSummaryAccessibilityFormat = NSLocalizedString(
578+
"shipping-labels.packages.items.summary.accessibility-label",
579+
value: "%1$@ with a total weight of %2$@ and a total price of %3$@",
580+
comment: "Accessibility label for the summary of product items in a shipment." +
581+
" The %1$@ is items count." +
582+
" The %2$@ is total weight." +
583+
" The %3$@ is total price."
584+
)
565585
}
566586
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Foundation
2+
3+
/// Reusable accessibility builder for `WooShippingItemRow` and `SelectableShipmentItemRo`
4+
enum ShippingItemRowAccessibility {
5+
static func accessibilityValue(
6+
itemName: String,
7+
quantity: String,
8+
details: String,
9+
weight: String,
10+
price: String
11+
) -> String {
12+
/// Covers item name and quantity if plural
13+
let quantifiedItemFormattedString: String
14+
if let quantityIntValue = Int(quantity), quantityIntValue > 1 {
15+
quantifiedItemFormattedString = String(
16+
format: Localization.accessibilityValueQuantityFormat,
17+
quantityIntValue,
18+
itemName
19+
)
20+
} else {
21+
quantifiedItemFormattedString = itemName
22+
}
23+
24+
return quantifiedItemFormattedString + ". " + String(
25+
format: Localization.accessibilityValueFormat,
26+
details,
27+
weight,
28+
price
29+
)
30+
}
31+
32+
enum Localization {
33+
static let accessibilityValueFormat = NSLocalizedString(
34+
"shipping_item_row.accessibility_value.format",
35+
value: "%1$@, Weight: %2$@, Total price: %3$@",
36+
comment: "Accessibility value for a shipping item row." +
37+
" The %1$@ is details text." +
38+
" The %2$@ is weight." +
39+
" The %3$@ is a total price"
40+
)
41+
42+
static let accessibilityValueQuantityFormat = NSLocalizedString(
43+
"shipping_item_row.quantity.format",
44+
value: "%1$d items of %2$@",
45+
comment: "Format for plural item quantity." +
46+
" The %1$@ is a plural quantity." +
47+
" The %2$@ is the item name."
48+
)
49+
}
50+
}

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemRow.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@ struct WooShippingItemRow: View {
5151
}
5252
}
5353
.frame(maxWidth: .infinity)
54+
.accessibilityElement(children: .ignore)
55+
.accessibilityValue(accessibilityValue)
56+
}
57+
}
58+
59+
/// Custom accessibility
60+
private extension WooShippingItemRow {
61+
var accessibilityValue: String {
62+
return ShippingItemRowAccessibility.accessibilityValue(
63+
itemName: name,
64+
quantity: quantityLabel,
65+
details: detailsLabel,
66+
weight: weightLabel,
67+
price: priceLabel
68+
)
5469
}
5570
}
5671

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemRowViewModel.swift

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,23 @@ private extension ShippingLabelPackageItem {
7171

7272
/// Formats the item dimensions with the provided dimension unit.
7373
///
74-
func formatDimensions(with unit: String) -> String {
75-
String(format: Localization.dimensionsFormat, dimensions.length, dimensions.width, dimensions.height, unit)
74+
func formatDimensions(with unit: String) -> String? {
75+
var validDimensions = [String]()
76+
if dimensions.length.isNotEmpty {
77+
validDimensions.append(dimensions.length)
78+
}
79+
if dimensions.width.isNotEmpty {
80+
validDimensions.append(dimensions.width)
81+
}
82+
if dimensions.height.isNotEmpty {
83+
validDimensions.append(dimensions.height)
84+
}
85+
86+
if validDimensions.isEmpty {
87+
return nil
88+
}
89+
90+
return validDimensions.joined(separator: Constants.dimensionsFormatSeparator) + " " + unit
7691
}
7792

7893
/// Formats the total item weight (per-unit weight x quantity) with the provided weight unit.
@@ -91,11 +106,8 @@ private extension ShippingLabelPackageItem {
91106
return currencyFormatter.formatAmount(totalPrice, with: currency) ?? totalPrice.description
92107
}
93108

94-
enum Localization {
95-
static let dimensionsFormat = NSLocalizedString("wooShipping.createLabels.items.dimensions",
96-
value: "%1$@ x %2$@ x %3$@ %4$@",
97-
comment: "Length, width, and height dimensions with the unit for an item to ship. "
98-
+ "Reads like: '20 x 35 x 5 cm'")
109+
enum Constants {
110+
static let dimensionsFormatSeparator = " × "
99111
}
100112
}
101113

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItems.swift

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ struct WooShippingItems: View {
1111
/// View models for items to ship
1212
let items: [WooShippingItemRowViewModel]
1313

14+
/// Summary header accessibility label
15+
let itemsSummaryAccessibilityValue: String
16+
1417
/// Whether the item list is collapsed
1518
@State private var isCollapsed: Bool = true
1619

@@ -27,6 +30,10 @@ struct WooShippingItems: View {
2730
.foregroundStyle(Color(.textSubtle))
2831
}
2932
.padding(.vertical, Layout.textContainerAdditionalVerticalPadding)
33+
.accessibilityElement(children: .ignore)
34+
.accessibilityLabel(Localization.collapsibleHeaderAccessibilityLabel)
35+
.accessibilityValue(itemsSummaryAccessibilityValue)
36+
.accessibilityHint(isCollapsed ? Localization.expandHint : Localization.collapseHint)
3037
},
3138
content: {
3239
VStack {
@@ -41,7 +48,11 @@ struct WooShippingItems: View {
4148
.frame(maxWidth: .infinity, alignment: .center)
4249
.if(isCollapsed) { view in
4350
view
44-
.roundedBorder(cornerRadius: Layout.borderCornerRadius, lineColor: Color(.separator), lineWidth: Layout.borderWidth)
51+
.roundedBorder(
52+
cornerRadius: Layout.borderCornerRadius,
53+
lineColor: Color(.separator),
54+
lineWidth: Layout.borderWidth)
55+
.accessibilityElement(children: .combine)
4556
}
4657
}
4758
}
@@ -56,6 +67,24 @@ private extension WooShippingItems {
5667

5768
static let textContainerAdditionalVerticalPadding: CGFloat = 2
5869
}
70+
71+
private enum Localization {
72+
static let expandHint = NSLocalizedString(
73+
"shipping-labels.packages.items.expand.accessibility-hint",
74+
value: "Double-tap to show all items",
75+
comment: "Accessibility hint to expand the product items section"
76+
)
77+
static let collapseHint = NSLocalizedString(
78+
"shipping-labels.packages.items.collapse.accessibility-hint",
79+
value: "Double-tap to hide all items",
80+
comment: "Accessibility hint to collapse the product items section"
81+
)
82+
static let collapsibleHeaderAccessibilityLabel = NSLocalizedString(
83+
"shipping-labels.packages.items.header.accessibilityLabel",
84+
value: "Products section",
85+
comment: "Accessibility label for collapsible products section"
86+
)
87+
}
5988
}
6089

6190
#Preview {
@@ -72,5 +101,8 @@ private extension WooShippingItems {
72101
name: "Little Nap Brazil 250g",
73102
detailsLabel: "15×10×8cm • Espresso",
74103
weightLabel: "275g",
75-
priceLabel: "$60.00")])
104+
priceLabel: "$60.00")],
105+
itemsSummaryAccessibilityValue: "6 items with a total weight of 825g" +
106+
" and a total price of $135.00"
107+
)
76108
}

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemsViewModel.swift

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,3 @@ private extension WooShippingItemsViewModel {
9999
+ "Reads like: '20 x 35 x 5 cm'")
100100
}
101101
}
102-
103-
/// Convenience extension to provide data to `WooShippingItemRow`
104-
extension WooShippingItems {
105-
init(viewModel: WooShippingItemsViewModel) {
106-
self.itemsCountLabel = viewModel.itemsCountLabel
107-
self.itemsDetailLabel = viewModel.itemsDetailLabel
108-
self.items = viewModel.itemRows
109-
}
110-
}

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/SelectableShipmentItemRow.swift

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,40 +22,55 @@ struct SelectableShipmentItemRow: View {
2222
}
2323
}
2424

25-
ProductImageThumbnail(productImageURL: viewModel.item.imageUrl,
26-
productImageSize: Layout.imageSize,
27-
scale: scale,
28-
productImageCornerRadius: Layout.imageCornerRadius,
29-
foregroundColor: Color(UIColor.listSmallIcon))
30-
.overlay(alignment: .topTrailing) {
31-
if viewModel.showQuantity {
32-
BadgeView(text: viewModel.item.quantityLabel,
33-
customizations: .init(textColor: .white, backgroundColor: .black),
34-
backgroundShape: badgeStyle)
35-
.offset(x: Layout.badgeOffset, y: -Layout.badgeOffset)
25+
AdaptiveStack(spacing: Layout.horizontalSpacing) {
26+
ProductImageThumbnail(productImageURL: viewModel.item.imageUrl,
27+
productImageSize: Layout.imageSize,
28+
scale: scale,
29+
productImageCornerRadius: Layout.imageCornerRadius,
30+
foregroundColor: Color(UIColor.listSmallIcon))
31+
.overlay(alignment: .topTrailing) {
32+
if viewModel.showQuantity {
33+
BadgeView(text: viewModel.item.quantityLabel,
34+
customizations: .init(textColor: .white, backgroundColor: .black),
35+
backgroundShape: badgeStyle)
36+
.offset(x: Layout.badgeOffset, y: -Layout.badgeOffset)
37+
}
3638
}
37-
}
38-
VStack(alignment: .leading) {
39-
Text(viewModel.item.name)
40-
.bodyStyle()
41-
Text(viewModel.item.detailsLabel)
42-
.subheadlineStyle()
43-
AdaptiveStack(verticalAlignment: .lastTextBaseline) {
44-
Text(viewModel.item.weightLabel)
39+
40+
VStack(alignment: .leading) {
41+
Text(viewModel.item.name)
42+
.bodyStyle()
43+
Text(viewModel.item.detailsLabel)
4544
.subheadlineStyle()
46-
Spacer()
47-
Text(viewModel.item.priceLabel)
48-
.font(.subheadline)
49-
.foregroundStyle(Color(.text))
45+
AdaptiveStack(verticalAlignment: .lastTextBaseline) {
46+
Text(viewModel.item.weightLabel)
47+
.subheadlineStyle()
48+
Spacer()
49+
Text(viewModel.item.priceLabel)
50+
.font(.subheadline)
51+
.foregroundStyle(Color(.text))
52+
}
5053
}
5154
}
55+
.accessibilityElement(children: .ignore)
56+
.accessibilityValue(accessibilityValue)
5257
}
5358
.frame(maxWidth: .infinity)
5459
.opacity(isEnabled ? 1 : Layout.disabledOpacity)
5560
}
5661
}
5762

5863
private extension SelectableShipmentItemRow {
64+
var accessibilityValue: String {
65+
return ShippingItemRowAccessibility.accessibilityValue(
66+
itemName: viewModel.item.name,
67+
quantity: viewModel.item.quantityLabel,
68+
details: viewModel.item.detailsLabel,
69+
weight: viewModel.item.weightLabel,
70+
price: viewModel.item.priceLabel
71+
)
72+
}
73+
5974
@ViewBuilder
6075
func selectionCircle(selected: Bool) -> some View {
6176
if selected {

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsView.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ private extension WooShippingCreateLabelsView {
143143
viewModel.shipments.enumerated().map { (index, shipment) in
144144
TopTabItem(name: String.localizedStringWithFormat(Localization.shipmentFormat, index + 1),
145145
icon: shipment.isPurchased ? Layout.purchasedIcon : nil,
146+
customAccessibilityValue: shipment.isPurchased ?
147+
Localization.Accessibility.shipmentTabFulfilled :
148+
Localization.Accessibility.shipmentTabUnfulfilled,
146149
content: {
147150
EmptyView()
148151
})
@@ -178,6 +181,7 @@ private extension WooShippingCreateLabelsView {
178181
Image(systemName: "pencil")
179182
.padding(.horizontal)
180183
}
184+
.accessibilityHint(Localization.Accessibility.editButtonHint)
181185
.renderedIf(viewModel.hasUnfulfilledShipments)
182186
}
183187
.disabled(viewModel.isPurchasingLabel)
@@ -692,6 +696,21 @@ private extension WooShippingCreateLabelsView {
692696
value: "Close",
693697
comment: "Title of the button to dismiss the shipping label screen")
694698

699+
enum Accessibility {
700+
static let editButtonHint = NSLocalizedString(
701+
"wooShipping.createLabel.editButton.accessibility.hint",
702+
value: "Opens the shipments editing form.",
703+
comment: "Accessibility hint of the button to open the shipments editing form.")
704+
static let shipmentTabFulfilled = NSLocalizedString(
705+
"wooShipping.createLabel.shipmentTab.accessibility.value.fulfilled",
706+
value: "Fulfilled.",
707+
comment: "Accessibility value indicating that the shipment of a tab is fulfilled.")
708+
static let shipmentTabUnfulfilled = NSLocalizedString(
709+
"wooShipping.createLabel.shipmentTab.accessibility.value.unfulfilled",
710+
value: "Unfulfilled.",
711+
comment: "Accessibility value indicating that the shipment of a tab is unfulfilled.")
712+
}
713+
695714
enum BottomSheet {
696715
static let shipmentDetails = NSLocalizedString("wooShipping.createLabels.bottomSheet.title",
697716
value: "Shipment details",

0 commit comments

Comments
 (0)