diff --git a/Backpack-SwiftUI/FieldSet/Classes/BPKFieldSet.swift b/Backpack-SwiftUI/FieldSet/Classes/BPKFieldSet.swift new file mode 100644 index 000000000..dc5a6ba5f --- /dev/null +++ b/Backpack-SwiftUI/FieldSet/Classes/BPKFieldSet.swift @@ -0,0 +1,184 @@ +/* + * Backpack - Skyscanner's Design System + * + * Copyright © 2023 Skyscanner Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import SwiftUI +import UIKit + +public protocol BPKFieldSetContentView: View { + associatedtype ContentView: BPKFieldSetContentView + + func inputState(_ state: BPKFieldSet.State) -> ContentView +} + +// swiftlint:disable line_length + +/// A component which wraps its content (view) and optionally adds a title, description and error label (depending on the field's state) around it. +/// Supported states are Default, and Error. The states are dispatched to the wrapped view. The wrapped view must conform to `BPKFieldSetStatusHandling` to ensure it can handle the dispatched state. +/// +/// Use `inputState(_ state: State)` to change the state of the field set. +/// +/// Use `accessibilityPrefix(_ prefix: String)` to add a prefix to each accessibilityIdentifier +/// that gets added to all fieldset's subcomponents + +// swiftlint:enable line_length + +public struct BPKFieldSet: View { + private var state: BPKFieldSet.State = .default + private let label: String? + private let content: Content + private let description: String? + private var accessibilityPrefix: String? + + public init( + label: String? = nil, + description: String? = nil, + content: () -> Content + ) { + self.label = label + self.description = description + self.content = content() + } + + public var body: some View { + VStack(alignment: .leading, spacing: .sm) { + labelView + content + .inputState(state) + .padding(.bottom, .sm) + .accessibilityIdentifier(accessibilityIdentifier(for: "wrapped_view")) + descriptionView + if case let .error(message) = state { + errorMessage(message) + .padding(.top, .sm) + } + } + } + + @ViewBuilder + private var labelView: some View { + if let label { + BPKText(label, style: .label2) + .lineLimit(nil) + .foregroundColor(state.labelColor) + .accessibilityIdentifier(accessibilityIdentifier(for: "label")) + } + } + + @ViewBuilder + private var descriptionView: some View { + if let description { + BPKText(description, style: .caption) + .lineLimit(nil) + .foregroundColor(state.descriptionColor) + .accessibilityIdentifier(accessibilityIdentifier(for: "descritpion")) + } + } + + private func errorMessage(_ message: String) -> some View { + HStack(spacing: .md) { + BPKIconView(.exclamationCircle) + .foregroundColor(.textErrorColor) + .accessibilityHidden(true) + BPKText(message, style: .caption) + .lineLimit(nil) + .foregroundColor(.textErrorColor) + .accessibilityIdentifier(accessibilityIdentifier(for: "error_message")) + } + } + + public func inputState(_ state: BPKFieldSet.State) -> BPKFieldSet { + var result = self + result.state = state + return result + } +} + +// MARK: - Accessibility + +extension BPKFieldSet { + public func accessibilityPrefix(_ prefix: String?) -> BPKFieldSet { + var result = self + result.accessibilityPrefix = prefix + return result + } + + private func accessibilityIdentifier(for label: String) -> String { + if let prefix = accessibilityPrefix { + return "\(prefix)_\(label)" + } + return "" + } +} + +// MARK: - Previews + +// swiftlint:disable closure_body_length +#Preview { + return ScrollView { + constructViews() + } + + @ViewBuilder + func constructViews() -> some View { + VStack(spacing: .base) { + BPKText("With Label & Description", style: .label1) + constructFieldSet( + withLabel: "Label", + andDescription: "Description", + wrappedView: BPKTextField(placeholder: "Enter text", .constant("")) + ) + Divider() + BPKText("With Label & No Description", style: .label1) + constructFieldSet( + withLabel: "Label", + wrappedView: BPKTextField(placeholder: "Enter text", .constant("")) + ) + Divider() + BPKText("With No Label & Description", style: .label1) + constructFieldSet( + andDescription: "Description", + wrappedView: BPKTextArea(.constant(""), placeholder: "Enter text") + ) + Divider() + BPKText("With No Label & No Description", style: .label1) + constructFieldSet( + wrappedView: BPKSelect( + placeholder: "Breakfast Choices", + options: ["Porridge", "Eggs", "Swift UI"], + selectedIndex: .constant(1) + ) + ) + } + } + + @ViewBuilder + func constructFieldSet( + withLabel label: String? = nil, + andDescription description: String? = nil, + wrappedView: some BPKFieldSetContentView + ) -> some View { + ForEach([0, 1], id: \.self) { index in + BPKFieldSet(label: label, description: description) { + wrappedView + } + .if(index == 1) { view in + view.inputState(.error(message: "Error Message")) + } + } + } +} diff --git a/Backpack-SwiftUI/FieldSet/Classes/BPKFieldSetState.swift b/Backpack-SwiftUI/FieldSet/Classes/BPKFieldSetState.swift new file mode 100644 index 000000000..e56b6065b --- /dev/null +++ b/Backpack-SwiftUI/FieldSet/Classes/BPKFieldSetState.swift @@ -0,0 +1,44 @@ +/* + * Backpack - Skyscanner's Design System + * + * Copyright 2018-2022 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import SwiftUI + +extension BPKFieldSet { + /// The state of the field set. + public enum State { + /// Default state. + case `default` + /// Error state + /// Adds an errors message beneath the description or the wrapped view if no description is provided. + case error(message: String) + + var labelColor: BPKColor { + switch self { + case .default: return .textPrimaryColor + case .error: return .textErrorColor + } + } + + var descriptionColor: BPKColor { + switch self { + case .default: return .textSecondaryColor + case .error: return .textSecondaryColor + } + } + } +} diff --git a/Backpack-SwiftUI/FieldSet/README.md b/Backpack-SwiftUI/FieldSet/README.md new file mode 100644 index 000000000..14c1c52c9 --- /dev/null +++ b/Backpack-SwiftUI/FieldSet/README.md @@ -0,0 +1,43 @@ +# Backpack-SwiftUI/BannerAlert + +[![Cocoapods](https://img.shields.io/cocoapods/v/Backpack-SwiftUI.svg?style=flat)](hhttps://cocoapods.org/pods/Backpack-SwiftUI) +[![class reference](https://img.shields.io/badge/Class%20reference-iOS-blue)](https://backpack.github.io/ios/versions/latest/swiftui/Structs/BPKFieldSet.html) +[![view on Github](https://img.shields.io/badge/Source%20code-GitHub-lightgrey)](https://github.com/Skyscanner/backpack-ios/tree/main/Backpack-SwiftUI/BPKFieldSet) + +## Default + +| Day | Night | +| --- | --- | +| | | + +## Error + +| Day | Night | +| --- | --- | +| | | + +# Usage + +FieldSet is a component which wraps its content (view) and optionally adds a title, description and error label (depending on the field's state) around it. + +Supported states are Default, and Error. The states are dispatched to the wrapped view. The wrapped view must conform to `BPKFieldSetContentView` to ensure it can handle the dispatched state. + + +## BPKFieldSet + +### Example of a FieldSet with Default state: + +```swift +BPKFieldSet(label: "Label", description: "Description") { + BPKTextField(placeholder: "Enter text", .constant("")) +} +``` + +### Example of a FieldSet with Error state: + +```swift +BPKFieldSet(label: "Label", description: "Description") { + BPKTextField(placeholder: "Enter text", .constant("")) +} +.inputState(.error(message: "Error Message")) +``` diff --git a/Backpack-SwiftUI/Select/Classes/BPKSelect+FieldSet.swift b/Backpack-SwiftUI/Select/Classes/BPKSelect+FieldSet.swift new file mode 100644 index 000000000..89ed95390 --- /dev/null +++ b/Backpack-SwiftUI/Select/Classes/BPKSelect+FieldSet.swift @@ -0,0 +1,42 @@ +/* + * Backpack - Skyscanner's Design System + * + * Copyright © 2023 Skyscanner Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import SwiftUI + +extension BPKSelect: BPKFieldSetContentView { + public func inputState(_ state: BPKFieldSet.State) -> BPKSelect { + switch state { + case .default: + return inputState(BPKSelect.State.default) + case .error: + return inputState(BPKSelect.State.error) + } + } +} + +#Preview { + BPKFieldSet(label: "Label", description: "Description") { + BPKSelect( + placeholder: "Breakfast Choices", + options: ["Porridge", "Eggs", "Swift UI"], + selectedIndex: .constant(1) + ) + } + .inputState(.error(message: "Error Message")) + .padding() +} diff --git a/Backpack-SwiftUI/Tests/FieldSet/BPKFieldSetTests.swift b/Backpack-SwiftUI/Tests/FieldSet/BPKFieldSetTests.swift new file mode 100644 index 000000000..5e16993a7 --- /dev/null +++ b/Backpack-SwiftUI/Tests/FieldSet/BPKFieldSetTests.swift @@ -0,0 +1,58 @@ +/* + * Backpack - Skyscanner's Design System + * + * Copyright 2018 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import XCTest +import SwiftUI +@testable import Backpack_SwiftUI + +final class BPKFieldSetTests: XCTestCase { + // swiftlint:disable large_tuple + private var testsCases: [(label: String?, description: String?, name: String)] { + [ + ("Label", "Description", "LabelAndDescription"), + ("Label", nil, "LabelAndNoDescription"), + (nil, "Description", "NoLabelAndDescription"), + (nil, nil, "NoLabelAndNoDescription") + ] + } + + func test_defaultState() { + testsCases.forEach { testCase in + assertSnapshot( + BPKFieldSet(label: testCase.label, description: testCase.description, content: { + BPKTextField(placeholder: "Enter text", .constant("")) + }) + .frame(width: 300), + testName: "test_defaultStateWith\(testCase.name)" + ) + } + } + + func test_errorState() { + testsCases.forEach { testCase in + assertSnapshot( + BPKFieldSet(label: testCase.label, description: testCase.description, content: { + BPKTextField(placeholder: "Enter text", .constant("")) + }) + .inputState(.error(message: "Error Message")) + .frame(width: 300), + testName: "test_errorStateWith\(testCase.name)" + ) + } + } +} diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndDescription.dark-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndDescription.dark-mode.png new file mode 100644 index 000000000..bbb1d1de3 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndDescription.dark-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndDescription.light-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndDescription.light-mode.png new file mode 100644 index 000000000..8266c1767 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndDescription.light-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndDescription.rtl.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndDescription.rtl.png new file mode 100644 index 000000000..34f885b03 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndDescription.rtl.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndNoDescription.dark-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndNoDescription.dark-mode.png new file mode 100644 index 000000000..c5e5eb2b6 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndNoDescription.dark-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndNoDescription.light-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndNoDescription.light-mode.png new file mode 100644 index 000000000..338d4a138 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndNoDescription.light-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndNoDescription.rtl.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndNoDescription.rtl.png new file mode 100644 index 000000000..43b0f3839 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithLabelAndNoDescription.rtl.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndDescription.dark-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndDescription.dark-mode.png new file mode 100644 index 000000000..af068fb7a Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndDescription.dark-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndDescription.light-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndDescription.light-mode.png new file mode 100644 index 000000000..e2b717d52 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndDescription.light-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndDescription.rtl.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndDescription.rtl.png new file mode 100644 index 000000000..96ee1441c Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndDescription.rtl.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndNoDescription.dark-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndNoDescription.dark-mode.png new file mode 100644 index 000000000..8f895f71c Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndNoDescription.dark-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndNoDescription.light-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndNoDescription.light-mode.png new file mode 100644 index 000000000..815496b66 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndNoDescription.light-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndNoDescription.rtl.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndNoDescription.rtl.png new file mode 100644 index 000000000..b3061caca Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_defaultStateWithNoLabelAndNoDescription.rtl.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndDescription.dark-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndDescription.dark-mode.png new file mode 100644 index 000000000..0f6e79b64 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndDescription.dark-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndDescription.light-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndDescription.light-mode.png new file mode 100644 index 000000000..a9425f955 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndDescription.light-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndDescription.rtl.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndDescription.rtl.png new file mode 100644 index 000000000..32589be4a Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndDescription.rtl.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndNoDescription.dark-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndNoDescription.dark-mode.png new file mode 100644 index 000000000..76928a255 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndNoDescription.dark-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndNoDescription.light-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndNoDescription.light-mode.png new file mode 100644 index 000000000..ee5177f66 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndNoDescription.light-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndNoDescription.rtl.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndNoDescription.rtl.png new file mode 100644 index 000000000..7fc6e7c0c Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithLabelAndNoDescription.rtl.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndDescription.dark-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndDescription.dark-mode.png new file mode 100644 index 000000000..511389666 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndDescription.dark-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndDescription.light-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndDescription.light-mode.png new file mode 100644 index 000000000..8b6ff9142 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndDescription.light-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndDescription.rtl.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndDescription.rtl.png new file mode 100644 index 000000000..e0d041ce5 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndDescription.rtl.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndNoDescription.dark-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndNoDescription.dark-mode.png new file mode 100644 index 000000000..fe96e241b Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndNoDescription.dark-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndNoDescription.light-mode.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndNoDescription.light-mode.png new file mode 100644 index 000000000..54a4c7c8b Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndNoDescription.light-mode.png differ diff --git a/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndNoDescription.rtl.png b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndNoDescription.rtl.png new file mode 100644 index 000000000..8ebb8c324 Binary files /dev/null and b/Backpack-SwiftUI/Tests/FieldSet/__Snapshots__/BPKFieldSetTests/test_errorStateWithNoLabelAndNoDescription.rtl.png differ diff --git a/Backpack-SwiftUI/TextArea/Classes/BPKTextArea+FieldSet.swift b/Backpack-SwiftUI/TextArea/Classes/BPKTextArea+FieldSet.swift new file mode 100644 index 000000000..314c94999 --- /dev/null +++ b/Backpack-SwiftUI/TextArea/Classes/BPKTextArea+FieldSet.swift @@ -0,0 +1,39 @@ +/* + * Backpack - Skyscanner's Design System + * + * Copyright © 2023 Skyscanner Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import SwiftUI + +extension BPKTextArea: BPKFieldSetContentView { + public func inputState(_ state: BPKFieldSet.State) -> BPKTextArea { + switch state { + case .default: + return inputState(BPKTextArea.State.default) + case .error: + return inputState(BPKTextArea.State.error) + } + } +} + +#Preview { + BPKFieldSet(label: "Label", description: "Description") { + BPKTextArea(.constant(""), placeholder: "Enter text") + } + .inputState(.error(message: "Error Message")) + .frame(height: 150) + .padding() +} diff --git a/Backpack-SwiftUI/TextField/Classes/BPKTextField+FieldSet.swift b/Backpack-SwiftUI/TextField/Classes/BPKTextField+FieldSet.swift new file mode 100644 index 000000000..fb2189a63 --- /dev/null +++ b/Backpack-SwiftUI/TextField/Classes/BPKTextField+FieldSet.swift @@ -0,0 +1,38 @@ +/* + * Backpack - Skyscanner's Design System + * + * Copyright © 2023 Skyscanner Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import SwiftUI + +extension BPKTextField: BPKFieldSetContentView { + public func inputState(_ state: BPKFieldSet.State) -> BPKTextField { + switch state { + case .default: + return inputState(BPKTextField.State.default) + case .error: + return inputState(BPKTextField.State.error) + } + } +} + +#Preview { + BPKFieldSet(label: "Label", description: "Description") { + BPKTextField(placeholder: "Enter text", .constant("")) + } + .inputState(.error(message: "Error Message")) + .padding() +} diff --git a/Example/Backpack Screenshot/SwiftUIScreenshots.swift b/Example/Backpack Screenshot/SwiftUIScreenshots.swift index 55639d89f..1485ed6bf 100644 --- a/Example/Backpack Screenshot/SwiftUIScreenshots.swift +++ b/Example/Backpack Screenshot/SwiftUIScreenshots.swift @@ -775,6 +775,15 @@ class SwiftUIScreenshots: BackpackSnapshotTestCase { tapBackButton() } + await navigate(title: "Field Set") { + app.tables.staticTexts["Default State"].tap() + saveScreenshot(component: "field-set", scenario: "default", userInterfaceStyle: userInterfaceStyle) + tapBackButton() + + app.tables.staticTexts["Error State"].tap() + saveScreenshot(component: "field-set", scenario: "error", userInterfaceStyle: userInterfaceStyle) + } + await navigate(title: "Snippet") { app.tables.staticTexts["Landscape"].tap() saveScreenshot(component: "snippet", scenario: "landscape", userInterfaceStyle: userInterfaceStyle) diff --git a/Example/Backpack.xcodeproj/project.pbxproj b/Example/Backpack.xcodeproj/project.pbxproj index a5bc466a2..e32cdd33c 100644 --- a/Example/Backpack.xcodeproj/project.pbxproj +++ b/Example/Backpack.xcodeproj/project.pbxproj @@ -12,6 +12,9 @@ 0245783E2ABC3AA1006008D1 /* InsetBannerExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0245783D2ABC3AA1006008D1 /* InsetBannerExampleView.swift */; }; 02D34C4A2A712A2200F99085 /* SnippetGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D34C492A712A2200F99085 /* SnippetGroups.swift */; }; 0E844AF7289141C2004C5865 /* SkeletonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E844AF6289141C2004C5865 /* SkeletonViewController.swift */; }; + 1318F74D2C7772F000D42847 /* TextFieldExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1318F74C2C7772F000D42847 /* TextFieldExampleView.swift */; }; + 1318F74E2C77734C00D42847 /* FieldSetExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1318F74A2C7772C400D42847 /* FieldSetExampleView.swift */; }; + 1318F7502C777D1800D42847 /* FieldSetGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1318F74F2C777D1800D42847 /* FieldSetGroups.swift */; }; 13E884A72C53F8C800A3138D /* BannerAlertExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13E884A62C53F8C800A3138D /* BannerAlertExampleView.swift */; }; 17068B8A299C0106000EA7F4 /* RatingGroupsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17068B89299C0106000EA7F4 /* RatingGroupsProvider.swift */; }; 1750EB6329915230005226DF /* CardButtonsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1750EB6229915230005226DF /* CardButtonsViewController.swift */; }; @@ -243,6 +246,9 @@ 0E844AF6289141C2004C5865 /* SkeletonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonViewController.swift; sourceTree = ""; }; 0F386FFD6327F785FBB09093 /* Pods-Backpack-Native.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Backpack-Native.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Backpack-Native/Pods-Backpack-Native.debug.xcconfig"; sourceTree = ""; }; 11F0F39D8252943CD3B0C19D /* Pods-Backpack_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Backpack_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-Backpack_Example/Pods-Backpack_Example.release.xcconfig"; sourceTree = ""; }; + 1318F74A2C7772C400D42847 /* FieldSetExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldSetExampleView.swift; sourceTree = ""; }; + 1318F74C2C7772F000D42847 /* TextFieldExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldExampleView.swift; sourceTree = ""; }; + 1318F74F2C777D1800D42847 /* FieldSetGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldSetGroups.swift; sourceTree = ""; }; 13E884A62C53F8C800A3138D /* BannerAlertExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BannerAlertExampleView.swift; sourceTree = ""; }; 17068B89299C0106000EA7F4 /* RatingGroupsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingGroupsProvider.swift; sourceTree = ""; }; 1750EB6229915230005226DF /* CardButtonsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardButtonsViewController.swift; sourceTree = ""; }; @@ -522,6 +528,14 @@ path = Skeleton; sourceTree = ""; }; + 1318F7492C7772C400D42847 /* FieldSet */ = { + isa = PBXGroup; + children = ( + 1318F74A2C7772C400D42847 /* FieldSetExampleView.swift */, + ); + path = FieldSet; + sourceTree = ""; + }; 13E8849B2C53A0A400A3138D /* BannerAlert */ = { isa = PBXGroup; children = ( @@ -780,6 +794,7 @@ 53C661FE29EA0DAB00BF1A62 /* TextField */ = { isa = PBXGroup; children = ( + 1318F74C2C7772F000D42847 /* TextFieldExampleView.swift */, 53C661FF29EA0DAB00BF1A62 /* TextFieldExampleView.swift */, ); path = TextField; @@ -869,6 +884,7 @@ 7904384929F11EA7006CD8E0 /* SliderGroups.swift */, 02D34C492A712A2200F99085 /* SnippetGroups.swift */, 53B6DB5D27FC39B40042B7C0 /* CalendarGroups.swift */, + 1318F74F2C777D1800D42847 /* FieldSetGroups.swift */, 53B6DB6327FC463F0042B7C0 /* FlareGroups.swift */, 53B6DB6527FC47290042B7C0 /* DialogGroups.swift */, 53B6DB6727FC4D520042B7C0 /* HorizontalNavigationGroups.swift */, @@ -1070,6 +1086,7 @@ 53C661FB29EA0DAB00BF1A62 /* StarRating */, 53C661E529EA0DAA00BF1A62 /* Switch */, 53C6620D29EA0DAB00BF1A62 /* Text */, + 1318F7492C7772C400D42847 /* FieldSet */, 53C661FE29EA0DAB00BF1A62 /* TextField */, ); path = Components; @@ -1845,6 +1862,7 @@ E35EA59624212F9900373CE4 /* BottomSheetResizableContentViewController.swift in Sources */, D27EE92C2193163900C877EA /* ChipsViewController.swift in Sources */, 1AF3D2CA2BB18924001623A4 /* ImageGalleryGridExampleView.swift in Sources */, + 1318F7502C777D1800D42847 /* FieldSetGroups.swift in Sources */, 79AFEDA92A9C7063001615A2 /* GraphicPromoExampleView.swift in Sources */, D2BBC1C922560FDD002DA71A /* BPKTableViewSelectableCell.swift in Sources */, 79F5129329AEFEB600D0997A /* LabelViewController.swift in Sources */, @@ -1910,6 +1928,7 @@ 53BA7BCE2B9B5E6A00F93982 /* RatingBarExampleView.swift in Sources */, 53E075AC27FC9AB00033147C /* CellsDataSource.swift in Sources */, 794E1FF6280A8A0000B965FF /* ContentUIHostingController.swift in Sources */, + 1318F74D2C7772F000D42847 /* TextFieldExampleView.swift in Sources */, 53C6621829EA0DAB00BF1A62 /* OverlayExampleView.swift in Sources */, 537ED1AF282D65A300032105 /* ShadowTokensView.swift in Sources */, 79F63C0229B0695C00FB19A9 /* OverlayViewController.swift in Sources */, @@ -1958,6 +1977,7 @@ 53D6396F2A7D46ED007EF376 /* MapMarkerExampleView.swift in Sources */, BA809EC82B05FC2E0030D1E7 /* AppSearchModalGroups.swift in Sources */, 53C7F0BA2A4C615D003A8740 /* ChipGroupSingleSelectWrapExampleView.swift in Sources */, + 1318F74E2C77734C00D42847 /* FieldSetExampleView.swift in Sources */, 5390DB5F29098D7300F0F790 /* SpacingTokensViewController.swift in Sources */, 53C6621F29EA0DAB00BF1A62 /* StarRatingPlayground.swift in Sources */, 8071379C25AF7974009869D1 /* BottomSheetPersistentViewController.swift in Sources */, diff --git a/Example/Backpack/SwiftUI/Components/FieldSet/FieldSetExampleView.swift b/Example/Backpack/SwiftUI/Components/FieldSet/FieldSetExampleView.swift new file mode 100644 index 000000000..92d265c02 --- /dev/null +++ b/Example/Backpack/SwiftUI/Components/FieldSet/FieldSetExampleView.swift @@ -0,0 +1,90 @@ +// +/* + * Backpack - Skyscanner's Design System + * + * Copyright © 2023 Skyscanner Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import SwiftUI +import Backpack_SwiftUI + +struct FieldSetExampleView: View { + @State var text1 = "" + let state: FieldState + + // swiftlint:disable closure_body_length + var body: some View { + ScrollView { + VStack(spacing: .md) { + BPKText("With Label & Description", style: .label1) + constructFieldSet( + withLabel: "Label", + andDescription: "Description", + wrappedView: BPKTextField(placeholder: "Enter text", .constant("")) + ) + Divider() + BPKText("With Label & No Description", style: .label1) + constructFieldSet( + withLabel: "Label", + wrappedView: BPKTextField(placeholder: "Enter text", .constant("")) + ) + Divider() + BPKText("With No Label & Description", style: .label1) + constructFieldSet( + andDescription: "Description", + wrappedView: BPKTextArea(.constant(""), placeholder: "Enter text") + ) + Divider() + BPKText("With No Label & No Description", style: .label1) + constructFieldSet( + wrappedView: BPKSelect( + placeholder: "Breakfast Choices", + options: ["Porridge", "Eggs", "Swift UI"], + selectedIndex: .constant(1) + ) + ) + } + .padding() + } + } + + @ViewBuilder + private func constructFieldSet( + withLabel label: String? = nil, + andDescription description: String? = nil, + wrappedView: some BPKFieldSetContentView + ) -> some View { + BPKFieldSet(label: label, description: description) { + wrappedView + } + .accessibilityPrefix("Example") + .if(state == .error) { view in + view.inputState(.error(message: "Error Message")) + } + } + + enum FieldState { + case `default` + case error + } +} + +#Preview("Default State") { + FieldSetExampleView(state: .default) +} + +#Preview("Error State") { + FieldSetExampleView(state: .error) +} diff --git a/Example/Backpack/Utils/FeatureStories/ComponentCells.swift b/Example/Backpack/Utils/FeatureStories/ComponentCells.swift index 9ff094ce4..ac8104b2b 100644 --- a/Example/Backpack/Utils/FeatureStories/ComponentCells.swift +++ b/Example/Backpack/Utils/FeatureStories/ComponentCells.swift @@ -54,6 +54,7 @@ struct ComponentCellsProvider { cardCarousel(), chips(), chipGroup(), + fieldSet(), flightLeg(), flare(), floatingNotification(), @@ -655,6 +656,15 @@ extension ComponentCellsProvider { showChildren: { showComponent(title: "Price", tabs: $0) } ) } + private func fieldSet() -> CellDataSource { + ComponentCellDataSource( + title: "Field Set", + tabs: [ + .swiftui(groups: FieldSetGroupsProvider(showPresentable: show(presentable:)).swiftUIGroups()) + ], + showChildren: { showComponent(title: "Field Set", tabs: $0) } + ) + } private func flightLeg() -> CellDataSource { ComponentCellDataSource( title: "Flight Leg", diff --git a/Example/Backpack/Utils/FeatureStories/Groups/FieldSetGroups.swift b/Example/Backpack/Utils/FeatureStories/Groups/FieldSetGroups.swift new file mode 100644 index 000000000..f52964bf1 --- /dev/null +++ b/Example/Backpack/Utils/FeatureStories/Groups/FieldSetGroups.swift @@ -0,0 +1,44 @@ +/* + * Backpack - Skyscanner's Design System + * + * Copyright 2018 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import SwiftUI + +@MainActor +struct FieldSetGroupsProvider { + let showPresentable: (Presentable) -> Void + + private func presentable( + _ title: String, + view: Content + ) -> CellDataSource { + PresentableCellDataSource.custom( + title: title, + customController: { ContentUIHostingController(view) }, + showPresentable: showPresentable + ) + } + + func swiftUIGroups() -> [Components.Group] { + SingleGroupProvider( + cellDataSources: [ + presentable("Default State", view: FieldSetExampleView(state: .default)), + presentable("Error State", view: FieldSetExampleView(state: .error)) + ] + ).groups() + } +} diff --git a/screenshots/iPhone SE (3rd generation)-swiftui_banner-alert___default_dm.png b/screenshots/iPhone-swiftui_banner-alert___default_dm.png similarity index 100% rename from screenshots/iPhone SE (3rd generation)-swiftui_banner-alert___default_dm.png rename to screenshots/iPhone-swiftui_banner-alert___default_dm.png diff --git a/screenshots/iPhone SE (3rd generation)-swiftui_banner-alert___default_lm.png b/screenshots/iPhone-swiftui_banner-alert___default_lm.png similarity index 100% rename from screenshots/iPhone SE (3rd generation)-swiftui_banner-alert___default_lm.png rename to screenshots/iPhone-swiftui_banner-alert___default_lm.png diff --git a/screenshots/iPhone-swiftui_field-set___default_dm.png b/screenshots/iPhone-swiftui_field-set___default_dm.png new file mode 100644 index 000000000..6aaf677c2 Binary files /dev/null and b/screenshots/iPhone-swiftui_field-set___default_dm.png differ diff --git a/screenshots/iPhone-swiftui_field-set___default_lm.png b/screenshots/iPhone-swiftui_field-set___default_lm.png new file mode 100644 index 000000000..5a4ce0a0c Binary files /dev/null and b/screenshots/iPhone-swiftui_field-set___default_lm.png differ diff --git a/screenshots/iPhone-swiftui_field-set___error_dm.png b/screenshots/iPhone-swiftui_field-set___error_dm.png new file mode 100644 index 000000000..4c9f1ca0f Binary files /dev/null and b/screenshots/iPhone-swiftui_field-set___error_dm.png differ diff --git a/screenshots/iPhone-swiftui_field-set___error_lm.png b/screenshots/iPhone-swiftui_field-set___error_lm.png new file mode 100644 index 000000000..2fbc40386 Binary files /dev/null and b/screenshots/iPhone-swiftui_field-set___error_lm.png differ