diff --git a/iosApp/iosApp/Components/AuthenticatedImage.swift b/iosApp/iosApp/Components/AuthenticatedImage.swift index 4dafae0..6cb172f 100644 --- a/iosApp/iosApp/Components/AuthenticatedImage.swift +++ b/iosApp/iosApp/Components/AuthenticatedImage.swift @@ -38,6 +38,7 @@ struct AuthenticatedImage: View { Image(uiImage: image) .resizable() .aspectRatio(contentMode: contentMode) + .accessibilityLabel("Image") case .failure: errorView } diff --git a/iosApp/iosApp/Components/TaskSummaryCard.swift b/iosApp/iosApp/Components/TaskSummaryCard.swift index 37ee0ee..339f3fe 100644 --- a/iosApp/iosApp/Components/TaskSummaryCard.swift +++ b/iosApp/iosApp/Components/TaskSummaryCard.swift @@ -27,6 +27,7 @@ struct TaskSummaryCard: View { .font(.headline) .fontWeight(.bold) .foregroundColor(Color.appTextPrimary) + .accessibilityAddTraits(.isHeader) ForEach(filteredCategories, id: \.name) { category in TaskCategoryRow(category: category) @@ -80,6 +81,7 @@ struct TaskCategoryRow: View { .padding(12) .background(categoryColor.opacity(0.1)) .cornerRadius(8) + .accessibilityElement(children: .combine) } } diff --git a/iosApp/iosApp/Contractor/ContractorCard.swift b/iosApp/iosApp/Contractor/ContractorCard.swift index 5a70f59..e64dcf4 100644 --- a/iosApp/iosApp/Contractor/ContractorCard.swift +++ b/iosApp/iosApp/Contractor/ContractorCard.swift @@ -76,11 +76,13 @@ struct ContractorCard: View { .foregroundColor(contractor.isFavorite ? Color.appAccent : Color.appTextSecondary.opacity(0.7)) } .buttonStyle(PlainButtonStyle()) + .accessibilityLabel(contractor.isFavorite ? "Remove \(contractor.name) from favorites" : "Add \(contractor.name) to favorites") // Chevron Image(systemName: "chevron.right") .font(.caption) .foregroundColor(Color.appTextSecondary.opacity(0.7)) + .accessibilityHidden(true) } .padding(AppSpacing.md) .background(Color.appBackgroundSecondary) diff --git a/iosApp/iosApp/Contractor/ContractorDetailView.swift b/iosApp/iosApp/Contractor/ContractorDetailView.swift index 94e526a..6f49532 100644 --- a/iosApp/iosApp/Contractor/ContractorDetailView.swift +++ b/iosApp/iosApp/Contractor/ContractorDetailView.swift @@ -64,6 +64,7 @@ struct ContractorDetailView: View { .foregroundColor(Color.appPrimary) .accessibilityIdentifier(AccessibilityIdentifiers.Contractor.menuButton) } + .accessibilityLabel("Contractor actions") } } } @@ -175,6 +176,7 @@ struct ContractorDetailView: View { Text(contractor.name) .font(.title3.weight(.semibold)) .foregroundColor(Color.appTextPrimary) + .accessibilityAddTraits(.isHeader) // Company if let company = contractor.company { diff --git a/iosApp/iosApp/Contractor/ContractorFormSheet.swift b/iosApp/iosApp/Contractor/ContractorFormSheet.swift index bd1ee69..a38f351 100644 --- a/iosApp/iosApp/Contractor/ContractorFormSheet.swift +++ b/iosApp/iosApp/Contractor/ContractorFormSheet.swift @@ -79,6 +79,7 @@ struct ContractorFormSheet: View { } } header: { Text(L10n.Contractors.basicInfoSection) + .accessibilityAddTraits(.isHeader) } footer: { Text(L10n.Contractors.basicInfoFooter) .font(.caption) @@ -155,6 +156,7 @@ struct ContractorFormSheet: View { } } header: { Text(L10n.Contractors.contactInfoSection) + .accessibilityAddTraits(.isHeader) } .sectionBackground() @@ -226,6 +228,7 @@ struct ContractorFormSheet: View { } } header: { Text(L10n.Contractors.addressSection) + .accessibilityAddTraits(.isHeader) } .sectionBackground() diff --git a/iosApp/iosApp/Contractor/ContractorsListView.swift b/iosApp/iosApp/Contractor/ContractorsListView.swift index 97dfbf7..77129b6 100644 --- a/iosApp/iosApp/Contractor/ContractorsListView.swift +++ b/iosApp/iosApp/Contractor/ContractorsListView.swift @@ -119,6 +119,7 @@ struct ContractorsListView: View { .font(.system(size: 16, weight: .medium)) .foregroundColor(showFavoritesOnly ? Color.appAccent : Color.appTextSecondary) } + .accessibilityLabel(showFavoritesOnly ? "Show all contractors" : "Show favorites only") // Specialty Filter Menu { @@ -142,6 +143,7 @@ struct ContractorsListView: View { .font(.system(size: 16, weight: .medium)) .foregroundColor(selectedSpecialty != nil ? Color.appPrimary : Color.appTextSecondary) } + .accessibilityLabel("Filter by specialty") // Add Button Button(action: { @@ -156,6 +158,7 @@ struct ContractorsListView: View { OrganicToolbarButton(systemName: "plus", isPrimary: true) } .accessibilityIdentifier(AccessibilityIdentifiers.Contractor.addButton) + .accessibilityLabel("Add contractor") } } } @@ -354,10 +357,12 @@ private struct OrganicContractorCard: View { .foregroundColor(contractor.isFavorite ? Color.appAccent : Color.appTextSecondary) } .buttonStyle(.plain) + .accessibilityLabel(contractor.isFavorite ? "Remove \(contractor.name) from favorites" : "Add \(contractor.name) to favorites") Image(systemName: "chevron.right") .font(.system(size: 12, weight: .semibold)) .foregroundColor(Color.appTextSecondary.opacity(0.5)) + .accessibilityHidden(true) } .padding(16) .background( diff --git a/iosApp/iosApp/Documents/Components/DocumentCard.swift b/iosApp/iosApp/Documents/Components/DocumentCard.swift index 50172e4..87bcd52 100644 --- a/iosApp/iosApp/Documents/Components/DocumentCard.swift +++ b/iosApp/iosApp/Documents/Components/DocumentCard.swift @@ -37,7 +37,8 @@ struct DocumentCard: View { .frame(width: 56, height: 56) }) .padding(AppSpacing.md) - + .accessibilityHidden(true) + Spacer() } @@ -77,11 +78,13 @@ struct DocumentCard: View { Image(systemName: "chevron.right") .foregroundColor(Color.appTextSecondary) .font(.system(size: 14)) + .accessibilityHidden(true) } .padding(AppSpacing.md) .background(Color.appBackgroundSecondary) .cornerRadius(AppRadius.md) .shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1) + .accessibilityElement(children: .combine) } private func getDocTypeDisplayName(_ type: String) -> String { diff --git a/iosApp/iosApp/Documents/Components/WarrantyCard.swift b/iosApp/iosApp/Documents/Components/WarrantyCard.swift index 9704602..edab805 100644 --- a/iosApp/iosApp/Documents/Components/WarrantyCard.swift +++ b/iosApp/iosApp/Documents/Components/WarrantyCard.swift @@ -115,6 +115,7 @@ struct WarrantyCard: View { .background(Color.appBackgroundSecondary) .cornerRadius(AppRadius.md) .shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1) + .accessibilityElement(children: .combine) } private func getCategoryDisplayName(_ category: String) -> String { diff --git a/iosApp/iosApp/Documents/DocumentDetailView.swift b/iosApp/iosApp/Documents/DocumentDetailView.swift index bfec17d..96f853f 100644 --- a/iosApp/iosApp/Documents/DocumentDetailView.swift +++ b/iosApp/iosApp/Documents/DocumentDetailView.swift @@ -68,6 +68,7 @@ struct DocumentDetailView: View { Image(systemName: "ellipsis.circle") .accessibilityIdentifier(AccessibilityIdentifiers.Document.menuButton) } + .accessibilityLabel("Document actions") } } } @@ -483,6 +484,7 @@ struct DocumentDetailView: View { Text(title) .font(.system(size: 16, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) + .accessibilityAddTraits(.isHeader) } @ViewBuilder diff --git a/iosApp/iosApp/Documents/DocumentFormView.swift b/iosApp/iosApp/Documents/DocumentFormView.swift index 675031e..b454220 100644 --- a/iosApp/iosApp/Documents/DocumentFormView.swift +++ b/iosApp/iosApp/Documents/DocumentFormView.swift @@ -163,6 +163,7 @@ struct DocumentFormView: View { .accessibilityIdentifier(AccessibilityIdentifiers.Document.providerContactField) } header: { Text(L10n.Documents.warrantyDetails) + .accessibilityAddTraits(.isHeader) } footer: { Text(L10n.Documents.requiredWarrantyFields) .font(.caption) @@ -363,6 +364,7 @@ struct DocumentFormView: View { .keyboardDismissToolbar() } header: { Text(L10n.Documents.basicInformation) + .accessibilityAddTraits(.isHeader) } footer: { Text(L10n.Documents.requiredTitle) .font(.caption) diff --git a/iosApp/iosApp/Documents/DocumentsWarrantiesView.swift b/iosApp/iosApp/Documents/DocumentsWarrantiesView.swift index 000f854..423590b 100644 --- a/iosApp/iosApp/Documents/DocumentsWarrantiesView.swift +++ b/iosApp/iosApp/Documents/DocumentsWarrantiesView.swift @@ -118,6 +118,7 @@ struct DocumentsWarrantiesView: View { .font(.system(size: 16, weight: .medium)) .foregroundColor(showActiveOnly ? Color.appPrimary : Color.appTextSecondary) } + .accessibilityLabel(showActiveOnly ? "Show all warranties" : "Show active warranties only") } // Filter Menu @@ -160,6 +161,7 @@ struct DocumentsWarrantiesView: View { .font(.system(size: 16, weight: .medium)) .foregroundColor((selectedCategory != nil || selectedDocType != nil) ? Color.appPrimary : Color.appTextSecondary) } + .accessibilityLabel("Filter documents") // Add Button Button(action: { @@ -174,6 +176,7 @@ struct DocumentsWarrantiesView: View { OrganicDocToolbarButton() } .accessibilityIdentifier(AccessibilityIdentifiers.Document.addButton) + .accessibilityLabel("Add document") } } } @@ -287,6 +290,8 @@ private struct OrganicSegmentButton: View { .background(isSelected ? Color.appPrimary : Color.clear) .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) } + .accessibilityLabel(title) + .accessibilityAddTraits(isSelected ? .isSelected : []) } } diff --git a/iosApp/iosApp/Helpers/AccessibilityLabels.swift b/iosApp/iosApp/Helpers/AccessibilityLabels.swift new file mode 100644 index 0000000..0b18e36 --- /dev/null +++ b/iosApp/iosApp/Helpers/AccessibilityLabels.swift @@ -0,0 +1,91 @@ +import Foundation + +/// Centralized accessibility labels for VoiceOver +/// These labels provide human-readable descriptions for screen reader users +struct A11y { + + // MARK: - Authentication + struct Auth { + static let loginButton = "Sign in" + static let appleSignIn = "Sign in with Apple" + static let googleSignIn = "Sign in with Google" + static let forgotPassword = "Forgot password" + static let signUp = "Create account" + static let passwordToggle = "Toggle password visibility" + static let appLogo = "honeyDue app logo" + } + + // MARK: - Navigation + struct Navigation { + static let residencesTab = "Properties" + static let tasksTab = "Tasks" + static let contractorsTab = "Contractors" + static let documentsTab = "Documents" + static let settingsButton = "Settings" + static let addButton = "Add" + static let backButton = "Back" + static let closeButton = "Close" + static let editButton = "Edit" + static let deleteButton = "Delete" + static let saveButton = "Save" + static let cancelButton = "Cancel" + } + + // MARK: - Residence + struct Residence { + static func card(name: String, taskCount: Int, overdueCount: Int) -> String { + "\(name), \(taskCount) tasks, \(overdueCount) overdue" + } + static let addProperty = "Add new property" + static let primaryBadge = "Primary property" + static func openInMaps(address: String) -> String { "Open \(address) in Maps" } + static func shareCode(code: String) -> String { "Share code: \(code)" } + static let copyShareCode = "Copy share code" + static let generateShareCode = "Generate new share code" + static func removeUser(name: String) -> String { "Remove \(name) from property" } + } + + // MARK: - Task + struct Task { + static func card(title: String, priority: String, dueDate: String) -> String { + "\(title), \(priority) priority, due \(dueDate)" + } + static let addTask = "Add new task" + static let taskActions = "Task actions" + static func priorityBadge(level: String) -> String { "Priority: \(level)" } + static func statusBadge(status: String) -> String { "Status: \(status)" } + static func completionCount(count: Int) -> String { "View \(count) completions" } + static func rating(value: Int) -> String { "Rated \(value) out of 5" } + static let markInProgress = "Mark as in progress" + static let completeTask = "Complete task" + static let archiveTask = "Archive task" + static let cancelTask = "Cancel task" + } + + // MARK: - Contractor + struct Contractor { + static func card(name: String, company: String?, specialty: String) -> String { + [name, company, specialty].compactMap { $0 }.joined(separator: ", ") + } + static let addContractor = "Add new contractor" + static func toggleFavorite(name: String, isFavorite: Bool) -> String { + isFavorite ? "Remove \(name) from favorites" : "Add \(name) to favorites" + } + } + + // MARK: - Document + struct Document { + static func card(title: String, type: String) -> String { "\(title), \(type)" } + static let addDocument = "Add new document" + } + + // MARK: - Common + struct Common { + static func stat(value: String, label: String) -> String { "\(value) \(label)" } + static let decorative = "" // For .accessibilityHidden(true) + static let retryButton = "Try again" + static let dismissError = "Dismiss error" + static func photo(index: Int) -> String { "Photo \(index)" } + static let removePhoto = "Remove photo" + } +} diff --git a/iosApp/iosApp/Login/LoginView.swift b/iosApp/iosApp/Login/LoginView.swift index 100f4f9..fe36d5f 100644 --- a/iosApp/iosApp/Login/LoginView.swift +++ b/iosApp/iosApp/Login/LoginView.swift @@ -69,6 +69,7 @@ struct LoginView: View { Text(L10n.Auth.welcomeBack) .font(.system(size: 26, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) + .accessibilityAddTraits(.isHeader) Text(L10n.Auth.signInSubtitle) .font(.system(size: 15, weight: .medium)) @@ -148,6 +149,7 @@ struct LoginView: View { action: viewModel.login ) .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.loginButton) + .accessibilityHint("Double tap to sign in") // Divider HStack(spacing: 12) { @@ -180,6 +182,7 @@ struct LoginView: View { } } .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.appleSignInButton) + .accessibilityLabel("Sign in with Apple") // Apple Sign In loading indicator if appleSignInViewModel.isLoading { @@ -216,6 +219,7 @@ struct LoginView: View { ) } .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.googleSignInButton) + .accessibilityLabel("Sign in with Google") // Apple Sign In Error if let appleError = appleSignInViewModel.errorMessage { diff --git a/iosApp/iosApp/Onboarding/OnboardingCoordinator.swift b/iosApp/iosApp/Onboarding/OnboardingCoordinator.swift index 514f428..dc28be5 100644 --- a/iosApp/iosApp/Onboarding/OnboardingCoordinator.swift +++ b/iosApp/iosApp/Onboarding/OnboardingCoordinator.swift @@ -197,6 +197,7 @@ struct OnboardingCoordinator: View { .foregroundColor(Color.appPrimary) } .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.backButton) + .accessibilityLabel("Go back") .frame(width: 44, alignment: .leading) .opacity(showBackButton ? 1 : 0) .disabled(!showBackButton) @@ -207,6 +208,7 @@ struct OnboardingCoordinator: View { if showProgressIndicator { OnboardingProgressIndicator(currentStep: currentProgressStep, totalSteps: 5) .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.progressIndicator) + .accessibilityLabel("Step \(currentProgressStep + 1) of 5") } Spacer() @@ -219,6 +221,7 @@ struct OnboardingCoordinator: View { .foregroundColor(Color.appTextSecondary) } .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.skipButton) + .accessibilityLabel("Skip") .frame(width: 44, alignment: .trailing) .opacity(showSkipButton ? 1 : 0) .disabled(!showSkipButton) diff --git a/iosApp/iosApp/Onboarding/OnboardingCreateAccountView.swift b/iosApp/iosApp/Onboarding/OnboardingCreateAccountView.swift index 8eb4c0f..19d5c5c 100644 --- a/iosApp/iosApp/Onboarding/OnboardingCreateAccountView.swift +++ b/iosApp/iosApp/Onboarding/OnboardingCreateAccountView.swift @@ -36,6 +36,7 @@ struct OnboardingCreateAccountContent: View { var body: some View { ZStack { WarmGradientBackground() + .a11yDecorative() // Decorative blobs GeometryReader { geo in @@ -56,6 +57,7 @@ struct OnboardingCreateAccountContent: View { .offset(x: geo.size.width * 0.55, y: geo.size.height * 0.05) .blur(radius: 20) } + .a11yDecorative() ScrollView(showsIndicators: false) { VStack(spacing: OrganicSpacing.comfortable) { @@ -108,6 +110,7 @@ struct OnboardingCreateAccountContent: View { .foregroundColor(Color.appTextPrimary) .multilineTextAlignment(.center) .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.createAccountTitle) + .a11yHeader() Text("Your data will be synced across devices") .font(.system(size: 15, weight: .medium)) @@ -226,6 +229,7 @@ struct OnboardingCreateAccountContent: View { .textInputAutocapitalization(.never) .autocorrectionDisabled() .textContentType(.username) + .accessibilityHint("Enter a unique username") OrganicOnboardingTextField( icon: "envelope.fill", @@ -239,6 +243,7 @@ struct OnboardingCreateAccountContent: View { .autocorrectionDisabled() .keyboardType(.emailAddress) .textContentType(.emailAddress) + .accessibilityHint("Enter your email address") OrganicOnboardingSecureField( icon: "lock.fill", @@ -248,6 +253,7 @@ struct OnboardingCreateAccountContent: View { accessibilityIdentifier: AccessibilityIdentifiers.Onboarding.passwordField ) .focused($focusedField, equals: .password) + .accessibilityHint("Enter a password with at least 8 characters") OrganicOnboardingSecureField( icon: "lock.fill", @@ -257,6 +263,7 @@ struct OnboardingCreateAccountContent: View { accessibilityIdentifier: AccessibilityIdentifiers.Onboarding.confirmPasswordField ) .focused($focusedField, equals: .confirmPassword) + .accessibilityHint("Re-enter your password to confirm") // Password Requirements if !viewModel.password.isEmpty { diff --git a/iosApp/iosApp/Onboarding/OnboardingFirstTaskView.swift b/iosApp/iosApp/Onboarding/OnboardingFirstTaskView.swift index 7e1048d..e56245a 100644 --- a/iosApp/iosApp/Onboarding/OnboardingFirstTaskView.swift +++ b/iosApp/iosApp/Onboarding/OnboardingFirstTaskView.swift @@ -179,6 +179,7 @@ struct OnboardingFirstTaskContent: View { var body: some View { ZStack { WarmGradientBackground() + .a11yDecorative() // Decorative blobs GeometryReader { geo in @@ -216,6 +217,7 @@ struct OnboardingFirstTaskContent: View { .offset(x: geo.size.width * 0.65, y: geo.size.height * 0.75) .blur(radius: 15) } + .a11yDecorative() VStack(spacing: 0) { ScrollViewReader { proxy in @@ -285,6 +287,7 @@ struct OnboardingFirstTaskContent: View { Text("You're all set up!") .font(.system(size: 26, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) + .a11yHeader() Text(onboardingState.regionalTemplates.isEmpty ? "Let's get you started with some tasks.\nThe more you pick, the more we'll help you remember!" @@ -310,6 +313,7 @@ struct OnboardingFirstTaskContent: View { .background((isAtMaxSelection ? Color.appAccent : Color.appPrimary).opacity(0.1)) .clipShape(Capsule()) .animation(.spring(response: 0.3), value: selectedCount) + .accessibilityLabel("\(selectedCount) of \(maxTasksAllowed) tasks selected") // Task categories VStack(spacing: 12) { @@ -381,6 +385,7 @@ struct OnboardingFirstTaskContent: View { ) } .padding(.horizontal, OrganicSpacing.comfortable) + .a11yButton("Add popular tasks") } .padding(.bottom, 140) // Space for button } @@ -606,6 +611,7 @@ private struct OrganicTaskCategorySection: View { Text(category.name) .font(.system(size: 16, weight: .semibold)) .foregroundColor(Color.appTextPrimary) + .a11yHeader() Spacer() @@ -738,6 +744,8 @@ private struct OrganicTaskTemplateRow: View { } .buttonStyle(.plain) .disabled(isDisabled) + .accessibilityLabel("\(template.title), \(template.frequency.capitalized)") + .accessibilityValue(isSelected ? "selected" : "not selected") } } diff --git a/iosApp/iosApp/Onboarding/OnboardingJoinResidenceView.swift b/iosApp/iosApp/Onboarding/OnboardingJoinResidenceView.swift index 0fc92d0..1270772 100644 --- a/iosApp/iosApp/Onboarding/OnboardingJoinResidenceView.swift +++ b/iosApp/iosApp/Onboarding/OnboardingJoinResidenceView.swift @@ -20,6 +20,7 @@ struct OnboardingJoinResidenceContent: View { var body: some View { ZStack { WarmGradientBackground() + .a11yDecorative() // Decorative blobs GeometryReader { geo in @@ -57,6 +58,7 @@ struct OnboardingJoinResidenceContent: View { .offset(x: geo.size.width * 0.65, y: geo.size.height * 0.6) .blur(radius: 20) } + .a11yDecorative() VStack(spacing: 0) { Spacer() @@ -110,6 +112,7 @@ struct OnboardingJoinResidenceContent: View { Text("Join a Residence") .font(.system(size: 26, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) + .a11yHeader() Text("Enter the 6-character code shared with you to join an existing home.") .font(.system(size: 15, weight: .medium)) @@ -137,6 +140,7 @@ struct OnboardingJoinResidenceContent: View { .textInputAutocapitalization(.characters) .autocorrectionDisabled() .focused($isCodeFieldFocused) + .accessibilityHint("Enter 6-character share code") .onChange(of: shareCode) { _, newValue in // Limit to 6 characters if newValue.count > 6 { diff --git a/iosApp/iosApp/Onboarding/OnboardingLocationView.swift b/iosApp/iosApp/Onboarding/OnboardingLocationView.swift index 562e9a3..09dfc36 100644 --- a/iosApp/iosApp/Onboarding/OnboardingLocationView.swift +++ b/iosApp/iosApp/Onboarding/OnboardingLocationView.swift @@ -17,6 +17,7 @@ struct OnboardingLocationContent: View { var body: some View { ZStack { WarmGradientBackground() + .a11yDecorative() // Decorative blobs GeometryReader { geo in @@ -54,6 +55,7 @@ struct OnboardingLocationContent: View { .offset(x: geo.size.width * 0.65, y: geo.size.height * 0.75) .blur(radius: 15) } + .a11yDecorative() VStack(spacing: 0) { Spacer() @@ -125,6 +127,7 @@ struct OnboardingLocationContent: View { .font(.system(size: 26, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) .multilineTextAlignment(.center) + .a11yHeader() Text("Enter your ZIP code so we can suggest\nmaintenance tasks for your climate region.") .font(.system(size: 15, weight: .medium)) @@ -163,6 +166,7 @@ struct OnboardingLocationContent: View { .keyboardType(.numberPad) .focused($isTextFieldFocused) .multilineTextAlignment(.center) + .accessibilityHint("Enter your ZIP code") .onChange(of: zipCode) { _, newValue in // Only allow digits, max 5 let filtered = String(newValue.filter(\.isNumber).prefix(5)) diff --git a/iosApp/iosApp/Onboarding/OnboardingNameResidenceView.swift b/iosApp/iosApp/Onboarding/OnboardingNameResidenceView.swift index fb67045..8903341 100644 --- a/iosApp/iosApp/Onboarding/OnboardingNameResidenceView.swift +++ b/iosApp/iosApp/Onboarding/OnboardingNameResidenceView.swift @@ -24,6 +24,7 @@ struct OnboardingNameResidenceContent: View { var body: some View { ZStack { WarmGradientBackground() + .a11yDecorative() // Decorative blobs GeometryReader { geo in @@ -61,6 +62,7 @@ struct OnboardingNameResidenceContent: View { .offset(x: geo.size.width * 0.55, y: geo.size.height * 0.6) .blur(radius: 20) } + .a11yDecorative() VStack(spacing: 0) { Spacer() @@ -124,6 +126,7 @@ struct OnboardingNameResidenceContent: View { .foregroundColor(Color.appTextPrimary) .multilineTextAlignment(.center) .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.nameResidenceTitle) + .a11yHeader() Text("Don't worry, nothing's written in stone here.\nYou can always change it later in the app.") .font(.system(size: 15, weight: .medium)) @@ -166,6 +169,7 @@ struct OnboardingNameResidenceContent: View { .focused($isTextFieldFocused) .submitLabel(.continue) .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.residenceNameField) + .accessibilityHint("Enter the name of your property") .onSubmit { if isValid { onContinue() diff --git a/iosApp/iosApp/Onboarding/OnboardingSubscriptionView.swift b/iosApp/iosApp/Onboarding/OnboardingSubscriptionView.swift index d351397..c32b941 100644 --- a/iosApp/iosApp/Onboarding/OnboardingSubscriptionView.swift +++ b/iosApp/iosApp/Onboarding/OnboardingSubscriptionView.swift @@ -54,6 +54,7 @@ struct OnboardingSubscriptionContent: View { var body: some View { ZStack { WarmGradientBackground() + .a11yDecorative() // Decorative blobs GeometryReader { geo in @@ -91,6 +92,7 @@ struct OnboardingSubscriptionContent: View { .offset(x: geo.size.width * 0.65, y: geo.size.height * 0.7) .blur(radius: 20) } + .a11yDecorative() ScrollView(showsIndicators: false) { VStack(spacing: OrganicSpacing.comfortable) { @@ -176,6 +178,7 @@ struct OnboardingSubscriptionContent: View { .font(.system(size: 14, weight: .medium)) .foregroundColor(Color.appTextSecondary) } + .accessibilityLabel("Rated 4.9 stars by 10K+ homeowners") } .padding(.top, OrganicSpacing.comfortable) @@ -272,6 +275,7 @@ struct OnboardingSubscriptionContent: View { .naturalShadow(.medium) } .disabled(isLoading) + .a11yButton("Start free trial") // Continue without Button(action: { @@ -281,6 +285,7 @@ struct OnboardingSubscriptionContent: View { .font(.system(size: 15, weight: .semibold)) .foregroundColor(Color.appTextSecondary) } + .a11yButton("Continue with free plan") // Legal text VStack(spacing: 4) { @@ -509,6 +514,8 @@ private struct OrganicPricingPlanCard: View { } .buttonStyle(.plain) .animation(.easeInOut(duration: 0.2), value: isSelected) + .accessibilityLabel("\(plan.title) plan, \(displayPrice ?? plan.price)\(plan.period)\(plan.savings.map { ", \($0)" } ?? "")") + .accessibilityValue(isSelected ? "Selected" : "") } } @@ -557,6 +564,7 @@ private struct OrganicSubscriptionBenefitRow: View { .foregroundColor(.white) } .naturalShadow(.subtle) + .a11yDecorative() VStack(alignment: .leading, spacing: 2) { Text(benefit.title) @@ -574,6 +582,7 @@ private struct OrganicSubscriptionBenefitRow: View { Image(systemName: "checkmark") .font(.system(size: 12, weight: .bold)) .foregroundColor(benefit.gradient[0]) + .a11yDecorative() } .padding(.horizontal, 4) .padding(.vertical, 6) diff --git a/iosApp/iosApp/Onboarding/OnboardingValuePropsView.swift b/iosApp/iosApp/Onboarding/OnboardingValuePropsView.swift index c04af2f..d789f06 100644 --- a/iosApp/iosApp/Onboarding/OnboardingValuePropsView.swift +++ b/iosApp/iosApp/Onboarding/OnboardingValuePropsView.swift @@ -57,6 +57,7 @@ struct OnboardingValuePropsContent: View { var body: some View { ZStack { WarmGradientBackground() + .a11yDecorative() VStack(spacing: 0) { // Feature cards in a tab view @@ -105,6 +106,7 @@ struct OnboardingValuePropsContent: View { .naturalShadow(.medium) } .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.valuePropsNextButton) + .accessibilityHint("Double tap to continue to the next step") .padding(.horizontal, OrganicSpacing.comfortable) .padding(.bottom, OrganicSpacing.airy) } @@ -153,6 +155,7 @@ struct OrganicFeatureCard: View { .frame(width: 180, height: 180) .scaleEffect(appeared ? 1 : 0.8) .opacity(appeared ? 1 : 0) + .a11yDecorative() // Icon circle ZStack { @@ -181,6 +184,7 @@ struct OrganicFeatureCard: View { .font(.system(size: 24, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) .multilineTextAlignment(.center) + .a11yHeader() Text(feature.subtitle) .font(.system(size: 15, weight: .semibold)) diff --git a/iosApp/iosApp/Onboarding/OnboardingVerifyEmailView.swift b/iosApp/iosApp/Onboarding/OnboardingVerifyEmailView.swift index e407fc0..2349998 100644 --- a/iosApp/iosApp/Onboarding/OnboardingVerifyEmailView.swift +++ b/iosApp/iosApp/Onboarding/OnboardingVerifyEmailView.swift @@ -14,6 +14,7 @@ struct OnboardingVerifyEmailContent: View { var body: some View { ZStack { WarmGradientBackground() + .a11yDecorative() // Decorative blobs GeometryReader { geo in @@ -51,6 +52,7 @@ struct OnboardingVerifyEmailContent: View { .offset(x: geo.size.width * 0.6, y: geo.size.height * 0.65) .blur(radius: 15) } + .a11yDecorative() VStack(spacing: 0) { Spacer() @@ -105,6 +107,7 @@ struct OnboardingVerifyEmailContent: View { .font(.system(size: 26, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.verifyEmailTitle) + .a11yHeader() Text("We sent a 6-digit code to your email address. Enter it below to verify your account.") .font(.system(size: 15, weight: .medium)) @@ -133,6 +136,7 @@ struct OnboardingVerifyEmailContent: View { .textContentType(.oneTimeCode) .focused($isCodeFieldFocused) .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.verificationCodeField) + .accessibilityHint("Enter 6-digit verification code") .keyboardDismissToolbar() .onChange(of: viewModel.code) { _, newValue in // Filter to digits only and truncate to 6 in one pass to prevent re-triggering diff --git a/iosApp/iosApp/Onboarding/OnboardingWelcomeView.swift b/iosApp/iosApp/Onboarding/OnboardingWelcomeView.swift index 58b96ca..0f6b985 100644 --- a/iosApp/iosApp/Onboarding/OnboardingWelcomeView.swift +++ b/iosApp/iosApp/Onboarding/OnboardingWelcomeView.swift @@ -14,6 +14,7 @@ struct OnboardingWelcomeView: View { var body: some View { ZStack { WarmGradientBackground() + .a11yDecorative() // Decorative blobs GeometryReader { geo in @@ -51,6 +52,7 @@ struct OnboardingWelcomeView: View { .offset(x: geo.size.width * 0.6, y: geo.size.height * 0.65) .blur(radius: 25) } + .a11yDecorative() VStack(spacing: 0) { Spacer() @@ -99,6 +101,7 @@ struct OnboardingWelcomeView: View { .font(.system(size: 32, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.welcomeTitle) + .a11yHeader() Text("Your home maintenance companion") .font(.system(size: 17, weight: .medium)) @@ -136,6 +139,7 @@ struct OnboardingWelcomeView: View { .naturalShadow(.medium) } .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.startFreshButton) + .accessibilityHint("Double tap to start setting up your property") // Secondary CTA - Join Existing Button(action: onJoinExisting) { @@ -156,6 +160,7 @@ struct OnboardingWelcomeView: View { ) } .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.joinExistingButton) + .accessibilityHint("Double tap to join an existing property with a share code") // Returning user login Button(action: { @@ -166,6 +171,7 @@ struct OnboardingWelcomeView: View { .foregroundColor(Color.appTextSecondary) } .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.loginButton) + .accessibilityHint("Double tap to log in to your existing account") .padding(.top, 8) } .padding(.horizontal, OrganicSpacing.comfortable) @@ -179,6 +185,7 @@ struct OnboardingWelcomeView: View { } .opacity(0.5) .padding(.bottom, 20) + .a11yDecorative() } } diff --git a/iosApp/iosApp/PasswordReset/ForgotPasswordView.swift b/iosApp/iosApp/PasswordReset/ForgotPasswordView.swift index f0e5c93..ed31cf1 100644 --- a/iosApp/iosApp/PasswordReset/ForgotPasswordView.swift +++ b/iosApp/iosApp/PasswordReset/ForgotPasswordView.swift @@ -41,6 +41,7 @@ struct ForgotPasswordView: View { Text("Forgot Password?") .font(.system(size: 26, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) + .a11yHeader() Text("Enter your email address and we'll send you a verification code") .font(.system(size: 15, weight: .medium)) @@ -83,6 +84,7 @@ struct ForgotPasswordView: View { viewModel.clearError() } .accessibilityIdentifier(AccessibilityIdentifiers.PasswordReset.emailField) + .accessibilityHint("Enter your account email address") } .padding(16) .background(Color.appBackgroundPrimary.opacity(0.5)) @@ -160,6 +162,7 @@ struct ForgotPasswordView: View { } .disabled(viewModel.email.isEmpty || viewModel.isLoading) .accessibilityIdentifier(AccessibilityIdentifiers.PasswordReset.sendCodeButton) + .accessibilityHint("Double tap to send a verification code to your email") // Back to Login Button(action: { dismiss() }) { diff --git a/iosApp/iosApp/PasswordReset/ResetPasswordView.swift b/iosApp/iosApp/PasswordReset/ResetPasswordView.swift index 1103320..0751c4c 100644 --- a/iosApp/iosApp/PasswordReset/ResetPasswordView.swift +++ b/iosApp/iosApp/PasswordReset/ResetPasswordView.swift @@ -68,6 +68,7 @@ struct ResetPasswordView: View { Text("Set New Password") .font(.system(size: 26, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) + .a11yHeader() Text("Create a strong password to secure your account") .font(.system(size: 15, weight: .medium)) diff --git a/iosApp/iosApp/PasswordReset/VerifyResetCodeView.swift b/iosApp/iosApp/PasswordReset/VerifyResetCodeView.swift index ad98a3a..24658b2 100644 --- a/iosApp/iosApp/PasswordReset/VerifyResetCodeView.swift +++ b/iosApp/iosApp/PasswordReset/VerifyResetCodeView.swift @@ -41,6 +41,7 @@ struct VerifyResetCodeView: View { Text("Check Your Email") .font(.system(size: 26, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) + .a11yHeader() Text("We sent a 6-digit code to") .font(.system(size: 15, weight: .medium)) @@ -89,6 +90,7 @@ struct VerifyResetCodeView: View { .focused($isCodeFocused) .keyboardDismissToolbar() .accessibilityIdentifier(AccessibilityIdentifiers.PasswordReset.codeField) + .accessibilityHint("Enter 6-digit verification code from your email") .padding(20) .background(Color.appBackgroundPrimary.opacity(0.5)) .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) diff --git a/iosApp/iosApp/Profile/NotificationPreferencesView.swift b/iosApp/iosApp/Profile/NotificationPreferencesView.swift index e967128..4b50f2c 100644 --- a/iosApp/iosApp/Profile/NotificationPreferencesView.swift +++ b/iosApp/iosApp/Profile/NotificationPreferencesView.swift @@ -40,6 +40,7 @@ struct NotificationPreferencesView: View { Text(L10n.Profile.notificationPreferences) .font(.system(size: 22, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) + .a11yHeader() Text(L10n.Profile.notificationPreferencesSubtitle) .font(.system(size: 14, weight: .medium)) @@ -97,6 +98,7 @@ struct NotificationPreferencesView: View { } .tint(Color.appPrimary) .accessibilityIdentifier("Notifications.TaskDueSoon") + .accessibilityHint("Get notified when tasks are due soon") .onChange(of: viewModel.taskDueSoon) { _, newValue in guard !isInitialLoad else { return } viewModel.updatePreference(taskDueSoon: newValue) @@ -133,6 +135,7 @@ struct NotificationPreferencesView: View { } .tint(Color.appPrimary) .accessibilityIdentifier("Notifications.TaskOverdue") + .accessibilityHint("Get notified when tasks are overdue") .onChange(of: viewModel.taskOverdue) { _, newValue in guard !isInitialLoad else { return } viewModel.updatePreference(taskOverdue: newValue) @@ -169,6 +172,7 @@ struct NotificationPreferencesView: View { } .tint(Color.appPrimary) .accessibilityIdentifier("Notifications.TaskCompleted") + .accessibilityHint("Get notified when tasks are completed by others") .onChange(of: viewModel.taskCompleted) { _, newValue in guard !isInitialLoad else { return } viewModel.updatePreference(taskCompleted: newValue) @@ -190,6 +194,7 @@ struct NotificationPreferencesView: View { } .tint(Color.appPrimary) .accessibilityIdentifier("Notifications.TaskAssigned") + .accessibilityHint("Get notified when tasks are assigned to you") .onChange(of: viewModel.taskAssigned) { _, newValue in guard !isInitialLoad else { return } viewModel.updatePreference(taskAssigned: newValue) @@ -225,6 +230,7 @@ struct NotificationPreferencesView: View { } .tint(Color.appPrimary) .accessibilityIdentifier("Notifications.ResidenceShared") + .accessibilityHint("Get notified when someone joins your property") .onChange(of: viewModel.residenceShared) { _, newValue in guard !isInitialLoad else { return } viewModel.updatePreference(residenceShared: newValue) @@ -246,6 +252,7 @@ struct NotificationPreferencesView: View { } .tint(Color.appPrimary) .accessibilityIdentifier("Notifications.WarrantyExpiring") + .accessibilityHint("Get notified when warranties are about to expire") .onChange(of: viewModel.warrantyExpiring) { _, newValue in guard !isInitialLoad else { return } viewModel.updatePreference(warrantyExpiring: newValue) @@ -267,6 +274,7 @@ struct NotificationPreferencesView: View { } .tint(Color.appPrimary) .accessibilityIdentifier("Notifications.DailyDigest") + .accessibilityHint("Receive a daily summary of upcoming tasks") .onChange(of: viewModel.dailyDigest) { _, newValue in guard !isInitialLoad else { return } viewModel.updatePreference(dailyDigest: newValue) @@ -309,6 +317,7 @@ struct NotificationPreferencesView: View { } .tint(Color.appPrimary) .accessibilityIdentifier("Notifications.EmailTaskCompleted") + .accessibilityHint("Receive email notifications when tasks are completed") .onChange(of: viewModel.emailTaskCompleted) { _, newValue in guard !isInitialLoad else { return } viewModel.updatePreference(emailTaskCompleted: newValue) diff --git a/iosApp/iosApp/Profile/ProfileTabView.swift b/iosApp/iosApp/Profile/ProfileTabView.swift index 2aef86a..696f2ea 100644 --- a/iosApp/iosApp/Profile/ProfileTabView.swift +++ b/iosApp/iosApp/Profile/ProfileTabView.swift @@ -56,8 +56,10 @@ struct ProfileTabView: View { Image(systemName: "chevron.right") .font(.caption) .foregroundColor(Color.appTextSecondary) + .a11yDecorative() } } + .accessibilityLabel(L10n.Profile.notifications) NavigationLink(destination: Text(L10n.Profile.privacy)) { Label(L10n.Profile.privacy, systemImage: "lock.shield") @@ -191,8 +193,10 @@ struct ProfileTabView: View { Image(systemName: "chevron.right") .font(.caption) .foregroundColor(Color.appTextSecondary) + .a11yDecorative() } } + .accessibilityLabel("\(L10n.Profile.theme), \(themeManager.currentTheme.displayName)") Button(action: { showingAnimationTesting = true @@ -210,8 +214,10 @@ struct ProfileTabView: View { Image(systemName: "chevron.right") .font(.caption) .foregroundColor(Color.appTextSecondary) + .a11yDecorative() } } + .accessibilityLabel("Completion Animation, \(animationPreference.selectedAnimation.rawValue)") } .sectionBackground() @@ -259,8 +265,11 @@ struct ProfileTabView: View { Image(systemName: "arrow.up.right") .font(.caption) .foregroundColor(Color.appTextSecondary) + .a11yDecorative() } } + .accessibilityLabel(L10n.Profile.contactSupport) + .accessibilityHint("Opens email to contact support") } .sectionBackground() diff --git a/iosApp/iosApp/Profile/ProfileView.swift b/iosApp/iosApp/Profile/ProfileView.swift index 9205203..ad99a92 100644 --- a/iosApp/iosApp/Profile/ProfileView.swift +++ b/iosApp/iosApp/Profile/ProfileView.swift @@ -57,6 +57,7 @@ struct ProfileView: View { Text(L10n.Profile.profileSettings) .font(.system(size: 22, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) + .a11yHeader() } .frame(maxWidth: .infinity) .padding(.vertical) @@ -73,6 +74,7 @@ struct ProfileView: View { focusedField = .lastName } .accessibilityIdentifier("Profile.FirstNameField") + .accessibilityHint("Enter your first name") TextField(L10n.Profile.lastName, text: $viewModel.lastName) .textInputAutocapitalization(.words) @@ -83,6 +85,7 @@ struct ProfileView: View { focusedField = .email } .accessibilityIdentifier("Profile.LastNameField") + .accessibilityHint("Enter your last name") } header: { Text(L10n.Profile.personalInformation) } @@ -99,6 +102,7 @@ struct ProfileView: View { viewModel.updateProfile() } .accessibilityIdentifier("Profile.EmailField") + .accessibilityHint("Enter your email address") } header: { Text(L10n.Profile.contact) } footer: { diff --git a/iosApp/iosApp/Profile/ThemeSelectionView.swift b/iosApp/iosApp/Profile/ThemeSelectionView.swift index 4d3e917..1bb9546 100644 --- a/iosApp/iosApp/Profile/ThemeSelectionView.swift +++ b/iosApp/iosApp/Profile/ThemeSelectionView.swift @@ -147,6 +147,8 @@ struct ThemeRow: View { } .padding(.vertical, 6) .contentShape(Rectangle()) + .accessibilityLabel(theme.displayName) + .accessibilityValue(isSelected ? "Selected" : "") } } diff --git a/iosApp/iosApp/Register/RegisterView.swift b/iosApp/iosApp/Register/RegisterView.swift index 8fd263b..42e2be4 100644 --- a/iosApp/iosApp/Register/RegisterView.swift +++ b/iosApp/iosApp/Register/RegisterView.swift @@ -64,6 +64,7 @@ struct RegisterView: View { Text(L10n.Auth.joinhoneyDue) .font(.system(size: 26, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) + .accessibilityAddTraits(.isHeader) Text(L10n.Auth.startManaging) .font(.system(size: 15, weight: .medium)) @@ -201,6 +202,7 @@ struct RegisterView: View { } .disabled(!isFormValid || viewModel.isLoading) .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerButton) + .accessibilityHint("Double tap to create account") // Login Link HStack(spacing: 6) { @@ -350,6 +352,7 @@ private struct OrganicSecureField: View { .font(.system(size: 16, weight: .medium)) .foregroundColor(Color.appTextSecondary) } + .accessibilityLabel("Toggle password visibility") } .padding(16) .background(Color.appBackgroundPrimary.opacity(0.5)) diff --git a/iosApp/iosApp/Residence/ResidenceDetailView.swift b/iosApp/iosApp/Residence/ResidenceDetailView.swift index 41a7545..2c09fd5 100644 --- a/iosApp/iosApp/Residence/ResidenceDetailView.swift +++ b/iosApp/iosApp/Residence/ResidenceDetailView.swift @@ -295,6 +295,7 @@ private extension ResidenceDetailView { Text(L10n.Residences.contractors) .font(.system(size: 20, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) + .accessibilityAddTraits(.isHeader) Spacer() } @@ -351,10 +352,11 @@ private extension ResidenceDetailView { showEditResidence = true } .accessibilityIdentifier(AccessibilityIdentifiers.Residence.editButton) + .accessibilityLabel("Edit property") } } } - + @ToolbarContentBuilder var trailingToolbar: some ToolbarContent { ToolbarItemGroup(placement: .navigationBarTrailing) { @@ -369,6 +371,7 @@ private extension ResidenceDetailView { } } .disabled(viewModel.isGeneratingReport) + .accessibilityLabel("Generate maintenance report") } // Manage Users button (owner only) - includes share code generation and easy share @@ -383,6 +386,7 @@ private extension ResidenceDetailView { } label: { Image(systemName: "person.2") } + .accessibilityLabel("Manage users") } Button { @@ -398,6 +402,7 @@ private extension ResidenceDetailView { Image(systemName: "plus") } .accessibilityIdentifier(AccessibilityIdentifiers.Task.addButton) + .accessibilityLabel("Add task") if let residence = viewModel.selectedResidence, isCurrentUserOwner(of: residence) { Button { @@ -407,6 +412,7 @@ private extension ResidenceDetailView { .foregroundStyle(Color.appError) } .accessibilityIdentifier(AccessibilityIdentifiers.Residence.deleteButton) + .accessibilityLabel("Delete property") } } } diff --git a/iosApp/iosApp/Residence/ResidencesListView.swift b/iosApp/iosApp/Residence/ResidencesListView.swift index ca153f8..77209aa 100644 --- a/iosApp/iosApp/Residence/ResidencesListView.swift +++ b/iosApp/iosApp/Residence/ResidencesListView.swift @@ -55,6 +55,7 @@ struct ResidencesListView: View { OrganicToolbarButton(systemName: "gearshape.fill") } .accessibilityIdentifier(AccessibilityIdentifiers.Navigation.settingsButton) + .accessibilityLabel("Settings") } ToolbarItemGroup(placement: .navigationBarTrailing) { @@ -69,6 +70,7 @@ struct ResidencesListView: View { }) { OrganicToolbarButton(systemName: "person.badge.plus") } + .accessibilityLabel("Join a property") Button(action: { // Check if we should show upgrade prompt before adding @@ -82,6 +84,7 @@ struct ResidencesListView: View { OrganicToolbarButton(systemName: "plus", isPrimary: true) } .accessibilityIdentifier(AccessibilityIdentifiers.Residence.addButton) + .accessibilityLabel("Add new property") } } .sheet(isPresented: $showingAddResidence) { diff --git a/iosApp/iosApp/ResidenceFormView.swift b/iosApp/iosApp/ResidenceFormView.swift index 8a8d00d..1231a40 100644 --- a/iosApp/iosApp/ResidenceFormView.swift +++ b/iosApp/iosApp/ResidenceFormView.swift @@ -80,6 +80,7 @@ struct ResidenceFormView: View { .accessibilityIdentifier(AccessibilityIdentifiers.Residence.propertyTypePicker) } header: { Text(L10n.Residences.propertyDetails) + .accessibilityAddTraits(.isHeader) } footer: { Text(L10n.Residences.nameRequired) .font(.caption) @@ -131,6 +132,7 @@ struct ResidenceFormView: View { .accessibilityIdentifier(AccessibilityIdentifiers.Residence.countryField) } header: { Text(L10n.Residences.address) + .accessibilityAddTraits(.isHeader) } .sectionBackground() @@ -162,6 +164,7 @@ struct ResidenceFormView: View { .accessibilityIdentifier(AccessibilityIdentifiers.Residence.yearBuiltField) } header: { Text(L10n.Residences.propertyFeatures) + .accessibilityAddTraits(.isHeader) } .sectionBackground() @@ -175,6 +178,7 @@ struct ResidenceFormView: View { .accessibilityIdentifier(AccessibilityIdentifiers.Residence.isPrimaryToggle) } header: { Text(L10n.Residences.additionalDetails) + .accessibilityAddTraits(.isHeader) } .sectionBackground() diff --git a/iosApp/iosApp/Shared/Components/ButtonStyles.swift b/iosApp/iosApp/Shared/Components/ButtonStyles.swift index 8ec85bd..0bb25eb 100644 --- a/iosApp/iosApp/Shared/Components/ButtonStyles.swift +++ b/iosApp/iosApp/Shared/Components/ButtonStyles.swift @@ -50,6 +50,7 @@ struct PrimaryButton: View { .cornerRadius(AppRadius.md) } .disabled(isDisabled || isLoading) + .accessibilityValue(isLoading ? "Loading" : "") } } @@ -296,5 +297,6 @@ struct OrganicPrimaryButton: View { ) } .disabled(isDisabled || isLoading) + .accessibilityValue(isLoading ? "Loading" : "") } } diff --git a/iosApp/iosApp/Shared/Components/FormComponents.swift b/iosApp/iosApp/Shared/Components/FormComponents.swift index 818784a..48d7dd6 100644 --- a/iosApp/iosApp/Shared/Components/FormComponents.swift +++ b/iosApp/iosApp/Shared/Components/FormComponents.swift @@ -257,6 +257,7 @@ struct IconTextField: View { .font(.system(size: 14, weight: .medium)) .foregroundColor(Color.appPrimary) } + .accessibilityHidden(true) if isSecure { SecureField(placeholder, text: $text) @@ -311,6 +312,7 @@ struct SecureIconTextField: View { .font(.system(size: 14, weight: .medium)) .foregroundColor(Color.appPrimary) } + .accessibilityHidden(true) Group { if isVisible { @@ -332,6 +334,7 @@ struct SecureIconTextField: View { .font(.system(size: 16, weight: .medium)) .foregroundColor(Color.appTextSecondary) } + .accessibilityLabel(A11y.Auth.passwordToggle) .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.passwordVisibilityToggle) } .padding(16) @@ -375,5 +378,6 @@ struct FieldError: View { Text(message) .font(.caption) .foregroundColor(Color.appError) + .accessibilityLabel(message) } } diff --git a/iosApp/iosApp/Shared/Components/SharedEmptyStateView.swift b/iosApp/iosApp/Shared/Components/SharedEmptyStateView.swift index 345e6ba..8982fb0 100644 --- a/iosApp/iosApp/Shared/Components/SharedEmptyStateView.swift +++ b/iosApp/iosApp/Shared/Components/SharedEmptyStateView.swift @@ -28,6 +28,7 @@ struct StandardEmptyStateView: View { Image(systemName: icon) .font(.system(size: 60)) .foregroundColor(Color.appTextSecondary.opacity(0.5)) + .accessibilityHidden(true) VStack(spacing: 8) { Text(title) @@ -55,6 +56,7 @@ struct StandardEmptyStateView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() + .accessibilityElement(children: .combine) } } @@ -108,6 +110,7 @@ struct OrganicEmptyState: View { .font(.system(size: 32, weight: .medium)) .foregroundColor(accentColor.opacity(0.6)) } + .accessibilityHidden(true) VStack(spacing: 8) { Text(title) @@ -140,6 +143,7 @@ struct OrganicEmptyState: View { .background(OrganicCardBackground(showBlob: true, blobVariation: blobVariation)) .clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous)) .naturalShadow(.subtle) + .accessibilityElement(children: .combine) } } @@ -154,6 +158,7 @@ struct ListEmptyState: View { Image(systemName: icon) .font(.system(size: 48)) .foregroundColor(Color.appTextSecondary.opacity(0.4)) + .accessibilityHidden(true) Text(message) .font(.subheadline) @@ -162,5 +167,6 @@ struct ListEmptyState: View { } .padding(.vertical, 40) .frame(maxWidth: .infinity) + .accessibilityElement(children: .combine) } } diff --git a/iosApp/iosApp/Shared/Extensions/AccessibilityModifiers.swift b/iosApp/iosApp/Shared/Extensions/AccessibilityModifiers.swift new file mode 100644 index 0000000..a348f0b --- /dev/null +++ b/iosApp/iosApp/Shared/Extensions/AccessibilityModifiers.swift @@ -0,0 +1,40 @@ +import SwiftUI + +extension View { + func a11yHeader(_ label: String) -> some View { + self.accessibilityLabel(label) + .accessibilityAddTraits(.isHeader) + } + + func a11yHeader() -> some View { + self.accessibilityAddTraits(.isHeader) + } + + func a11yDecorative() -> some View { + self.accessibilityHidden(true) + } + + func a11yButton(_ label: String, hint: String? = nil) -> some View { + let view = self.accessibilityLabel(label) + .accessibilityAddTraits(.isButton) + if let hint = hint { + return AnyView(view.accessibilityHint(hint)) + } + return AnyView(view) + } + + func a11yImage(_ description: String) -> some View { + self.accessibilityLabel(description) + .accessibilityAddTraits(.isImage) + } + + func a11yCard(label: String) -> some View { + self.accessibilityElement(children: .combine) + .accessibilityLabel(label) + } + + func a11yStatValue(_ value: String, label: String) -> some View { + self.accessibilityElement(children: .combine) + .accessibilityLabel(A11y.Common.stat(value: value, label: label)) + } +} diff --git a/iosApp/iosApp/Subscription/FeatureComparisonView.swift b/iosApp/iosApp/Subscription/FeatureComparisonView.swift index 7660d3f..a864e45 100644 --- a/iosApp/iosApp/Subscription/FeatureComparisonView.swift +++ b/iosApp/iosApp/Subscription/FeatureComparisonView.swift @@ -49,6 +49,7 @@ struct FeatureComparisonView: View { Text("Choose Your Plan") .font(.title.weight(.bold)) .foregroundColor(Color.appTextPrimary) + .a11yHeader() Text("Upgrade to Pro for unlimited access") .font(.subheadline) @@ -64,16 +65,19 @@ struct FeatureComparisonView: View { .font(.headline) .foregroundColor(Color.appTextPrimary) .frame(maxWidth: .infinity, alignment: .leading) - + .a11yHeader() + Text("Free") .font(.headline) .foregroundColor(Color.appTextSecondary) .frame(width: 80) - + .a11yHeader() + Text("Pro") .font(.headline) .foregroundColor(Color.appPrimary) .frame(width: 80) + .a11yHeader() } .padding() .background(Color.appBackgroundSecondary) @@ -181,6 +185,7 @@ struct FeatureComparisonView: View { Button("Close") { isPresented = false } + .accessibilityLabel("Close") } } .alert("Subscription Active", isPresented: $purchaseHelper.showSuccessAlert) { @@ -253,6 +258,7 @@ struct SubscriptionButton: View { ) } .disabled(isProcessing) + .accessibilityLabel("\(product.displayName), \(product.displayPrice)\(savingsText.map { ", \($0)" } ?? "")") } } @@ -260,20 +266,20 @@ struct ComparisonRow: View { let featureName: String let freeText: String let proText: String - + var body: some View { HStack { Text(featureName) .font(.body) .foregroundColor(Color.appTextPrimary) .frame(maxWidth: .infinity, alignment: .leading) - + Text(freeText) .font(.subheadline) .foregroundColor(Color.appTextSecondary) .frame(width: 80) .multilineTextAlignment(.center) - + Text(proText) .font(.subheadline.weight(.medium)) .foregroundColor(Color.appPrimary) @@ -281,6 +287,8 @@ struct ComparisonRow: View { .multilineTextAlignment(.center) } .padding() + .accessibilityElement(children: .combine) + .accessibilityLabel("\(featureName): Free: \(freeText), Pro: \(proText)") } } diff --git a/iosApp/iosApp/Subscription/UpgradeFeatureView.swift b/iosApp/iosApp/Subscription/UpgradeFeatureView.swift index 8c73e27..aec93c1 100644 --- a/iosApp/iosApp/Subscription/UpgradeFeatureView.swift +++ b/iosApp/iosApp/Subscription/UpgradeFeatureView.swift @@ -108,6 +108,7 @@ struct UpgradeFeatureView: View { .font(.system(size: 24, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) .multilineTextAlignment(.center) + .a11yHeader() Text(message) .font(.system(size: 15, weight: .medium)) @@ -219,7 +220,7 @@ struct UpgradeFeatureView: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(WarmGradientBackground()) + .background(WarmGradientBackground().a11yDecorative()) .sheet(isPresented: $showFeatureComparison) { FeatureComparisonView(isPresented: $showFeatureComparison) } diff --git a/iosApp/iosApp/Subscription/UpgradePromptView.swift b/iosApp/iosApp/Subscription/UpgradePromptView.swift index 6629079..f515f6b 100644 --- a/iosApp/iosApp/Subscription/UpgradePromptView.swift +++ b/iosApp/iosApp/Subscription/UpgradePromptView.swift @@ -147,6 +147,7 @@ struct UpgradePromptView: View { NavigationStack { ZStack { WarmGradientBackground() + .a11yDecorative() ScrollView(showsIndicators: false) { VStack(spacing: OrganicSpacing.comfortable) { @@ -338,6 +339,7 @@ struct UpgradePromptView: View { .background(Color.appBackgroundSecondary.opacity(0.8)) .clipShape(Circle()) } + .a11yButton("Close") } } .sheet(isPresented: $showFeatureComparison) { @@ -474,6 +476,7 @@ private struct OrganicSubscriptionButton: View { ) } .disabled(isProcessing) + .accessibilityLabel("\(product.displayName), \(product.displayPrice)\(savingsText.map { ", \($0)" } ?? "")") } } diff --git a/iosApp/iosApp/Subviews/Common/ErrorMessageView.swift b/iosApp/iosApp/Subviews/Common/ErrorMessageView.swift index 1530102..7f26324 100644 --- a/iosApp/iosApp/Subviews/Common/ErrorMessageView.swift +++ b/iosApp/iosApp/Subviews/Common/ErrorMessageView.swift @@ -8,6 +8,7 @@ struct ErrorMessageView: View { HStack { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(Color.appError) + .accessibilityHidden(true) Text(message) .font(.caption) @@ -19,6 +20,7 @@ struct ErrorMessageView: View { Image(systemName: "xmark.circle.fill") .foregroundColor(Color.appError) } + .accessibilityLabel(A11y.Common.dismissError) } .padding() .background(Color.appError.opacity(0.1)) diff --git a/iosApp/iosApp/Subviews/Common/ErrorView.swift b/iosApp/iosApp/Subviews/Common/ErrorView.swift index 7682123..f487a0e 100644 --- a/iosApp/iosApp/Subviews/Common/ErrorView.swift +++ b/iosApp/iosApp/Subviews/Common/ErrorView.swift @@ -15,6 +15,7 @@ struct ErrorView: View { .font(.system(size: 44, weight: .medium)) .foregroundColor(Color.appError) } + .accessibilityHidden(true) Text("Error: \(message)") .font(.system(size: 15, weight: .medium)) @@ -31,6 +32,7 @@ struct ErrorView: View { .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) .naturalShadow(.subtle) } + .accessibilityLabel(A11y.Common.retryButton) } .padding(OrganicSpacing.comfortable) } diff --git a/iosApp/iosApp/Subviews/Common/HomeNavigationCard.swift b/iosApp/iosApp/Subviews/Common/HomeNavigationCard.swift index 27fa479..d5ba26e 100644 --- a/iosApp/iosApp/Subviews/Common/HomeNavigationCard.swift +++ b/iosApp/iosApp/Subviews/Common/HomeNavigationCard.swift @@ -36,10 +36,12 @@ struct HomeNavigationCard: View { Image(systemName: "chevron.right") .font(.system(size: 16, weight: .semibold)) .foregroundColor(Color.appTextSecondary.opacity(0.7)) + .accessibilityHidden(true) } .padding(AppSpacing.lg) .background(Color.appBackgroundSecondary) .cornerRadius(AppRadius.lg) .shadow(color: AppShadow.md.color, radius: AppShadow.md.radius, x: AppShadow.md.x, y: AppShadow.md.y) + .accessibilityElement(children: .combine) } } diff --git a/iosApp/iosApp/Subviews/Common/HoneyDueIconView.swift b/iosApp/iosApp/Subviews/Common/HoneyDueIconView.swift index 72b9c43..1ba4a9f 100644 --- a/iosApp/iosApp/Subviews/Common/HoneyDueIconView.swift +++ b/iosApp/iosApp/Subviews/Common/HoneyDueIconView.swift @@ -233,6 +233,7 @@ struct HoneyDueIconView: View { .offset(x: offsetX, y: offsetY) } .aspectRatio(1, contentMode: .fit) + .accessibilityHidden(true) } } @@ -257,6 +258,7 @@ struct AnimatedHoneyDueIconView: View { showBackground: showBackground, backgroundOpacity: backgroundOpacity ) + .accessibilityHidden(true) .onAppear { animateIn() } diff --git a/iosApp/iosApp/Subviews/Common/ImageThumbnailView.swift b/iosApp/iosApp/Subviews/Common/ImageThumbnailView.swift index 19d26d4..c8bf484 100644 --- a/iosApp/iosApp/Subviews/Common/ImageThumbnailView.swift +++ b/iosApp/iosApp/Subviews/Common/ImageThumbnailView.swift @@ -15,6 +15,8 @@ struct ImageThumbnailView: View { RoundedRectangle(cornerRadius: 12, style: .continuous) .strokeBorder(.quaternary, lineWidth: 1) } + .accessibilityLabel("Attached photo") + .accessibilityAddTraits(.isImage) Button(action: onRemove) { Image(systemName: "xmark.circle.fill") @@ -26,6 +28,7 @@ struct ImageThumbnailView: View { .padding(4) } } + .accessibilityLabel(A11y.Common.removePhoto) .offset(x: 8, y: -8) } } diff --git a/iosApp/iosApp/Subviews/Common/StatView.swift b/iosApp/iosApp/Subviews/Common/StatView.swift index cf1ae54..62abb68 100644 --- a/iosApp/iosApp/Subviews/Common/StatView.swift +++ b/iosApp/iosApp/Subviews/Common/StatView.swift @@ -63,5 +63,7 @@ struct StatView: View { .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) + .accessibilityElement(children: .combine) + .accessibilityLabel(A11y.Common.stat(value: value, label: label)) } } diff --git a/iosApp/iosApp/Subviews/Residence/PropertyDetailItem.swift b/iosApp/iosApp/Subviews/Residence/PropertyDetailItem.swift index 3513310..75a9536 100644 --- a/iosApp/iosApp/Subviews/Residence/PropertyDetailItem.swift +++ b/iosApp/iosApp/Subviews/Residence/PropertyDetailItem.swift @@ -20,6 +20,8 @@ struct PropertyDetailItem: View { .font(.caption2) .foregroundColor(Color.appTextSecondary) } + .accessibilityElement(children: .combine) + .accessibilityLabel(A11y.Common.stat(value: value, label: label)) } } diff --git a/iosApp/iosApp/Subviews/Residence/PropertyHeaderCard.swift b/iosApp/iosApp/Subviews/Residence/PropertyHeaderCard.swift index 40c9bc9..76ba166 100644 --- a/iosApp/iosApp/Subviews/Residence/PropertyHeaderCard.swift +++ b/iosApp/iosApp/Subviews/Residence/PropertyHeaderCard.swift @@ -17,6 +17,7 @@ struct PropertyHeaderCard: View { Text(residence.name) .font(.system(size: 24, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) + .accessibilityAddTraits(.isHeader) if let propertyTypeName = residence.propertyTypeName { Text(propertyTypeName.uppercased()) @@ -142,6 +143,7 @@ struct PropertyHeaderCard: View { } .padding(.horizontal, 12) .padding(.vertical, 16) + .accessibilityElement(children: .combine) } } .background(PropertyHeaderBackground()) @@ -218,6 +220,7 @@ private struct PropertyDetailIcon: View { .foregroundColor(Color.appTextOnPrimary) } .naturalShadow(.subtle) + .a11yDecorative() } } diff --git a/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift b/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift index 8046b15..7bb865a 100644 --- a/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift +++ b/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift @@ -78,6 +78,7 @@ struct ResidenceCard: View { .foregroundColor(Color.appPrimary.opacity(0.6)) } } + .accessibilityLabel("Open \(residence.streetAddress) in Maps") .padding(.top, 2) } } @@ -148,6 +149,23 @@ struct ResidenceCard: View { .background(CardBackgroundView(hasOverdue: hasOverdueTasks)) .clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous)) .naturalShadow(.medium) + .accessibilityElement(children: .combine) + .accessibilityLabel({ + var parts = [residence.name] + if let propertyTypeName = residence.propertyTypeName { + parts.append(propertyTypeName) + } + if !residence.streetAddress.isEmpty { + parts.append(residence.streetAddress) + } + if taskMetrics.totalCount > 0 { + parts.append("\(taskMetrics.totalCount) tasks") + } + if residence.isPrimary { + parts.append("Primary property") + } + return parts.joined(separator: ", ") + }()) } } @@ -198,6 +216,7 @@ private struct PrimaryBadgeView: View { .font(.system(size: 14, weight: .bold)) .foregroundColor(Color.appAccent) } + .accessibilityLabel("Primary property") } } diff --git a/iosApp/iosApp/Subviews/Residence/ShareCodeCard.swift b/iosApp/iosApp/Subviews/Residence/ShareCodeCard.swift index 228a566..8e91165 100644 --- a/iosApp/iosApp/Subviews/Residence/ShareCodeCard.swift +++ b/iosApp/iosApp/Subviews/Residence/ShareCodeCard.swift @@ -62,6 +62,7 @@ struct ShareCodeCard: View { .fill(Color.appTextSecondary.opacity(0.3)) .frame(height: 1) } + .accessibilityHidden(true) // Share Code Section VStack(alignment: .leading, spacing: 8) { @@ -75,6 +76,7 @@ struct ShareCodeCard: View { .font(.system(size: 32, weight: .bold, design: .monospaced)) .foregroundColor(Color.appPrimary) .kerning(4) + .accessibilityLabel("Share code: \(shareCode.code)") Spacer() @@ -85,6 +87,7 @@ struct ShareCodeCard: View { .foregroundColor(Color.appPrimary) } .buttonStyle(.bordered) + .accessibilityLabel("Copy share code") } else { Text("No active code") .font(.body) @@ -110,6 +113,7 @@ struct ShareCodeCard: View { } .buttonStyle(.borderedProminent) .disabled(isGeneratingCode) + .accessibilityLabel("Generate new share code") if shareCode != nil { Text("Share this 6-character code. They can enter it in the app to join.") diff --git a/iosApp/iosApp/Subviews/Residence/SummaryCard.swift b/iosApp/iosApp/Subviews/Residence/SummaryCard.swift index 26daea0..5fdd18f 100644 --- a/iosApp/iosApp/Subviews/Residence/SummaryCard.swift +++ b/iosApp/iosApp/Subviews/Residence/SummaryCard.swift @@ -15,6 +15,7 @@ struct SummaryCard: View { .padding(.horizontal, OrganicSpacing.cozy) .padding(.top, OrganicSpacing.cozy) .padding(.bottom, 20) + .accessibilityAddTraits(.isHeader) // Main Stats Row HStack(spacing: 0) { @@ -120,6 +121,7 @@ private struct OrganicStatItem: View { .foregroundColor(Color.appTextSecondary) } .frame(maxWidth: .infinity) + .accessibilityElement(children: .combine) } } @@ -149,6 +151,7 @@ private struct TimelineStatPill: View { .foregroundColor(Color.appTextSecondary) } .frame(maxWidth: .infinity) + .accessibilityElement(children: .combine) .padding(.vertical, 18) .background( RoundedRectangle(cornerRadius: 20, style: .continuous) diff --git a/iosApp/iosApp/Subviews/Residence/SummaryStatView.swift b/iosApp/iosApp/Subviews/Residence/SummaryStatView.swift index 89badba..fe738d9 100644 --- a/iosApp/iosApp/Subviews/Residence/SummaryStatView.swift +++ b/iosApp/iosApp/Subviews/Residence/SummaryStatView.swift @@ -36,6 +36,8 @@ struct SummaryStatView: View { .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) + .accessibilityElement(children: .combine) + .accessibilityLabel(A11y.Common.stat(value: value, label: label)) } } diff --git a/iosApp/iosApp/Subviews/Residence/TaskStatChip.swift b/iosApp/iosApp/Subviews/Residence/TaskStatChip.swift index cfc0149..8f6d747 100644 --- a/iosApp/iosApp/Subviews/Residence/TaskStatChip.swift +++ b/iosApp/iosApp/Subviews/Residence/TaskStatChip.swift @@ -21,6 +21,8 @@ struct TaskStatChip: View { .font(.caption) .foregroundColor(Color.appTextSecondary) } + .accessibilityElement(children: .combine) + .accessibilityLabel(A11y.Common.stat(value: value, label: label)) } } diff --git a/iosApp/iosApp/Subviews/Residence/UserListItem.swift b/iosApp/iosApp/Subviews/Residence/UserListItem.swift index 5a415d8..0b99a7e 100644 --- a/iosApp/iosApp/Subviews/Residence/UserListItem.swift +++ b/iosApp/iosApp/Subviews/Residence/UserListItem.swift @@ -26,6 +26,7 @@ struct UserListItem: View { .padding(.vertical, 2) .background(Color.appPrimary.opacity(0.1)) .cornerRadius(4) + .accessibilityLabel("Owner") } } @@ -54,6 +55,7 @@ struct UserListItem: View { Image(systemName: "trash") .foregroundColor(Color.appError) } + .accessibilityLabel("Remove \(user.username) from property") } } .padding() diff --git a/iosApp/iosApp/Subviews/Task/CompletionCardView.swift b/iosApp/iosApp/Subviews/Task/CompletionCardView.swift index 880f935..d4e5a6c 100644 --- a/iosApp/iosApp/Subviews/Task/CompletionCardView.swift +++ b/iosApp/iosApp/Subviews/Task/CompletionCardView.swift @@ -28,6 +28,7 @@ struct CompletionCardView: View { .padding(.vertical, 4) .background(Color.appAccent.opacity(0.1)) .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) + .accessibilityLabel("Rated \(rating) out of 5") } } @@ -90,6 +91,7 @@ struct CompletionCardView: View { .foregroundColor(Color.appPrimary) .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) } + .accessibilityLabel("View \(images.count) photos") } } .padding(14) diff --git a/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift b/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift index 962f9d0..a36d8c6 100644 --- a/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift +++ b/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift @@ -86,6 +86,7 @@ struct DynamicTaskCard: View { .stroke(Color.appPrimary, lineWidth: 2) ) } + .accessibilityLabel("Task actions") .zIndex(10) .menuOrder(.fixed) } @@ -111,6 +112,7 @@ struct DynamicTaskCard: View { .stroke(Color.appAccent, lineWidth: 2) ) } + .accessibilityLabel("View \(task.completionCount) completions") } } } diff --git a/iosApp/iosApp/Subviews/Task/PhotoViewerSheet.swift b/iosApp/iosApp/Subviews/Task/PhotoViewerSheet.swift index f0f3e2d..f2fbe88 100644 --- a/iosApp/iosApp/Subviews/Task/PhotoViewerSheet.swift +++ b/iosApp/iosApp/Subviews/Task/PhotoViewerSheet.swift @@ -15,6 +15,7 @@ struct PhotoViewerSheet: View { VStack(spacing: 16) { AuthenticatedImage(mediaURL: selectedImage.mediaUrl) .frame(minHeight: 300) + .accessibilityLabel("Completion photo\(selectedImage.caption.map { ", \($0)" } ?? "")") if let caption = selectedImage.caption { VStack(alignment: .leading, spacing: 8) { @@ -46,6 +47,7 @@ struct PhotoViewerSheet: View { Text("Back") } } + .accessibilityLabel("Back to all photos") } ToolbarItem(placement: .navigationBarTrailing) { Button("Done") { @@ -80,6 +82,7 @@ struct PhotoViewerSheet: View { } } .buttonStyle(.plain) + .accessibilityLabel("Photo\(image.caption.map { ", \($0)" } ?? "")") } } .padding() diff --git a/iosApp/iosApp/Subviews/Task/PriorityBadge.swift b/iosApp/iosApp/Subviews/Task/PriorityBadge.swift index f4564f5..8e9cba6 100644 --- a/iosApp/iosApp/Subviews/Task/PriorityBadge.swift +++ b/iosApp/iosApp/Subviews/Task/PriorityBadge.swift @@ -22,6 +22,8 @@ struct PriorityBadge: View { .stroke(priorityColor.opacity(0.2), lineWidth: 1) ) ) + .accessibilityElement(children: .combine) + .accessibilityLabel(A11y.Task.priorityBadge(level: priority.capitalized)) } private var priorityIcon: String { diff --git a/iosApp/iosApp/Subviews/Task/StatusBadge.swift b/iosApp/iosApp/Subviews/Task/StatusBadge.swift index 0c7213d..f5d4e40 100644 --- a/iosApp/iosApp/Subviews/Task/StatusBadge.swift +++ b/iosApp/iosApp/Subviews/Task/StatusBadge.swift @@ -22,6 +22,8 @@ struct StatusBadge: View { .stroke(statusColor.opacity(0.2), lineWidth: 1) ) ) + .accessibilityElement(children: .combine) + .accessibilityLabel(A11y.Task.statusBadge(status: formatStatus(status))) } private func formatStatus(_ status: String) -> String { diff --git a/iosApp/iosApp/Subviews/Task/TaskActionButtons.swift b/iosApp/iosApp/Subviews/Task/TaskActionButtons.swift index a41cd83..29ca3ce 100644 --- a/iosApp/iosApp/Subviews/Task/TaskActionButtons.swift +++ b/iosApp/iosApp/Subviews/Task/TaskActionButtons.swift @@ -19,6 +19,8 @@ struct EditTaskButton: View { .frame(maxWidth: .infinity) } .buttonStyle(.bordered) + .accessibilityLabel("Edit task") + .accessibilityHint("Double tap to edit this task") } } @@ -41,6 +43,8 @@ struct CancelTaskButton: View { } .buttonStyle(.bordered) .tint(Color.appError) + .accessibilityLabel("Cancel task") + .accessibilityHint("Double tap to cancel this task") .alert("Cancel Task", isPresented: $showConfirmation) { Button("Cancel", role: .cancel) { } Button("Cancel Task", role: .destructive) { @@ -82,6 +86,8 @@ struct UncancelTaskButton: View { } .buttonStyle(.borderedProminent) .tint(Color.appPrimary) + .accessibilityLabel("Restore task") + .accessibilityHint("Double tap to restore this task") } } @@ -114,6 +120,8 @@ struct MarkInProgressButton: View { } .buttonStyle(.bordered) .tint(Color.appAccent) + .accessibilityLabel("Mark as in progress") + .accessibilityHint("Double tap to mark this task as in progress") } } @@ -138,6 +146,8 @@ struct CompleteTaskButton: View { .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) + .accessibilityLabel("Complete task") + .accessibilityHint("Double tap to complete this task") } } @@ -160,6 +170,8 @@ struct ArchiveTaskButton: View { } .buttonStyle(.bordered) .tint(.gray) + .accessibilityLabel("Archive task") + .accessibilityHint("Double tap to archive this task") .alert("Archive Task", isPresented: $showConfirmation) { Button("Cancel", role: .cancel) { } Button("Archive", role: .destructive) { @@ -201,5 +213,7 @@ struct UnarchiveTaskButton: View { } .buttonStyle(.bordered) .tint(Color.appPrimary) + .accessibilityLabel("Unarchive task") + .accessibilityHint("Double tap to unarchive this task") } } diff --git a/iosApp/iosApp/Subviews/Task/TaskCard.swift b/iosApp/iosApp/Subviews/Task/TaskCard.swift index 5544c52..93b5fa3 100644 --- a/iosApp/iosApp/Subviews/Task/TaskCard.swift +++ b/iosApp/iosApp/Subviews/Task/TaskCard.swift @@ -93,6 +93,8 @@ struct TaskCard: View { isCompletionsExpanded.toggle() } } + .accessibilityLabel("Completions (\(task.completions.count))") + .accessibilityHint("Double tap to \(isCompletionsExpanded ? "collapse" : "expand") completions") if isCompletionsExpanded { ForEach(task.completions, id: \.id) { completion in diff --git a/iosApp/iosApp/Task/AllTasksView.swift b/iosApp/iosApp/Task/AllTasksView.swift index d6d3546..6cb86fb 100644 --- a/iosApp/iosApp/Task/AllTasksView.swift +++ b/iosApp/iosApp/Task/AllTasksView.swift @@ -269,6 +269,7 @@ struct AllTasksView: View { } .disabled((residenceViewModel.myResidences?.residences.isEmpty ?? true) || isLoadingTasks) .accessibilityIdentifier(AccessibilityIdentifiers.Task.refreshButton) + .accessibilityLabel("Refresh tasks") Button(action: { if subscriptionCache.shouldShowUpgradePrompt(currentCount: totalTaskCount, limitKey: "tasks") { @@ -281,6 +282,7 @@ struct AllTasksView: View { } .disabled((residenceViewModel.myResidences?.residences.isEmpty ?? true)) .accessibilityIdentifier(AccessibilityIdentifiers.Task.addButton) + .accessibilityLabel("Add new task") } } } diff --git a/iosApp/iosApp/Task/CompleteTaskView.swift b/iosApp/iosApp/Task/CompleteTaskView.swift index 71204ee..bca4bdc 100644 --- a/iosApp/iosApp/Task/CompleteTaskView.swift +++ b/iosApp/iosApp/Task/CompleteTaskView.swift @@ -180,6 +180,18 @@ struct CompleteTaskView: View { .frame(maxWidth: .infinity) } .accessibilityIdentifier(AccessibilityIdentifiers.Task.ratingView) + .accessibilityElement(children: .combine) + .accessibilityLabel("Rating: \(rating) out of 5 stars") + .accessibilityAdjustableAction { direction in + switch direction { + case .increment: + if rating < 5 { rating += 1 } + case .decrement: + if rating > 1 { rating -= 1 } + @unknown default: + break + } + } } footer: { Text(L10n.Tasks.rateQuality) } diff --git a/iosApp/iosApp/Task/CompletionHistorySheet.swift b/iosApp/iosApp/Task/CompletionHistorySheet.swift index cddcee8..f04461a 100644 --- a/iosApp/iosApp/Task/CompletionHistorySheet.swift +++ b/iosApp/iosApp/Task/CompletionHistorySheet.swift @@ -28,6 +28,7 @@ struct CompletionHistorySheet: View { } .navigationTitle(L10n.Tasks.completionHistory) .navigationBarTitleDisplayMode(.inline) + .accessibilityLabel("Completion history for \(taskTitle)") .toolbar { ToolbarItem(placement: .cancellationAction) { Button(L10n.Common.done) { @@ -186,6 +187,7 @@ struct CompletionHistoryCard: View { Text(DateUtils.formatDateTimeWithTime(completion.completionDate)) .font(.system(size: 16, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) + .a11yHeader() if let completedBy = completion.completedByName, !completedBy.isEmpty { HStack(spacing: 5) { diff --git a/iosApp/iosApp/Task/TaskFormView.swift b/iosApp/iosApp/Task/TaskFormView.swift index 2c03665..ee618b7 100644 --- a/iosApp/iosApp/Task/TaskFormView.swift +++ b/iosApp/iosApp/Task/TaskFormView.swift @@ -190,6 +190,7 @@ struct TaskFormView: View { .accessibilityIdentifier(AccessibilityIdentifiers.Task.descriptionField) } header: { Text(L10n.Tasks.taskDetails) + .accessibilityAddTraits(.isHeader) } footer: { Text(L10n.Tasks.titleRequired) .font(.caption) @@ -237,6 +238,7 @@ struct TaskFormView: View { .accessibilityIdentifier(AccessibilityIdentifiers.Task.dueDatePicker) } header: { Text(L10n.Tasks.scheduling) + .accessibilityAddTraits(.isHeader) } footer: { if selectedFrequency?.name.lowercased() == "custom" { Text("Enter the number of days between each occurrence") @@ -258,6 +260,7 @@ struct TaskFormView: View { Toggle(L10n.Tasks.inProgressLabel, isOn: $inProgress) } header: { Text(L10n.Tasks.priorityAndStatus) + .accessibilityAddTraits(.isHeader) } .sectionBackground() diff --git a/iosApp/iosApp/Task/TaskTemplatesBrowserView.swift b/iosApp/iosApp/Task/TaskTemplatesBrowserView.swift index 3f377f9..da2aa4d 100644 --- a/iosApp/iosApp/Task/TaskTemplatesBrowserView.swift +++ b/iosApp/iosApp/Task/TaskTemplatesBrowserView.swift @@ -35,6 +35,7 @@ struct TaskTemplatesBrowserView: View { .standardFormStyle() .background(WarmGradientBackground()) .searchable(text: $searchText, prompt: "Search templates...") + .accessibilityHint("Search task templates by name") .navigationTitle("Task Templates") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -206,6 +207,8 @@ struct TaskTemplatesBrowserView: View { .contentShape(Rectangle()) } .buttonStyle(.plain) + .accessibilityLabel("\(template.title), \(template.frequencyDisplay)") + .accessibilityHint("Double tap to use this template") } // MARK: - Helpers