[PM-26064] Consolidate various shared components (#2036)

This commit is contained in:
Katherine Bertelsen 2025-10-13 15:44:30 -05:00 committed by GitHub
parent 9393aa3859
commit 9a81338d2e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 53 additions and 340 deletions

View File

@ -1,107 +0,0 @@
import BitwardenKit
import SwiftUI
/// A standardized view used to display some text into a row of a list. This is commonly used in
/// forms.
struct BitwardenTextValueField<AccessoryContent>: View where AccessoryContent: View {
// MARK: Properties
/// The (optional) title of the field.
var title: String?
/// The (optional) accessibility identifier to apply to the title of the field (if it exists)
var titleAccessibilityIdentifier: String?
/// The text value to display in this field.
var value: String
/// The (optional) accessibility identifier to apply to the displayed value of the field
var valueAccessibilityIdentifier: String?
/// Any accessory content that should be displayed on the trailing edge of the field. This
/// content automatically has the `AccessoryButtonStyle` applied to it.
var accessoryContent: AccessoryContent?
// MARK: View
var body: some View {
BitwardenField(title: title, titleAccessibilityIdentifier: titleAccessibilityIdentifier) {
Text(value)
.styleGuide(.body)
.multilineTextAlignment(.leading)
.foregroundColor(Asset.Colors.textPrimary.swiftUIColor)
.accessibilityIdentifier(valueAccessibilityIdentifier ?? value)
} accessoryContent: {
accessoryContent
}
}
// MARK: Initialization
/// Creates a new `BitwardenTextValueField`.
///
/// - Parameters:
/// - title: The (optional) title of the field.
/// - titleAccessibilityIdentifier: The (optional) accessibility identifier to apply
/// to the title of the field (if it exists)
/// - value: The text value to display in this field.
/// - valueAccessibilityIdentifier: The (optional) accessibility identifier to apply
/// to the displayed value of the field
/// - accessoryContent: Any accessory content that should be displayed on the trailing edge of
/// the field. This content automatically has the `AccessoryButtonStyle` applied to it.
///
init(
title: String? = nil,
titleAccessibilityIdentifier: String? = "ItemName",
value: String,
valueAccessibilityIdentifier: String? = "ItemValue",
@ViewBuilder accessoryContent: () -> AccessoryContent,
) {
self.title = title
self.titleAccessibilityIdentifier = titleAccessibilityIdentifier
self.value = value
self.valueAccessibilityIdentifier = valueAccessibilityIdentifier
self.accessoryContent = accessoryContent()
}
}
extension BitwardenTextValueField where AccessoryContent == EmptyView {
/// Creates a new `BitwardenTextValueField` without accessory content.
///
/// - Parameters:
/// - title: The (optional) title of the field.
/// - titleAccessibilityIdentifier: The (optional) accessibility identifier to apply
/// to the title of the field (if it exists)
/// - value: The text value to display in this field.
///
init(
title: String? = nil,
titleAccessibilityIdentifier: String? = "ItemName",
value: String,
valueAccessibilityIdentifier: String? = "ItemValue",
) {
self.init(
title: title,
titleAccessibilityIdentifier: titleAccessibilityIdentifier,
value: value,
valueAccessibilityIdentifier: valueAccessibilityIdentifier,
) {
EmptyView()
}
}
}
// MARK: Previews
#if DEBUG
#Preview("No buttons") {
VStack {
BitwardenTextValueField(
title: "Title",
value: "Text field text",
)
.padding()
}
.background(Color(.systemGroupedBackground))
}
#endif

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import SwiftUI

View File

