// // Copyright 2021-2024 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. // import SwiftUI struct RoundedBorderTextField: View { // MARK: - Properties var title: String? let placeHolder: String @Binding var text: String var footerText: String? var isError = false var isFirstResponder = false var configuration = UIKitTextInputConfiguration() @State var isSecureTextVisible = false var onTextChanged: ((String) -> Void)? var onEditingChanged: ((Bool) -> Void)? var onCommit: (() -> Void)? // MARK: Private @State private var isEditing = false @Environment(\.theme) private var theme: ThemeSwiftUI @Environment(\.isEnabled) private var isEnabled // MARK: Public var body: some View { VStack(alignment: .leading, spacing: -1) { if let title = self.title { Text(title) .foregroundColor(theme.colors.primaryContent) .font(theme.fonts.subheadline) .multilineTextAlignment(.leading) .padding(.bottom, 8) } ZStack(alignment: .leading) { if text.isEmpty { Text(placeHolder) .font(theme.fonts.callout) .foregroundColor(theme.colors.tertiaryContent) .lineLimit(1) .accessibilityHidden(true) } ThemableTextField(placeholder: "", text: $text, configuration: configuration, isSecureTextVisible: $isSecureTextVisible) { isEditing in self.isEditing = isEditing onEditingChanged?(isEditing) } onCommit: { onCommit?() } .makeFirstResponder(isFirstResponder) .addButton(isEnabled) .onChange(of: text) { newText in onTextChanged?(newText) } .frame(height: 30) .allowsHitTesting(isEnabled) .opacity(isEnabled ? 1 : 0.5) } .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: text.isEmpty ? 8 : 0)) .background(RoundedRectangle(cornerRadius: 8).fill(theme.colors.background)) .overlay(RoundedRectangle(cornerRadius: 8).stroke(borderColor, lineWidth: borderWidth)) if let footerText = self.footerText { Text(footerText) .foregroundColor(isError ? theme.colors.alert : theme.colors.tertiaryContent) .font(theme.fonts.footnote) .multilineTextAlignment(.leading) .padding(.top, 8) .transition(.opacity) } } .animation(.easeOut(duration: 0.2)) } /// The text field's border color. private var borderColor: Color { if isEditing { // bwi: 4769 if BWIBuildSettings.shared.useNewBumColors { return Color(ThemeService.shared().theme.tintColor) } else { return Color(hex: 0x108194) } } else if footerText != nil, isError { return theme.colors.alert } else { return theme.colors.quinaryContent } } /// The text field's border width. private var borderWidth: CGFloat { isEditing || (footerText != nil && isError) ? 2 : 1 } } // MARK: - Previews struct TextFieldWithError_Previews: PreviewProvider { static var previews: some View { Group { sampleView.theme(.light).preferredColorScheme(.light) sampleView.theme(.dark).preferredColorScheme(.dark) } .padding() } static var sampleView: some View { VStack(alignment: .center, spacing: 20) { RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant(""), footerText: nil, isError: false) RoundedBorderTextField(placeHolder: "A placeholder", text: .constant("Some text"), footerText: nil, isError: false) RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: "Some error text", isError: true) RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: "Some normal text", isError: false) RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: "Some normal text", isError: false) .disabled(true) Spacer().frame(height: 0) RoundedBorderTextField(title: "Password", placeHolder: "Enter your password", text: .constant(""), configuration: UIKitTextInputConfiguration(isSecureTextEntry: true)) RoundedBorderTextField(title: "Password", placeHolder: "Enter your password", text: .constant("password"), configuration: UIKitTextInputConfiguration(isSecureTextEntry: true)) } } }