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:
Trey t
2025-12-15 21:20:33 -06:00
parent e44bcdd988
commit e7c09f687a
9 changed files with 44 additions and 0 deletions

View File

@@ -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)

View File

@@ -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)

View 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())
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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 {