Skip to content

Commit

Permalink
Merge pull request #765 from kiwicom/763-improve-inputfield-and-texta…
Browse files Browse the repository at this point in the history
…rea-native-keyboard-avoidance

Improve inputfield native keyboard avoidance
  • Loading branch information
PavelHolec authored Nov 15, 2024
2 parents f822178 + 5ee69c2 commit 80f35d8
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 70 deletions.
Binary file modified Snapshots/iPhone/InputFieldTests/testInputFields.1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Snapshots/iPhone/InputFieldTests/testInputFields.2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Snapshots/iPhone/InputFieldTests/testInputFieldsPassword.1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 16 additions & 15 deletions Sources/Orbit/Components/InputField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ public struct InputField<Label: View, Prompt: View, Prefix: View, Suffix: View>:
private let isSecure: Bool
private let passwordStrength: PasswordStrengthIndicator.PasswordStrength?
private let message: Message?
@Binding private var messageHeight: CGFloat

// Builder properties (keyboard related)
var autocapitalizationType: UITextAutocapitalizationType = .none
Expand All @@ -82,7 +81,7 @@ public struct InputField<Label: View, Prompt: View, Prefix: View, Suffix: View>:
var shouldDeleteBackwardAction: (String) -> Bool = { _ in true }

public var body: some View {
FieldWrapper(message: message, messageHeight: $messageHeight) {
FieldWrapper(message: message) {
InputContent(state: state, message: message, isFocused: isFocused) {
textField
} label: {
Expand Down Expand Up @@ -119,7 +118,8 @@ public struct InputField<Label: View, Prompt: View, Prefix: View, Suffix: View>:
isSecureTextEntry: isSecure && isSecureTextRedacted,
state: state,
leadingPadding: .small,
trailingPadding: .small
trailingPadding: .small,
keyboardSpacing: keyboardSpacing
)
.returnKeyType(returnKeyType)
.autocorrectionDisabled(isAutocorrectionDisabled)
Expand All @@ -135,6 +135,12 @@ public struct InputField<Label: View, Prompt: View, Prefix: View, Suffix: View>:
isFocused = false
inputFieldEndEditingAction()
}
// Reverts the additional keyboard spacing used for native keyboard avoidance
.padding(.bottom, -keyboardSpacing)
.overlay(
resolvedPrompt,
alignment: .leadingFirstTextBaseline
)
.accessibility(children: nil) {
label
} value: {
Expand All @@ -155,6 +161,10 @@ public struct InputField<Label: View, Prompt: View, Prefix: View, Suffix: View>:
}
}
}

private var keyboardSpacing: CGFloat {
.medium
}