@ -1,71 +0,0 @@
import BitwardenResources
import SwiftUI
// MARK: - SearchNoResultsView
/// A view that displays the no search results image and text.
///
struct SearchNoResultsView<Content: View>: View {
// MARK: Properties
/// An optional view to display at the top of the scroll view above the no results image and text.
var headerView: Content?
// MARK: View
var body: some View {
GeometryReader { reader in
ScrollView {
VStack(spacing: 0) {
if let headerView {
headerView
}
VStack(spacing: 35) {
Image(decorative: SharedAsset.Icons.search24)
.resizable()
.frame(width: 74, height: 74)
.foregroundColor(Asset.Colors.textSecondary.swiftUIColor)
Text(Localizations.thereAreNoItemsThatMatchTheSearch)
.multilineTextAlignment(.center)
.styleGuide(.callout)
.foregroundColor(Asset.Colors.textPrimary.swiftUIColor)
}
.accessibilityIdentifier("NoSearchResultsLabel")
.frame(maxWidth: .infinity, minHeight: reader.size.height, maxHeight: .infinity)
}
}
}
.background(Color(asset: Asset.Colors.backgroundSecondary))
}
// MARK: Initialization
/// Initialize a `SearchNoResultsView`.
///
init() where Content == EmptyView {
headerView = nil
}
/// Initialize a `SearchNoResultsView` with a header view.
///
/// - Parameter headerView: An optional view to display at the top of the scroll view above the
/// no results image and text.
///
init(headerView: () -> Content) {
self.headerView = headerView()
}
}
// MARK: - Previews
#Preview("No Results") {
SearchNoResultsView()
}
#Preview("No Results With Header") {
SearchNoResultsView {
Text("Optional header text!")
}
}

View File

@ -1,39 +0,0 @@
import SwiftUI
// MARK: - SectionHeaderView
/// A section header.
///
struct SectionHeaderView: View {
// MARK: Properties
/// The section title.
let title: String
// MARK: View
var body: some View {
Text(title)
.styleGuide(.footnote)
.accessibilityAddTraits(.isHeader)
.foregroundColor(Color(asset: Asset.Colors.textSecondary))
.textCase(.uppercase)
}
// MARK: Initialization
/// Initializes a new section header.
///
/// - Parameters:
/// - title: The section title.
///
init(_ title: String) {
self.title = title
}
}
// MARK: Previews
#Preview {
SectionHeaderView("Section header")
}

View File

@ -1,19 +0,0 @@
// swiftlint:disable:this file_name
import SnapshotTesting
import XCTest
@testable import AuthenticatorShared
class SectionViewTests: BitwardenTestCase {
// MARK: Tests
/// Test a snapshot of the sectionView.
func disabletest_snapshot_sectionView() {
for preview in SectionView_Previews._allPreviews {
assertSnapshots(
of: preview.content,
as: [.defaultPortrait],
)
}
}
}

View File

