iOS VoiceOver accessibility overhaul — 67 files
New framework: - AccessibilityLabels.swift: centralized A11y struct with VoiceOver strings - AccessibilityModifiers.swift: reusable .a11yHeader, .a11yDecorative, .a11yButton, .a11yCard, .a11yStatValue View extensions Shared components: decorative elements hidden, stat views combined, status/priority badges labeled, error views announced, empty states grouped Cards: ResidenceCard, TaskCard, DynamicTaskCard, ContractorCard, DocumentCard, WarrantyCard — all grouped with combined labels, chevrons hidden, action buttons labeled Main screens: Login, Register, Residences, Tasks, Contractors, Documents — toolbar buttons labeled, section headers marked, form field hints added Onboarding: all 10 views — header traits, button hints, task selection state, progress indicator, decorative backgrounds hidden Profile/Subscription: toggle hints, theme selection state, feature comparison table accessibility, subscription button labels iOS build verified: BUILD SUCCEEDED
This commit is contained in:
@@ -38,6 +38,7 @@ struct AuthenticatedImage: View {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: contentMode)
|
||||
.accessibilityLabel("Image")
|
||||
case .failure:
|
||||
errorView
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 : [])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
91
iosApp/iosApp/Helpers/AccessibilityLabels.swift
Normal file
91
iosApp/iosApp/Helpers/AccessibilityLabels.swift
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() }) {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -147,6 +147,8 @@ struct ThemeRow: View {
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
.contentShape(Rectangle())
|
||||
.accessibilityLabel(theme.displayName)
|
||||
.accessibilityValue(isSelected ? "Selected" : "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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" : "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
40
iosApp/iosApp/Shared/Extensions/AccessibilityModifiers.swift
Normal file
40
iosApp/iosApp/Shared/Extensions/AccessibilityModifiers.swift
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)" } ?? "")")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,5 +63,7 @@ struct StatView: View {
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel(A11y.Common.stat(value: value, label: label))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ struct PropertyDetailItem: View {
|
||||
.font(.caption2)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel(A11y.Common.stat(value: value, label: label))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -36,6 +36,8 @@ struct SummaryStatView: View {
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel(A11y.Common.stat(value: value, label: label))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ struct TaskStatChip: View {
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel(A11y.Common.stat(value: value, label: label))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user