@ViewBuilder private var defaultLabel: some View {
switch labelStyle {
Expand Down Expand Up @@ -195,15 +205,13 @@ public struct InputField<Label: View, Prompt: View, Prefix: View, Suffix: View>:
///
/// - Parameters:
/// - message: Optional message below the text field.
/// - messageHeight: Binding to the current height of the optional message.
public init(
value: Binding<String>,
state: InputState = .default,
labelStyle: InputLabelStyle = .default,
isSecure: Bool = false,
passwordStrength: PasswordStrengthIndicator.PasswordStrength? = nil,
message: Message? = nil,
messageHeight: Binding<CGFloat> = .constant(0),
@ViewBuilder label: () -> Label,
@ViewBuilder prompt: () -> Prompt = { EmptyView() },
@ViewBuilder prefix: () -> Prefix = { EmptyView() },
Expand All @@ -215,7 +223,6 @@ public struct InputField<Label: View, Prompt: View, Prefix: View, Suffix: View>:
self.isSecure = isSecure
self.passwordStrength = passwordStrength
self.message = message
self._messageHeight = messageHeight
self.label = label()
self.prompt = prompt()
self.prefix = prefix()
Expand All @@ -230,7 +237,6 @@ public extension InputField where Label == Text, Prompt == Text, Prefix == Icon,
///
/// - Parameters:
/// - message: Optional message below the text field.
/// - messageHeight: Binding to the current height of the optional message.
@_disfavoredOverload
init(
_ label: some StringProtocol = String(""),
Expand All @@ -242,17 +248,15 @@ public extension InputField where Label == Text, Prompt == Text, Prefix == Icon,
labelStyle: InputLabelStyle = .default,
isSecure: Bool = false,
passwordStrength: PasswordStrengthIndicator.PasswordStrength? = nil,
message: Message? = nil,
messageHeight: Binding<CGFloat> = .constant(0)
message: Message? = nil
) {
self.init(
value: value,
state: state,
labelStyle: labelStyle,
isSecure: isSecure,
passwordStrength: passwordStrength,
message: message,
messageHeight: messageHeight
message: message
) {
Text(label)
} prompt: {
Expand All @@ -268,7 +272,6 @@ public extension InputField where Label == Text, Prompt == Text, Prefix == Icon,
///
/// - Parameters:
/// - message: Optional message below the text field.
/// - messageHeight: Binding to the current height of the optional message.
@_semantics("swiftui.init_with_localization")
init(
_ label: LocalizedStringKey = "",
Expand All @@ -281,7 +284,6 @@ public extension InputField where Label == Text, Prompt == Text, Prefix == Icon,
isSecure: Bool = false,
passwordStrength: PasswordStrengthIndicator.PasswordStrength? = nil,
message: Message? = nil,
messageHeight: Binding<CGFloat> = .constant(0),
tableName: String? = nil,
bundle: Bundle? = nil,
labelComment: StaticString? = nil
Expand All @@ -292,8 +294,7 @@ public extension InputField where Label == Text, Prompt == Text, Prefix == Icon,
labelStyle: labelStyle,
isSecure: isSecure,
passwordStrength: passwordStrength,
message: message,
messageHeight: messageHeight
message: message
) {
Text(label, tableName: tableName, bundle: bundle)
} prompt: {
Expand Down
14 changes: 3 additions & 11 deletions Sources/Orbit/Components/Select.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ public struct Select<Label: View, Value: View, Prompt: View, Prefix: View, Suffi
@Environment(\.isEnabled) private var isEnabled
@Environment(\.isHapticsEnabled) private var isHapticsEnabled
@Environment(\.textColor) private var textColor

@Binding private var messageHeight: CGFloat

private let state: InputState
private let labelStyle: InputLabelStyle
Expand All @@ -56,7 +54,7 @@ public struct Select<Label: View, Value: View, Prompt: View, Prefix: View, Suffi
@ViewBuilder private let suffix: Suffix

public var body: some View {
FieldWrapper(message: message, messageHeight: $messageHeight) {
FieldWrapper(message: message) {
SwiftUI.Button {
if isHapticsEnabled {
HapticsProvider.sendHapticFeedback(.light(0.5))
Expand Down Expand Up @@ -143,7 +141,6 @@ public struct Select<Label: View, Value: View, Prompt: View, Prefix: View, Suffi
state: InputState = .default,
labelStyle: InputLabelStyle = .default,
message: Message? = nil,
messageHeight: Binding<CGFloat> = .constant(0),
action: @escaping () -> Void,
@ViewBuilder label: () -> Label = { EmptyView() },
@ViewBuilder value: () -> Value = { EmptyView() },
Expand All @@ -154,7 +151,6 @@ public struct Select<Label: View, Value: View, Prompt: View, Prefix: View, Suffi
self.state = state
self.labelStyle = labelStyle
self.message = message
self._messageHeight = messageHeight
self.action = action
self.label = label()
self.value = value()
Expand All @@ -178,14 +174,12 @@ public extension Select where Prefix == Icon, Suffix == Icon, Label == Text, Val
state: InputState = .default,
labelStyle: InputLabelStyle = .default,
message: Message? = nil,
messageHeight: Binding<CGFloat> = .constant(0),
action: @escaping () -> Void
) {
self.init(
state: state,
labelStyle: labelStyle,
message: message,
messageHeight: messageHeight
message: message
) {
action()
} label: {
Expand Down Expand Up @@ -213,7 +207,6 @@ public extension Select where Prefix == Icon, Suffix == Icon, Label == Text, Val
state: InputState = .default,
labelStyle: InputLabelStyle = .default,
message: Message? = nil,
messageHeight: Binding<CGFloat> = .constant(0),
tableName: String? = nil,
bundle: Bundle? = nil,
labelComment: StaticString? = nil,
Expand All @@ -222,8 +215,7 @@ public extension Select where Prefix == Icon, Suffix == Icon, Label == Text, Val
self.init(
state: state,
labelStyle: labelStyle,
message: message,
messageHeight: messageHeight
message: message
) {
action()
} label: {
Expand Down
18 changes: 4 additions & 14 deletions Sources/Orbit/Components/Textarea.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ public struct Textarea<Label: View, Prompt: View>: View, TextFieldBuildable {
private let state: InputState

private let message: Message?
@Binding private var messageHeight: CGFloat
@ViewBuilder private let label: Label
@ViewBuilder private let prompt: Prompt

Expand All @@ -48,7 +47,7 @@ public struct Textarea<Label: View, Prompt: View>: View, TextFieldBuildable {
var shouldDeleteBackwardAction: (String) -> Bool = { _ in true }

public var body: some View {
FieldWrapper(message: message, messageHeight: $messageHeight) {
FieldWrapper(message: message) {
InputContent(state: state, message: message, isFocused: isFocused) {
textView
.alignmentGuide(.firstTextBaseline) { dimension in
Expand Down Expand Up @@ -109,19 +108,16 @@ public struct Textarea<Label: View, Prompt: View>: View, TextFieldBuildable {
///
/// - Parameters:
/// - message: Optional message below the text field.
/// - messageHeight: Binding to the current height of the optional message.
public init(
value: Binding<String>,
state: InputState = .default,
message: Message? = nil,
messageHeight: Binding<CGFloat> = .constant(0),
@ViewBuilder label: () -> Label,
@ViewBuilder prompt: () -> Prompt = { EmptyView() }
) {
self._value = value
self.state = state
self.message = message
self._messageHeight = messageHeight
self.label = label()
self.prompt = prompt()
}
Expand All @@ -134,21 +130,18 @@ public extension Textarea where Label == Text, Prompt == Text {
///
/// - Parameters:
/// - message: Optional message below the text field.
/// - messageHeight: Binding to the current height of the optional message.
@_disfavoredOverload
init(
_ label: some StringProtocol = String(""),
value: Binding<String>,
prompt: some StringProtocol = String(""),
state: InputState = .default,
message: Message? = nil,
messageHeight: Binding<CGFloat> = .constant(0)
message: Message? = nil
) {
self.init(
value: value,
state: state,
message: message,
messageHeight: messageHeight
message: message
) {
Text(label)
} prompt: {
Expand All @@ -160,24 +153,21 @@ public extension Textarea where Label == Text, Prompt == Text {
///
/// - Parameters:
/// - message: Optional message below the text field.
/// - messageHeight: Binding to the current height of the optional message.
@_semantics("swiftui.init_with_localization")
init(
_ label: LocalizedStringKey = "",
value: Binding<String>,
prompt: LocalizedStringKey = "",
state: InputState = .default,
message: Message? = nil,
messageHeight: Binding<CGFloat> = .constant(0),
tableName: String? = nil,
bundle: Bundle? = nil,
labelComment: StaticString? = nil
) {
self.init(
value: value,
state: state,
message: message,
messageHeight: messageHeight
message: message
) {
Text(label, tableName: tableName, bundle: bundle)
} prompt: {
Expand Down
35 changes: 11 additions & 24 deletions Sources/Orbit/Support/Forms/FieldWrapper.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import SwiftUI

/// Orbit support component that orovides label and message around input field.
/// Orbit support component that provides label and message around the form field.
public struct FieldWrapper<Label: View, Content: View, Footer: View>: View {

@Binding private var messageHeight: CGFloat

private let message: Message?
@ViewBuilder private let content: Content
@ViewBuilder private let label: Label
@ViewBuilder private let footer: Footer
@ViewBuilder private let content: Content

public var body: some View {
VStack(alignment: .leading, spacing: 0) {
Expand All @@ -18,27 +16,23 @@ public struct FieldWrapper<Label: View, Content: View, Footer: View>: View {

content

ContentHeightReader(height: $messageHeight) {
VStack(alignment: .leading, spacing: 0) {
footer
VStack(alignment: .leading, spacing: 0) {
footer

FieldMessage(message)
.padding(.top, .xxSmall)
}
FieldMessage(message)
.padding(.top, .xxSmall)
}
}
}

/// Creates Orbit ``FieldWrapper`` around form field content with a custom label and an additional message content.
public init(
message: Message? = nil,
messageHeight: Binding<CGFloat> = .constant(0),
@ViewBuilder content: () -> Content,
@ViewBuilder label: () -> Label,
@ViewBuilder footer: () -> Footer = { EmptyView() }
) {
self.message = message
self._messageHeight = messageHeight
self.content = content()
self.label = label()
self.footer = footer()
Expand All @@ -53,13 +47,11 @@ public extension FieldWrapper where Label == Text {
init(
_ label: some StringProtocol = String(""),
message: Message? = nil,
messageHeight: Binding<CGFloat> = .constant(0),
@ViewBuilder content: () -> Content,
@ViewBuilder footer: () -> Footer = { EmptyView() }
) {
self.init(
message: message,
messageHeight: messageHeight,
content: content,
label: {
Text(label)
Expand All @@ -73,13 +65,11 @@ public extension FieldWrapper where Label == Text {
init(
_ label: LocalizedStringKey = "",
message: Message? = nil,
messageHeight: Binding<CGFloat> = .constant(0),
@ViewBuilder content: () -> Content,
@ViewBuilder footer: () -> Footer = { EmptyView() }
) {
self.init(
message: message,
messageHeight: messageHeight,
content: content,
label: {
Text(label)
Expand Down Expand Up @@ -114,32 +104,29 @@ struct FieldWrapperPreviews: PreviewProvider {
contentPlaceholder
}

StateWrapper((true, true, CGFloat(0), false)) { state in
StateWrapper((true, true, false)) { state in
VStack(alignment: .leading, spacing: .large) {
FieldWrapper(
state.0.wrappedValue ? "Form Field Label" : "",
message: state.1.wrappedValue ? .error("Error message") : .none,
messageHeight: state.2
message: state.1.wrappedValue ? .error("Error message") : .none
) {
contentPlaceholder
}

Text("Message height: \(state.2.wrappedValue)")

HStack(spacing: .medium) {
Button("Toggle label") {
state.0.wrappedValue.toggle()
state.3.wrappedValue.toggle()
state.2.wrappedValue.toggle()
}
Button("Toggle message") {
state.1.wrappedValue.toggle()
state.3.wrappedValue.toggle()
state.2.wrappedValue.toggle()
}
}

Spacer()
}
.animation(.easeOut(duration: 1), value: state.3.wrappedValue)
.animation(.easeOut(duration: 1), value: state.2.wrappedValue)
}
.previewDisplayName("Live preview")
}
Expand Down
Loading

0 comments on commit 80f35d8

Please sign in to comment.