Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.PasswordVisualTransformation
import com.mirego.pilot.components.PilotTextField
import com.mirego.pilot.components.type.PilotTextContentType
import com.mirego.pilot.components.type.PilotTextObfuscationMode
import com.mirego.pilot.components.ui.PilotFormattedVisualTransformation
import com.mirego.pilot.components.ui.mergeWith
import com.mirego.pilot.components.ui.type.composeValue
Expand Down Expand Up @@ -55,13 +55,19 @@ public fun PilotTextField(
val keyboardType by pilotTextField.keyboardType.collectAsState()
val keyboardReturnKeyType by pilotTextField.keyboardReturnKeyType.collectAsState()
val textContentType by pilotTextField.contentType.collectAsState()
val textObfuscationMode by pilotTextField.textObfuscationMode.collectAsState()

val modifierWithSemantics = textContentType.composeValue?.let { composeContentType ->
modifier.semantics {
contentType = composeContentType
}
} ?: modifier

val visualTransformation = when (textObfuscationMode) {
PilotTextObfuscationMode.Hidden -> PasswordVisualTransformation()
PilotTextObfuscationMode.Visible -> PilotFormattedVisualTransformation(pilotTextField.formatText)
}

OutlinedTextField(
value = textValue,
onValueChange = pilotTextField::onValueChange,
Expand All @@ -80,10 +86,7 @@ public fun PilotTextField(
prefix = prefix,
suffix = suffix,
supportingText = supportingText,
visualTransformation = when (textContentType) {
PilotTextContentType.Password, PilotTextContentType.NewPassword -> PasswordVisualTransformation()
else -> PilotFormattedVisualTransformation(pilotTextField.formatText)
},
visualTransformation = visualTransformation,
isError = isError,
keyboardActions = pilotTextField.mergeWith(keyboardActions),
keyboardOptions = KeyboardOptions(
Expand Down
13 changes: 11 additions & 2 deletions components/common/api/components.api
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ public final class com/mirego/pilot/components/PilotSwitch {

public class com/mirego/pilot/components/PilotTextField {
public static final field $stable I
public fun <init> (Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
public synthetic fun <init> (Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
public synthetic fun <init> (Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getAutoCapitalization ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getAutoCorrect ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getContentType ()Lkotlinx/coroutines/flow/StateFlow;
Expand All @@ -176,6 +176,7 @@ public class com/mirego/pilot/components/PilotTextField {
public final fun getOnReturnKeyTap ()Lkotlin/jvm/functions/Function0;
public final fun getPlaceholder ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getText ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getTextObfuscationMode ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getTransformText ()Lkotlin/jvm/functions/Function1;
public final fun getUnformatText ()Lkotlin/jvm/functions/Function1;
public final fun onValueChange (Ljava/lang/String;)V
Expand Down Expand Up @@ -414,6 +415,14 @@ public final class com/mirego/pilot/components/type/PilotTextContentType : java/
public static fun values ()[Lcom/mirego/pilot/components/type/PilotTextContentType;
}

public final class com/mirego/pilot/components/type/PilotTextObfuscationMode : java/lang/Enum {
public static final field Hidden Lcom/mirego/pilot/components/type/PilotTextObfuscationMode;
public static final field Visible Lcom/mirego/pilot/components/type/PilotTextObfuscationMode;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/mirego/pilot/components/type/PilotTextObfuscationMode;
public static fun values ()[Lcom/mirego/pilot/components/type/PilotTextObfuscationMode;
}

public final class com/mirego/pilot/components/ui/DefaultPilotImageResourceProvider : com/mirego/pilot/components/ui/PilotImageResourceProvider {
public static final field $stable I
public fun <init> ()V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.mirego.pilot.components.type.PilotKeyboardAutoCapitalization
import com.mirego.pilot.components.type.PilotKeyboardReturnKeyType
import com.mirego.pilot.components.type.PilotKeyboardType
import com.mirego.pilot.components.type.PilotTextContentType
import com.mirego.pilot.components.type.PilotTextObfuscationMode
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

Expand All @@ -13,6 +14,7 @@ public open class PilotTextField(
public val keyboardType: StateFlow<PilotKeyboardType> = MutableStateFlow(PilotKeyboardType.Default),
public val keyboardReturnKeyType: StateFlow<PilotKeyboardReturnKeyType> = MutableStateFlow(PilotKeyboardReturnKeyType.Default),
public val contentType: StateFlow<PilotTextContentType> = MutableStateFlow(PilotTextContentType.NotSet),
public val textObfuscationMode: StateFlow<PilotTextObfuscationMode> = MutableStateFlow(PilotTextObfuscationMode.Visible),
public val autoCorrect: StateFlow<Boolean> = MutableStateFlow(true),
public val autoCapitalization: StateFlow<PilotKeyboardAutoCapitalization> = MutableStateFlow(PilotKeyboardAutoCapitalization.Sentences),
public val onReturnKeyTap: () -> Unit = {},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.mirego.pilot.components.type

/**
* Defines how text should be obscured in a secure text field.
*/
public enum class PilotTextObfuscationMode {
/**
* All characters are obscured (replaced with bullets/dots).
*/
Hidden,

/**
* All characters are visible as plain text.
*/
Visible,
}
73 changes: 53 additions & 20 deletions components/ios/base/PilotTextFieldView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,17 @@ public struct PilotTextFieldView<Label>: View where Label: View {
@ObservedObject private var keyboardType: StateObservable<PilotKeyboardType>
@ObservedObject private var keyboardReturnKeyType: StateObservable<PilotKeyboardReturnKeyType>
@ObservedObject private var contentType: StateObservable<PilotTextContentType>
@ObservedObject private var textObfuscationMode: StateObservable<PilotTextObfuscationMode>
@ObservedObject private var autoCorrect: StateObservable<KotlinBoolean>
@ObservedObject private var autoCapitalization: StateObservable<PilotKeyboardAutoCapitalization>

@State private var textFieldText: String
@FocusState private var focusedField: FieldType?

private enum FieldType {
case secure
case plain
}

public init(_ pilotTextField: PilotTextField, placeholderBuilder: @escaping (String) -> Label) {
self.pilotTextField = pilotTextField
Expand All @@ -24,34 +31,60 @@ public struct PilotTextFieldView<Label>: View where Label: View {
_keyboardType = ObservedObject(wrappedValue: StateObservable(pilotTextField.keyboardType))
_keyboardReturnKeyType = ObservedObject(wrappedValue: StateObservable(pilotTextField.keyboardReturnKeyType))
_contentType = ObservedObject(wrappedValue: StateObservable(pilotTextField.contentType))
_textObfuscationMode = ObservedObject(wrappedValue: StateObservable(pilotTextField.textObfuscationMode))
_autoCorrect = ObservedObject(wrappedValue: StateObservable(pilotTextField.autoCorrect))
_autoCapitalization = ObservedObject(wrappedValue: StateObservable(pilotTextField.autoCapitalization))

_textFieldText = State(initialValue: pilotTextField.formatText(pilotTextField.transformText(pilotTextField.text.value)))
}

public var body: some View {
TextField(text: $textFieldText) {
placeholderBuilder(placeholder.value)
}
.onSubmit {
pilotTextField.onReturnKeyTap()
}
.submitLabel(keyboardReturnKeyType.value.submitLabel)
#if canImport(UIKit)
.keyboardType(keyboardType.value.uiKeyboardType)
.autocapitalization(autoCapitalization.value.uiTextAutocapitalizationType)
.textContentType(contentType.value.uiTextContentType)
#endif
.disableAutocorrection(!autoCorrect.value.boolValue)
.textFieldStyle(ExtendedTapAreaTextFieldStyle())
.onChange(of: text.value) { newValue in
textFieldText = pilotTextField.formatText(newValue)
baseTextField
.onSubmit {
pilotTextField.onReturnKeyTap()
}
.submitLabel(keyboardReturnKeyType.value.submitLabel)
#if canImport(UIKit)
.keyboardType(keyboardType.value.uiKeyboardType)
.autocapitalization(autoCapitalization.value.uiTextAutocapitalizationType)
.textContentType(contentType.value.uiTextContentType)
#endif
.disableAutocorrection(!autoCorrect.value.boolValue)
.textFieldStyle(ExtendedTapAreaTextFieldStyle())
.onChange(of: text.value) { newValue in
textFieldText = pilotTextField.formatText(newValue)
}
.onChange(of: textFieldText) { newValue in
let unformattedText = pilotTextField.unformatText(newValue)
textFieldText = pilotTextField.formatText(pilotTextField.transformText(unformattedText))
pilotTextField.onValueChange(text: unformattedText)
}
.onChange(of: textObfuscationMode.value) { newValue in
if focusedField != nil {
withAnimation(nil) {
focusedField = newValue == .hidden ? .secure : .plain
}
}
}
}

@ViewBuilder
private var baseTextField: some View {
ZStack {
SecureField(text: $textFieldText) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we rendering both?

@gbourassa gbourassa Jan 29, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no standard way in SwiftUI to have a secure text field that you can toggle visibility. So the most popular approach seems to be to have both, and only showing one. But rendering one OR the other caused visual glitch on keyboard / focus. Rendering both of them and changing opacity is smoother.

placeholderBuilder(placeholder.value)
}
.opacity(textObfuscationMode.value == .hidden ? 1 : 0)
.focused($focusedField, equals: .secure)

TextField(text: $textFieldText) {
placeholderBuilder(placeholder.value)
}
.opacity(textObfuscationMode.value == .visible ? 1 : 0)
.focused($focusedField, equals: .plain)
}
.onChange(of: textFieldText) { newValue in
let unformattedText = pilotTextField.unformatText(newValue)
textFieldText = pilotTextField.formatText(pilotTextField.transformText(unformattedText))
pilotTextField.onValueChange(text: unformattedText)
.transaction { transaction in
transaction.animation = nil
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions components/ios/base/Types/PilotTextObfuscationMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Shared
import SwiftUI

extension PilotTextObfuscationMode: Equatable {
public static func == (lhs: PilotTextObfuscationMode, rhs: PilotTextObfuscationMode) -> Bool {
switch (lhs, rhs) {
case (.hidden, .hidden), (.visible, .visible):
return true
default:
return false
}
}
}
Loading