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:
Trey T
2026-03-26 14:51:29 -05:00
parent 0d80df07f6
commit af73f8861b
67 changed files with 394 additions and 8 deletions

View File

@@ -38,6 +38,7 @@ struct AuthenticatedImage: View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: contentMode)
.accessibilityLabel("Image")
case .failure:
errorView
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 : [])
}
}

View 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"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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() }) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -147,6 +147,8 @@ struct ThemeRow: View {
}
.padding(.vertical, 6)
.contentShape(Rectangle())
.accessibilityLabel(theme.displayName)
.accessibilityValue(isSelected ? "Selected" : "")
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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" : "")
}
}

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -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)" } ?? "")")
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -63,5 +63,7 @@ struct StatView: View {
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.accessibilityElement(children: .combine)
.accessibilityLabel(A11y.Common.stat(value: value, label: label))
}
}

View File

@@ -20,6 +20,8 @@ struct PropertyDetailItem: View {
.font(.caption2)
.foregroundColor(Color.appTextSecondary)
}
.accessibilityElement(children: .combine)
.accessibilityLabel(A11y.Common.stat(value: value, label: label))
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,6 +36,8 @@ struct SummaryStatView: View {
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.accessibilityElement(children: .combine)
.accessibilityLabel(A11y.Common.stat(value: value, label: label))
}
}

View File

@@ -21,6 +21,8 @@ struct TaskStatChip: View {
.font(.caption)
.foregroundColor(Color.appTextSecondary)
}
.accessibilityElement(children: .combine)
.accessibilityLabel(A11y.Common.stat(value: value, label: label))
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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