[PM-26064] Move BitwardenTextField to BitwardenKit (#2010)

This commit is contained in:
Katherine Bertelsen 2025-10-06 11:34:51 -05:00 committed by GitHub
parent c9cb4f2588
commit fbbb2a2811
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 28 additions and 285 deletions

View File

@ -1,263 +0,0 @@
import BitwardenResources
import SwiftUI
// MARK: - BitwardenTextField
/// The standard text field used within this application. The text field can be
/// configured to act as a password field with visibility toggling, and supports
/// displaying additional content on the trailing edge of the text field.
///
struct BitwardenTextField<TrailingContent: View>: View {
// MARK: Private Properties
/// A flag indicating if this field is currently focused.
private var isFocused: Bool { isTextFieldFocused || isSecureFieldFocused }
/// A flag indicating if the secure field is currently focused.
@FocusState private var isSecureFieldFocused
/// A flag indicating if the text field is currently focused.
@FocusState private var isTextFieldFocused
// MARK: Properties
/// The accessibility identifier for the text field.
let accessibilityIdentifier: String?
/// Whether the password can be viewed (only applies if a password exists in the field).
let canViewPassword: Bool
/// The footer text displayed below the text field.
let footer: String?
/// Whether a password in this text field is visible.
let isPasswordVisible: Binding<Bool>?
/// The accessibility identifier for the button to toggle password visibility.
let passwordVisibilityAccessibilityId: String?
/// The placeholder that is displayed in the textfield.
let placeholder: String
/// The text entered into the text field.
@Binding var text: String
/// The title of the text field.
let title: String?
/// Optional content view that is displayed on the trailing edge of the menu value.
let trailingContent: TrailingContent?
// MARK: View
var body: some View {
let isTrailingContentShown = isPasswordVisible != nil || trailingContent != nil
if isTrailingContentShown {
BitwardenField(title: title, footer: footer) {
textField
} accessoryContent: {
if let isPasswordVisible, canViewPassword {
AccessoryButton(
asset: isPasswordVisible.wrappedValue
? SharedAsset.Icons.eyeSlash24
: SharedAsset.Icons.eye24,
accessibilityLabel: isPasswordVisible.wrappedValue
? Localizations.passwordIsVisibleTapToHide
: Localizations.passwordIsNotVisibleTapToShow
) {
isPasswordVisible.wrappedValue.toggle()
}
.accessibilityIdentifier(passwordVisibilityAccessibilityId ?? "TextVisibilityToggle")
if let trailingContent {
trailingContent
}
} else if let trailingContent {
trailingContent
}
}
} else {
BitwardenField(title: title, footer: footer) {
textField
}
}
}
// MARK: Private views
/// The text field.
private var textField: some View {
HStack(spacing: 8) {
ZStack {
let isPassword = isPasswordVisible != nil
let isPasswordVisible = isPasswordVisible?.wrappedValue ?? false
TextField(placeholder, text: $text)
.focused($isTextFieldFocused)
.styleGuide(isPassword ? .bodyMonospaced : .body, includeLineSpacing: false)
.hidden(!isPasswordVisible && isPassword)
.id(title)
if isPassword, !isPasswordVisible {
SecureField(placeholder, text: $text)
.focused($isSecureFieldFocused)
.styleGuide(.bodyMonospaced, includeLineSpacing: false)
.id(title)
}
}
.frame(maxWidth: .infinity, minHeight: 28)
.accessibilityIdentifier(accessibilityIdentifier ?? "BitwardenTextField")
Button {
text = ""
} label: {
SharedAsset.Icons.circleX16.swiftUIImage
.foregroundColor(Asset.Colors.primaryBitwarden.swiftUIColor)
.frame(width: 14, height: 14)
}
.padding(.vertical, 5)
.hidden(text.isEmpty || !isFocused)
}
.tint(Asset.Colors.primaryBitwarden.swiftUIColor)
}
// MARK: Initialization
/// Initializes a new `BitwardenTextField`.
///
/// - Parameters:
/// - title: The title of the text field.
/// - footer: The footer text displayed below the text field.
/// - text: The text entered into the text field.
/// - accessibilityIdentifier: The accessibility identifier for the text field.
/// - passwordVisibilityAccessibilityId: The accessibility ID for the button to toggle password visibility.
/// - canViewPassword: Whether the password can be viewed.
/// - isPasswordVisible: Whether the password is visible.
/// - placeholder: An optional placeholder to display in the text field.
///
init(
title: String? = nil,
text: Binding<String>,
footer: String? = nil,
accessibilityIdentifier: String? = nil,
passwordVisibilityAccessibilityId: String? = nil,
canViewPassword: Bool = true,
isPasswordVisible: Binding<Bool>? = nil,
placeholder: String? = nil,
@ViewBuilder trailingContent: () -> TrailingContent
) {
self.accessibilityIdentifier = accessibilityIdentifier
self.isPasswordVisible = isPasswordVisible
self.footer = footer
self.canViewPassword = canViewPassword
self.passwordVisibilityAccessibilityId = passwordVisibilityAccessibilityId
self.placeholder = placeholder ?? ""
_text = text
self.title = title
self.trailingContent = trailingContent()
}
}
extension BitwardenTextField where TrailingContent == EmptyView {
/// Initializes a new `BitwardenTextField`.
///
/// - Parameters:
/// - title: The title of the text field.
/// - footer: The footer text displayed below the text field.
/// - text: The text entered into the text field.
/// - accessibilityIdentifier: The accessibility identifier for the text field.
/// - passwordVisibilityAccessibilityId: The accessibility ID for the button to toggle password visibility.
/// - canViewPassword: Whether the password can be viewed.
/// - isPasswordVisible: Whether the password is visible.
/// - placeholder: An optional placeholder to display in the text field.
///
init(
title: String? = nil,
text: Binding<String>,
footer: String? = nil,
accessibilityIdentifier: String? = nil,
passwordVisibilityAccessibilityId: String? = nil,
canViewPassword: Bool = true,
isPasswordVisible: Binding<Bool>? = nil,
placeholder: String? = nil
) {
self.accessibilityIdentifier = accessibilityIdentifier
self.canViewPassword = canViewPassword
self.footer = footer
self.isPasswordVisible = isPasswordVisible
self.passwordVisibilityAccessibilityId = passwordVisibilityAccessibilityId
self.placeholder = placeholder ?? ""
_text = text
self.title = title
trailingContent = nil
}
}
// MARK: Previews
#if DEBUG
#Preview("No buttons") {
VStack {
BitwardenTextField(
title: "Title",
text: .constant("Text field text")
)
.textContentType(.emailAddress)
.padding()
}
.background(Color(.systemGroupedBackground))
}
#Preview("Password button") {
VStack {
BitwardenTextField(
title: "Title",
text: .constant("Text field text"),
isPasswordVisible: .constant(false)
)
.textContentType(.password)
.padding()
}
.background(Color(.systemGroupedBackground))
}
#Preview("Password revealed") {
VStack {
BitwardenTextField(
title: "Title",
text: .constant("Password"),
isPasswordVisible: .constant(true)
)
.textContentType(.password)
.padding()
}
.background(Color(.systemGroupedBackground))
}
#Preview("Additional buttons") {
VStack {
BitwardenTextField(
title: "Title",
text: .constant("Text field text")
) {
AccessoryButton(asset: SharedAsset.Icons.cog16, accessibilityLabel: "") {}
}
.padding()
}
.background(Color(.systemGroupedBackground))
}
#Preview("Footer text") {
VStack {
BitwardenTextField(
title: "Title",
text: .constant("Text field text"),
footer: "Text field footer",
isPasswordVisible: .constant(false)
) {
AccessoryButton(asset: SharedAsset.Icons.cog16, accessibilityLabel: "") {}
}
.padding()
}
.background(Color(.systemGroupedBackground))
}
#endif

