Add keyboard dismiss toolbar for iOS numeric and multi-line fields
Creates a reusable KeyboardDismissToolbar view modifier that adds a "Done" button to dismiss keyboards that don't have a return key. Applied to all numeric keyboards (numberPad, decimalPad, phonePad) and multi-line text inputs (TextEditor, TextField with axis: .vertical). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,7 @@ struct ContractorFormSheet: View {
|
||||
TextField(L10n.Contractors.phoneLabel, text: $phone)
|
||||
.keyboardType(.phonePad)
|
||||
.focused($focusedField, equals: .phone)
|
||||
.keyboardDismissToolbar()
|
||||
}
|
||||
|
||||
HStack {
|
||||
@@ -203,6 +204,7 @@ struct ContractorFormSheet: View {
|
||||
.keyboardType(.numberPad)
|
||||
.focused($focusedField, equals: .postalCode)
|
||||
.frame(maxWidth: 100)
|
||||
.keyboardDismissToolbar()
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.Contractors.addressSection)
|
||||
@@ -220,6 +222,7 @@ struct ContractorFormSheet: View {
|
||||
TextEditor(text: $notes)
|
||||
.frame(height: 100)
|
||||
.focused($focusedField, equals: .notes)
|
||||
.keyboardDismissToolbar()
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.Contractors.notesSection)
|
||||
|
||||
@@ -145,6 +145,7 @@ struct DocumentFormView: View {
|
||||
Section(L10n.Documents.warrantyClaims) {
|
||||
TextField(L10n.Documents.claimPhoneOptional, text: $claimPhone)
|
||||
.keyboardType(.phonePad)
|
||||
.keyboardDismissToolbar()
|
||||
TextField(L10n.Documents.claimEmailOptional, text: $claimEmail)
|
||||
.keyboardType(.emailAddress)
|
||||
TextField(L10n.Documents.claimWebsiteOptional, text: $claimWebsite)
|
||||
@@ -327,6 +328,7 @@ struct DocumentFormView: View {
|
||||
|
||||
TextField(L10n.Documents.descriptionOptional, text: $description, axis: .vertical)
|
||||
.lineLimit(3...6)
|
||||
.keyboardDismissToolbar()
|
||||
} header: {
|
||||
Text(L10n.Documents.basicInformation)
|
||||
} footer: {
|
||||
@@ -358,6 +360,7 @@ struct DocumentFormView: View {
|
||||
.textInputAutocapitalization(.never)
|
||||
TextField(L10n.Documents.notesOptional, text: $notes, axis: .vertical)
|
||||
.lineLimit(3...6)
|
||||
.keyboardDismissToolbar()
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
|
||||
28
iosApp/iosApp/Helpers/KeyboardDismissToolbar.swift
Normal file
28
iosApp/iosApp/Helpers/KeyboardDismissToolbar.swift
Normal file
@@ -0,0 +1,28 @@
|
||||
import SwiftUI
|
||||
|
||||
/// A view modifier that adds a keyboard toolbar with a "Done" button to dismiss the keyboard.
|
||||
/// Use this for numeric keyboards (.numberPad, .decimalPad, .phonePad) and multi-line text fields
|
||||
/// that don't have a return key for dismissal.
|
||||
struct KeyboardDismissToolbar: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
Spacer()
|
||||
Button("Done") {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Adds a keyboard toolbar with a "Done" button to dismiss the keyboard.
|
||||
/// Use this for numeric keyboards and multi-line text fields.
|
||||
func keyboardDismissToolbar() -> some View {
|
||||
modifier(KeyboardDismissToolbar())
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ struct OnboardingVerifyEmailContent: View {
|
||||
.textContentType(.oneTimeCode)
|
||||
.focused($isCodeFieldFocused)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.verificationCodeField)
|
||||
.keyboardDismissToolbar()
|
||||
.onChange(of: viewModel.code) { _, newValue in
|
||||
// Limit to 6 digits
|
||||
if newValue.count > 6 {
|
||||
|
||||
@@ -55,6 +55,7 @@ struct VerifyResetCodeView: View {
|
||||
.multilineTextAlignment(.center)
|
||||
.keyboardType(.numberPad)
|
||||
.focused($isCodeFocused)
|
||||
.keyboardDismissToolbar()
|
||||
.onChange(of: viewModel.code) { _, newValue in
|
||||
// Limit to 6 digits
|
||||
if newValue.count > 6 {
|
||||
|
||||
@@ -156,11 +156,13 @@ struct ResidenceFormView: View {
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.yearBuiltField)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
.keyboardDismissToolbar()
|
||||
|
||||
Section(header: Text(L10n.Residences.additionalDetails)) {
|
||||
TextField(L10n.Residences.description, text: $description, axis: .vertical)
|
||||
.lineLimit(3...6)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.descriptionField)
|
||||
.keyboardDismissToolbar()
|
||||
|
||||
Toggle(L10n.Residences.primaryResidence, isOn: $isPrimary)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.isPrimaryToggle)
|
||||
|
||||
@@ -112,6 +112,7 @@ struct CompleteTaskView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
.keyboardDismissToolbar()
|
||||
} label: {
|
||||
Label(L10n.Tasks.actualCost, systemImage: "dollarsign.circle")
|
||||
}
|
||||
@@ -132,6 +133,7 @@ struct CompleteTaskView: View {
|
||||
TextEditor(text: $notes)
|
||||
.frame(minHeight: 100)
|
||||
.scrollContentBackground(.hidden)
|
||||
.keyboardDismissToolbar()
|
||||
}
|
||||
} footer: {
|
||||
Text(L10n.Tasks.optionalNotes)
|
||||
|
||||
@@ -188,6 +188,7 @@ struct TaskFormView: View {
|
||||
TextField(L10n.Tasks.descriptionOptional, text: $description, axis: .vertical)
|
||||
.lineLimit(3...6)
|
||||
.focused($focusedField, equals: .description)
|
||||
.keyboardDismissToolbar()
|
||||
} header: {
|
||||
Text(L10n.Tasks.taskDetails)
|
||||
} footer: {
|
||||
@@ -232,6 +233,7 @@ struct TaskFormView: View {
|
||||
TextField(L10n.Tasks.customInterval, text: $intervalDays)
|
||||
.keyboardType(.numberPad)
|
||||
.focused($focusedField, equals: .intervalDays)
|
||||
.keyboardDismissToolbar()
|
||||
}
|
||||
|
||||
DatePicker(L10n.Tasks.dueDate, selection: $dueDate, displayedComponents: .date)
|
||||
@@ -274,6 +276,7 @@ struct TaskFormView: View {
|
||||
.focused($focusedField, equals: .estimatedCost)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
.keyboardDismissToolbar()
|
||||
|
||||
if let errorMessage = viewModel.errorMessage {
|
||||
Section {
|
||||
|
||||
@@ -67,6 +67,7 @@ struct VerifyEmailView: View {
|
||||
.padding(.horizontal)
|
||||
.focused($isFocused)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.verificationCodeField)
|
||||
.keyboardDismissToolbar()
|
||||
.onChange(of: viewModel.code) { _, newValue in
|
||||
// Limit to 6 digits
|
||||
if newValue.count > 6 {
|
||||
|
||||
Reference in New Issue
Block a user