Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve inputfield native keyboard avoidance #765

Merged
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
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
Loading