View File

@ -1,3 +1,4 @@
import BitwardenKit
import SwiftUI
// MARK: - FormTextField

View File

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

View File

@ -1,10 +1,9 @@
import BitwardenKit
import BitwardenResources
import SwiftUI
/// A view that displays a button for use as an accessory to a field.
///
struct AccessoryButton: View {
public struct AccessoryButton: View {
// MARK: Types
/// A type that wraps a synchrounous or asynchrounous block that is executed by this button.
@ -31,7 +30,7 @@ struct AccessoryButton: View {
/// The image to display in the button.
var asset: SharedImageAsset
var body: some View {
public var body: some View {
switch action {
case let .async(action):
AsyncButton(action: action) {
@ -64,7 +63,7 @@ struct AccessoryButton: View {
/// - accessibilityLabel: The accessibility label of the button.
/// - action: The action to perform when the user triggers the button.
///
init(asset: SharedImageAsset,
public init(asset: SharedImageAsset,
accessibilityLabel: String,
accessibilityIdentifier: String = "",
action: @escaping () -> Void) {
@ -83,7 +82,7 @@ struct AccessoryButton: View {
/// - accessibilityIdentifier: The accessibility identifier of the button.
/// - action: The action to perform when the user triggers the button.
///
init(asset: SharedImageAsset,
public init(asset: SharedImageAsset,
accessibilityLabel: String,
accessibilityIdentifier: String = "",
action: @escaping () async -> Void) {

View File

@ -8,7 +8,7 @@ import SwiftUI
/// contains a value. At that point, the text label will float up above the input field. This is
/// primarily meant to wrap a text field or view.
///
struct BitwardenFloatingTextLabel<Content: View, TrailingContent: View>: View {
public struct BitwardenFloatingTextLabel<Content: View, TrailingContent: View>: View {
// MARK: Properties
/// The primary content containing the text input field for the label.
@ -31,7 +31,7 @@ struct BitwardenFloatingTextLabel<Content: View, TrailingContent: View>: View {
// MARK: View
var body: some View {
public var body: some View {
HStack(spacing: 8) {
ZStack(alignment: showPlaceholder ? .leading : .topLeading) {
// The placeholder and title text which is vertically centered in the view when the
@ -77,7 +77,7 @@ struct BitwardenFloatingTextLabel<Content: View, TrailingContent: View>: View {
/// - trailingContent: Optional trailing content to display on the trailing edge of the label
/// and text input field.
///
init(
public init(
title: String?,
isTextFieldDisabled: Bool = false,
showPlaceholder: Bool,
@ -100,7 +100,7 @@ struct BitwardenFloatingTextLabel<Content: View, TrailingContent: View>: View {
/// the field.
/// - content: The primary content containing the text input field for the label.
///
init(
public init(
title: String?,
isTextFieldDisabled: Bool = false,
showPlaceholder: Bool,

View File

@ -11,7 +11,7 @@ import SwiftUIIntrospect
/// displaying additional content on the trailing edge of the text field.
///
@MainActor
struct BitwardenTextField<FooterContent: View, TrailingContent: View>: View {
public struct BitwardenTextField<FooterContent: View, TrailingContent: View>: View {
// MARK: Private Properties
/// A value indicating whether the textfield is currently enabled or disabled.
@ -68,7 +68,7 @@ struct BitwardenTextField<FooterContent: View, TrailingContent: View>: View {
// MARK: View
var body: some View {
public var body: some View {
VStack(spacing: 0) {
contentView
@ -215,7 +215,7 @@ struct BitwardenTextField<FooterContent: View, TrailingContent: View>: View {
/// - isTextFieldDisabled: Whether the text field is disabled.
/// - trailingContent: Optional content view that is displayed on the trailing edge of the field.
///
init(
public init(
title: String? = nil,
text: Binding<String>,
footer: String? = nil,
@ -254,7 +254,7 @@ struct BitwardenTextField<FooterContent: View, TrailingContent: View>: View {
/// - trailingContent: Optional content view that is displayed on the trailing edge of the field.
/// - footerContent: The (optional) footer content to display underneath the field.
///
init(
public init(
title: String? = nil,
text: Binding<String>,
accessibilityIdentifier: String? = nil,
@ -280,7 +280,7 @@ struct BitwardenTextField<FooterContent: View, TrailingContent: View>: View {
}
}
extension BitwardenTextField where TrailingContent == EmptyView {
public extension BitwardenTextField where TrailingContent == EmptyView {
/// Initializes a new `BitwardenTextField`.
///
/// - Parameters:
@ -321,7 +321,7 @@ extension BitwardenTextField where TrailingContent == EmptyView {
}
}
extension BitwardenTextField where FooterContent == EmptyView, TrailingContent == EmptyView {
public extension BitwardenTextField where FooterContent == EmptyView, TrailingContent == EmptyView {
/// Initializes a new `BitwardenTextField`.
///
/// - Parameters:

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

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

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

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

View File

@ -1,3 +1,4 @@
import BitwardenKit
import SwiftUI
// MARK: - FormTextField

View File

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

View File

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

View File

@ -20,7 +20,7 @@ struct BitwardenTextFieldType: BaseViewType {
static var typePrefix: String = "BitwardenTextField"
static var namespacedPrefixes: [String] = [
"AuthenticatorShared.BitwardenTextField",
"BitwardenKit.BitwardenTextField",
]
}

View File

@ -56,7 +56,7 @@ struct BitwardenTextFieldType: BaseViewType {
static var typePrefix: String = "BitwardenTextField"
static var namespacedPrefixes: [String] = [
"BitwardenShared.BitwardenTextField",
"BitwardenKit.BitwardenTextField",
]
}

View File

@ -125,6 +125,7 @@ targets:
- "**/GoogleService-Info.*.plist"
buildPhase: none
dependencies:
- package: SwiftUIIntrospect
- target: BitwardenResources
- target: Networking
BitwardenKitMocks:

View File

@ -14,10 +14,6 @@ options:
settings:
MARKETING_VERSION: 2024.6.0 # Bump this for a new version update.
CURRENT_PROJECT_VERSION: 1
packages:
SwiftUIIntrospect:
url: https://github.com/siteline/SwiftUI-Introspect
exactVersion: 1.3.0
projectReferences:
BitwardenKit:
path: BitwardenKit.xcodeproj
@ -350,7 +346,6 @@ targets:
buildPhase: none
dependencies:
- package: BitwardenSdk
- package: SwiftUIIntrospect
- target: BitwardenKit/AuthenticatorBridgeKit
- target: BitwardenKit/BitwardenKit
- target: BitwardenKit/BitwardenResources