@ -1,74 +0,0 @@
import BitwardenKit
import BitwardenResources
import SwiftUI
// MARK: - SectionView
/// A section view.
///
struct SectionView<Content: View>: View {
// MARK: Properties
/// Content displayed below section header view.
let content: Content
/// The spacing of the content.
let contentSpacing: CGFloat
/// The section header title.
let title: String
/// The spacing between title and content.
let titleSpacing: CGFloat
// MARK: View
var body: some View {
VStack(alignment: .leading, spacing: titleSpacing) {
SectionHeaderView(title)
VStack(alignment: .leading, spacing: contentSpacing) {
content
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
// MARK: Initialization
/// Initializes a new section view.
///
/// - Parameters:
/// - title: The section header title.
/// - titleSpacing: The spacing between title and content.
/// - content: The content displayed below the section title.
/// - contentSpacing: The spacing of content items.
///
init(
_ title: String,
titleSpacing: CGFloat = 8,
contentSpacing: CGFloat = 16,
@ViewBuilder content: () -> Content,
) {
self.title = title
self.titleSpacing = titleSpacing
self.contentSpacing = contentSpacing
self.content = content()
}
}
// MARK: Previews
#if DEBUG
struct SectionView_Previews: PreviewProvider {
static var previews: some View {
VStack {
SectionView("Section View") {
BitwardenTextField(title: Localizations.name, text: .constant("name"))
}
}
.padding(16)
.background(Asset.Colors.backgroundSecondary.swiftUIColor)
}
}
#endif

View File

@ -1,10 +1,9 @@
import BitwardenKit
import BitwardenResources
import SwiftUI
/// A standardized view used to display some text into a row of a list. This is commonly used in
/// forms.
struct BitwardenTextValueField<AccessoryContent>: View where AccessoryContent: View {
public struct BitwardenTextValueField<AccessoryContent>: View where AccessoryContent: View {
// MARK: Properties
/// Whether the text selection is enabled.
@ -42,7 +41,7 @@ struct BitwardenTextValueField<AccessoryContent>: View where AccessoryContent: V
// MARK: View
var body: some View {
public var body: some View {
BitwardenField(
title: title,
titleAccessibilityIdentifier: titleAccessibilityIdentifier,
@ -90,7 +89,7 @@ struct BitwardenTextValueField<AccessoryContent>: View where AccessoryContent: V
/// - useUIKitTextView: Whether we should use a UITextView or a SwiftUI version.
/// - accessoryContent: Any accessory content that should be displayed on the trailing edge of
/// the field. This content automatically has the `AccessoryButtonStyle` applied to it.
init(
public init(
title: String? = nil,
titleAccessibilityIdentifier: String? = "ItemName",
value: String,
@ -109,7 +108,7 @@ struct BitwardenTextValueField<AccessoryContent>: View where AccessoryContent: V
}
}
extension BitwardenTextValueField where AccessoryContent == EmptyView {
public extension BitwardenTextValueField where AccessoryContent == EmptyView {
/// Creates a new `BitwardenTextValueField` without accessory content.
///
/// - Parameters:
@ -144,7 +143,7 @@ extension BitwardenTextValueField where AccessoryContent == EmptyView {
}
}
extension BitwardenTextValueField where AccessoryContent == AccessoryButton {
public extension BitwardenTextValueField where AccessoryContent == AccessoryButton {
/// Creates a new `BitwardenTextValueField` with a button as accessory content.
///
/// - Parameters:

View File

@ -1,4 +1,3 @@
import BitwardenKit
import BitwardenResources
import SwiftUI
import UIKit
@ -7,12 +6,12 @@ import UIKit
/// A custom `UITextView` wrapped in a `UIViewRepresentable` for use in SwiftUI.
///
struct BitwardenUITextView: UIViewRepresentable {
public struct BitwardenUITextView: UIViewRepresentable {
// MARK: - Coordinator
/// A coordinator to act as the delegate for `UITextView`, handling text changes and other events.
///
class Coordinator: NSObject, UITextViewDelegate {
public class Coordinator: NSObject, UITextViewDelegate {
/// The parent view.
var parent: BitwardenUITextView
@ -29,7 +28,7 @@ struct BitwardenUITextView: UIViewRepresentable {
/// - calculatedHeight: The height of the text view.
/// - isFocused: A binding for whether the text view has focus.
///
init(
public init(
_ parent: BitwardenUITextView,
calculatedHeight: Binding<CGFloat>,
isFocused: Binding<Bool>,
@ -39,11 +38,11 @@ struct BitwardenUITextView: UIViewRepresentable {
_isFocused = isFocused
}
func textViewDidBeginEditing(_ textView: UITextView) {
public func textViewDidBeginEditing(_ textView: UITextView) {
isFocused = true
}
func textViewDidChange(_ uiView: UITextView) {
public func textViewDidChange(_ uiView: UITextView) {
parent.text = uiView.text
parent.recalculateHeight(
view: uiView,
@ -51,7 +50,7 @@ struct BitwardenUITextView: UIViewRepresentable {
)
}
func textViewDidEndEditing(_ textView: UITextView) {
public func textViewDidEndEditing(_ textView: UITextView) {
isFocused = false
}
}
@ -75,11 +74,34 @@ struct BitwardenUITextView: UIViewRepresentable {
/// A binding for whether the text view has focus.
@Binding var isFocused: Bool
// MARK: - Initializers
/// Public version of synthesized initializer.
///
/// - Parameters:
/// - text: The text entered into the text field.
/// - calculatedHeight: The calculated height of the `UITextView`. This value is dynamically updated
/// based on the content size, and it helps to adjust the height of the view in SwiftUI.
/// - isEditable: Indicates whether the `UITextView` is editable. When set to `true`, the user can edit the
/// text. If `false`, the text view is read-only.
/// - isFocused: A binding for whether the text view has focus.
public init(
text: Binding<String>,
calculatedHeight: Binding<CGFloat>,
isEditable: Bool,
isFocused: Binding<Bool>,
) {
_text = text
_calculatedHeight = calculatedHeight
self.isEditable = isEditable
_isFocused = isFocused
}
/// Creates and returns the coordinator for the `UITextView`.
///
/// - Returns: A `Coordinator` instance to manage the `UITextView`'s events.
///
func makeCoordinator() -> Coordinator {
public func makeCoordinator() -> Coordinator {
Coordinator(self, calculatedHeight: $calculatedHeight, isFocused: $isFocused)
}
@ -90,7 +112,7 @@ struct BitwardenUITextView: UIViewRepresentable {
/// - Parameter context: The context containing the coordinator for this view.
/// - Returns: A configured `UITextView` instance.
///
func makeUIView(context: Context) -> UITextView {
public func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.adjustsFontForContentSizeCategory = true
textView.autocapitalizationType = .sentences
@ -118,7 +140,7 @@ struct BitwardenUITextView: UIViewRepresentable {
/// - uiView: The `UITextView` instance being updated.
/// - context: The context containing the coordinator for this view.
///
func updateUIView(
public func updateUIView(
_ uiView: UITextView,
context: Context,
) {
@ -172,7 +194,7 @@ struct BitwardenUITextView: UIViewRepresentable {
}
@available(iOS 16, *)
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context: Context) -> CGSize? {
public func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context: Context) -> CGSize? {
guard let width = proposal.width else { return nil }
let size = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
return CGSize(width: width, height: size.height)

View File

@ -5,7 +5,7 @@ import SwiftUI
/// A view that displays the no search results image and text.
///
struct SearchNoResultsView<Content: View>: View {
public struct SearchNoResultsView<Content: View>: View {
// MARK: Properties
/// An optional view to display at the top of the scroll view above the no results image and text.
@ -13,7 +13,7 @@ struct SearchNoResultsView<Content: View>: View {
// MARK: View
var body: some View {
public var body: some View {
GeometryReader { reader in
ScrollView {
VStack(spacing: 0) {
@ -45,7 +45,7 @@ struct SearchNoResultsView<Content: View>: View {
/// Initialize a `SearchNoResultsView`.
///
init() where Content == EmptyView {
public init() where Content == EmptyView {
headerView = nil
}
@ -54,7 +54,7 @@ struct SearchNoResultsView<Content: View>: View {
/// - Parameter headerView: An optional view to display at the top of the scroll view above the
/// no results image and text.
///
init(headerView: () -> Content) {
public init(headerView: () -> Content) {
self.headerView = headerView()
}
}

View File

@ -5,7 +5,7 @@ import SwiftUI
/// A section header.
///
struct SectionHeaderView: View {
public struct SectionHeaderView: View {
// MARK: Properties
/// The section title.
@ -13,7 +13,7 @@ struct SectionHeaderView: View {
// MARK: View
var body: some View {
public var body: some View {
Text(title)
.styleGuide(.caption1, weight: .bold)
.accessibilityAddTraits(.isHeader)
@ -29,7 +29,7 @@ struct SectionHeaderView: View {
/// - Parameters:
/// - title: The section title.
///
init(_ title: String) {
public init(_ title: String) {
self.title = title
}
}

View File

@ -1,4 +1,3 @@
import BitwardenKit
import BitwardenResources
import SwiftUI
@ -6,7 +5,7 @@ import SwiftUI
/// A section view.
///
struct SectionView<Content: View>: View {
public struct SectionView<Content: View>: View {
// MARK: Properties
/// Content displayed below section header view.
@ -23,7 +22,7 @@ struct SectionView<Content: View>: View {
// MARK: View
var body: some View {
public var body: some View {
VStack(alignment: .leading, spacing: titleSpacing) {
SectionHeaderView(title)
@ -44,7 +43,7 @@ struct SectionView<Content: View>: View {
/// - content: The content displayed below the section title.
/// - contentSpacing: The spacing of content items.
///
init(
public init(
_ title: String,
titleSpacing: CGFloat = 8,
contentSpacing: CGFloat = 16,

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import SwiftUI

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import SwiftUI

View File

@ -79,7 +79,7 @@ struct BitwardenUITextViewType: BaseViewType {
static var typePrefix: String = "BitwardenUITextView"
static var namespacedPrefixes: [String] = [
"BitwardenShared.BitwardenUITextView",
"BitwardenKit.BitwardenUITextView",
]
}
@ -112,7 +112,7 @@ struct SettingsMenuFieldType: BaseViewType {
static var typePrefix: String = "SettingsMenuField"
static var namespacedPrefixes: [String] = [
"BitwardenShared.SettingsMenuField",
"BitwardenKit.SettingsMenuField",
]
}