Skip to content

Commit 7cc6ca8

Browse files
authored
Merge pull request #5517 from woocommerce/issue/5408-order-creation-products-ui
Order Creation: Products section UI
2 parents 935914b + fbb4e85 commit 7cc6ca8

File tree

4 files changed

+220
-9
lines changed

4 files changed

+220
-9
lines changed

WooCommerce/Classes/Extensions/UIImage+Woo.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,18 @@ extension UIImage {
667667
return UIImage(systemName: "plus", withConfiguration: Configurations.barButtonItemSymbol)!
668668
}
669669

670+
/// Small Plus Icon
671+
///
672+
static var plusSmallImage: UIImage {
673+
return UIImage.gridicon(.plusSmall)
674+
}
675+
676+
/// Small Minus Icon
677+
///
678+
static var minusSmallImage: UIImage {
679+
return UIImage.gridicon(.minusSmall)
680+
}
681+
670682
/// Search Icon - used in `UIBarButtonItem`
671683
///
672684
static var searchBarButtonItemImage: UIImage {

WooCommerce/Classes/ViewRelated/Orders/Order Creation/NewOrder.swift

Lines changed: 169 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ final class NewOrderHostingController: UIHostingController<NewOrder> {
2929
.sink { [weak self] notice in
3030
switch notice {
3131
case .error:
32-
self?.noticePresenter.enqueue(notice: .init(title: Localization.errorMessage, feedbackType: .error))
32+
self?.noticePresenter.enqueue(notice: .init(title: NewOrder.Localization.errorMessage, feedbackType: .error))
3333
}
3434

3535
// Nullify the presentation intent.
@@ -45,11 +45,17 @@ struct NewOrder: View {
4545
@ObservedObject var viewModel: NewOrderViewModel
4646

4747
var body: some View {
48-
ScrollView {
49-
EmptyView()
50-
}
51-
.background(Color(.listBackground))
48+
GeometryReader { geometry in
49+
ScrollView {
50+
VStack(spacing: Layout.noSpacing) {
51+
Spacer(minLength: Layout.sectionSpacing)
52+
53+
ProductsSection(geometry: geometry)
54+
}
55+
}
56+
.background(Color(.listBackground))
5257
.ignoresSafeArea(.container, edges: [.horizontal, .bottom])
58+
}
5359
.navigationTitle(Localization.title)
5460
.navigationBarTitleDisplayMode(.inline)
5561
.toolbar {
@@ -70,11 +76,147 @@ struct NewOrder: View {
7076
}
7177
}
7278

79+
// MARK: Order Sections
80+
/// Represents the Products section
81+
///
82+
private struct ProductsSection: View {
83+
let geometry: GeometryProxy
84+
85+
var body: some View {
86+
Group {
87+
Divider()
88+
89+
VStack(alignment: .leading, spacing: NewOrder.Layout.verticalSpacing) {
90+
Text(NewOrder.Localization.products)
91+
.headlineStyle()
92+
93+
// TODO: Add a product row for each product added to the order
94+
ProductRow()
95+
96+
Button(NewOrder.Localization.addProduct) {
97+
// TODO: Open Add Product modal view
98+
}
99+
.buttonStyle(PlusButtonStyle())
100+
}
101+
.padding(.horizontal, insets: geometry.safeAreaInsets)
102+
.padding()
103+
.background(Color(.listForeground))
104+
105+
Divider()
106+
}
107+
}
108+
109+
/// Represent a single product row in the Product section
110+
///
111+
struct ProductRow: View {
112+
113+
// Tracks the scale of the view due to accessibility changes
114+
@ScaledMetric private var scale: CGFloat = 1
115+
116+
var body: some View {
117+
AdaptiveStack(horizontalAlignment: .leading) {
118+
HStack(alignment: .top) {
119+
// Product image
120+
// TODO: Display actual product image when available
121+
Image(uiImage: .productPlaceholderImage)
122+
.resizable()
123+
.aspectRatio(contentMode: .fill)
124+
.frame(width: NewOrder.Layout.productImageSize * scale, height: NewOrder.Layout.productImageSize * scale)
125+
.foregroundColor(Color(UIColor.listSmallIcon))
126+
.accessibilityHidden(true)
127+
128+
// Product details
129+
VStack(alignment: .leading) {
130+
Text("Love Ficus") // Fake data - product name
131+
Text("7 in stock • $20.00") // Fake data - stock / price
132+
.subheadlineStyle()
133+
Text("SKU: 123456") // Fake data - SKU
134+
.subheadlineStyle()
135+
}
136+
.accessibilityElement(children: .combine)
137+
}
138+
139+
Spacer()
140+
141+
ProductStepper()
142+
}
143+
144+
Divider()
145+
}
146+
}
147+
148+
/// Represents a custom stepper.
149+
/// Used to change the product quantity in the order.
150+
///
151+
struct ProductStepper: View {
152+
153+
// Tracks the scale of the view due to accessibility changes
154+
@ScaledMetric private var scale: CGFloat = 1
155+
156+
var body: some View {
157+
HStack(spacing: NewOrder.Layout.stepperSpacing * scale) {
158+
Button {
159+
// TODO: Decrement the product quantity
160+
} label: {
161+
Image(uiImage: .minusSmallImage)
162+
.resizable()
163+
.aspectRatio(contentMode: .fit)
164+
.frame(height: NewOrder.Layout.stepperButtonSize * scale)
165+
}
166+
167+
Text("1") // Fake data - quantity
168+
169+
Button {
170+
// TODO: Increment the product quantity
171+
} label: {
172+
Image(uiImage: .plusSmallImage)
173+
.resizable()
174+
.aspectRatio(contentMode: .fit)
175+
.frame(height: NewOrder.Layout.stepperButtonSize * scale)
176+
}
177+
}
178+
.padding(NewOrder.Layout.stepperSpacing/2 * scale)
179+
.overlay(
180+
RoundedRectangle(cornerRadius: NewOrder.Layout.stepperBorderRadius)
181+
.stroke(Color(UIColor.separator), lineWidth: NewOrder.Layout.stepperBorderWidth)
182+
)
183+
.accessibilityElement(children: .ignore)
184+
.accessibility(label: Text("Quantity"))
185+
.accessibility(value: Text("1")) // Fake static data - quantity
186+
.accessibilityAdjustableAction { direction in
187+
switch direction {
188+
case .decrement:
189+
break // TODO: Decrement the product quantity
190+
case .increment:
191+
break // TODO: Increment the product quantity
192+
@unknown default:
193+
break
194+
}
195+
}
196+
}
197+
}
198+
}
199+
73200
// MARK: Constants
74-
private enum Localization {
75-
static let title = NSLocalizedString("New Order", comment: "Title for the order creation screen")
76-
static let createButton = NSLocalizedString("Create", comment: "Button to create an order on the New Order screen")
77-
static let errorMessage = NSLocalizedString("Unable to create new order", comment: "Notice displayed when order creation fails")
201+
private extension NewOrder {
202+
enum Layout {
203+
static let sectionSpacing: CGFloat = 16.0
204+
static let verticalSpacing: CGFloat = 22.0
205+
static let noSpacing: CGFloat = 0.0
206+
static let productImageSize: CGFloat = 44.0
207+
static let stepperBorderWidth: CGFloat = 1.0
208+
static let stepperBorderRadius: CGFloat = 4.0
209+
static let stepperButtonSize: CGFloat = 22.0
210+
static let stepperSpacing: CGFloat = 22.0
211+
}
212+
213+
enum Localization {
214+
static let title = NSLocalizedString("New Order", comment: "Title for the order creation screen")
215+
static let createButton = NSLocalizedString("Create", comment: "Button to create an order on the New Order screen")
216+
static let errorMessage = NSLocalizedString("Unable to create new order", comment: "Notice displayed when order creation fails")
217+
static let products = NSLocalizedString("Products", comment: "Title text of the section that shows the Products when creating a new order")
218+
static let addProduct = NSLocalizedString("Add product", comment: "Title text of the button that adds a product when creating a new order")
219+
}
78220
}
79221

80222
struct NewOrder_Previews: PreviewProvider {
@@ -84,5 +226,23 @@ struct NewOrder_Previews: PreviewProvider {
84226
NavigationView {
85227
NewOrder(viewModel: viewModel)
86228
}
229+
230+
NavigationView {
231+
NewOrder(viewModel: viewModel)
232+
}
233+
.environment(\.sizeCategory, .accessibilityExtraExtraLarge)
234+
.previewDisplayName("Accessibility")
235+
236+
NavigationView {
237+
NewOrder(viewModel: viewModel)
238+
}
239+
.environment(\.colorScheme, .dark)
240+
.previewDisplayName("Dark")
241+
242+
NavigationView {
243+
NewOrder(viewModel: viewModel)
244+
}
245+
.environment(\.layoutDirection, .rightToLeft)
246+
.previewDisplayName("Right to left")
87247
}
88248
}

WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ButtonStyles.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ struct LinkButtonStyle: ButtonStyle {
2323
}
2424
}
2525

26+
struct PlusButtonStyle: ButtonStyle {
27+
func makeBody(configuration: Configuration) -> some View {
28+
return PlusButton(configuration: configuration)
29+
}
30+
}
31+
2632
/// Adds a primary button style while showing a progress view on top of the button when required.
2733
///
2834
struct PrimaryLoadingButtonStyle: PrimitiveButtonStyle {
@@ -191,6 +197,28 @@ private struct LinkButton: View {
191197
}
192198
}
193199

200+
private struct PlusButton: View {
201+
let configuration: ButtonStyleConfiguration
202+
203+
var body: some View {
204+
HStack {
205+
Label {
206+
configuration.label
207+
} icon: {
208+
Image(uiImage: .plusImage)
209+
}
210+
Spacer()
211+
}
212+
.frame(maxWidth: .infinity)
213+
.foregroundColor(Color(foregroundColor))
214+
.background(Color(.clear))
215+
}
216+
217+
var foregroundColor: UIColor {
218+
configuration.isPressed ? .accentDark : .accent
219+
}
220+
}
221+
194222
private enum Style {
195223
static let defaultCornerRadius = CGFloat(8.0)
196224
static let defaultBorderWidth = CGFloat(1.0)
@@ -216,6 +244,9 @@ struct PrimaryButton_Previews: PreviewProvider {
216244

217245
Button("Link button") {}
218246
.buttonStyle(LinkButtonStyle())
247+
248+
Button("Plus button") {}
249+
.buttonStyle(PlusButtonStyle())
219250
}
220251
.padding()
221252
}

WooCommerce/WooCommerceTests/Extensions/IconsTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ final class IconsTests: XCTestCase {
246246
XCTAssertNotNil(UIImage.mailImage)
247247
}
248248

249+
func test_minus_small_image_is_not_nil() {
250+
XCTAssertNotNil(UIImage.minusSmallImage)
251+
}
252+
249253
func testMoreImageIconIsNotNil() {
250254
XCTAssertNotNil(UIImage.moreImage)
251255
}
@@ -318,6 +322,10 @@ final class IconsTests: XCTestCase {
318322
XCTAssertNotNil(UIImage.plusBarButtonItemImage)
319323
}
320324

325+
func test_plus_small_image_is_not_nil() {
326+
XCTAssertNotNil(UIImage.plusSmallImage)
327+
}
328+
321329
func testPriceImageIconIsNotNil() {
322330
XCTAssertNotNil(UIImage.priceImage)
323331
}

0 commit comments

Comments
 (0)