From c220e26bb496e455a5bdb6a8dc22abf09ecfed5f Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Tue, 21 Jul 2026 20:47:34 -0500 Subject: feat(a11y): color independence, reduce motion, reduce transparency (#21 phase 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit unchanged at 11 dark / 14 light — expected, as none of these settings are exercised by performAccessibilityAudit, and simctl can toggle only Increase Contrast, not Differentiate Without Color, Reduce Motion, or Reduce Transparency. Correct by construction and build-clean; runtime behaviour is verified in the Phase 6 manual pass. Color independence: - Widget status is now an SF Symbol (checkmark.circle.fill / exclamationmark.triangle.fill / exclamationmark.octagon.fill), the same vocabulary as the in-app badges, replacing a silent colour-only dot on both the domain rows and the small-view count pills. Status now survives greyscale and reads consistently across surfaces. - Under accessibilityDifferentiateWithoutColor: the Dashboard summary-card dot becomes a per-filter symbol, the selected quick-filter chip gains a checkmark and a border (selection was fill-colour only, and also gains the .isSelected trait), and LabeledValueRow prefixes a warning/failure symbol. All gated on the setting so the default UI stays uncluttered. Reduce motion: all five withAnimation/.animation sites now pass nil under accessibilityReduceMotion — AppCopyButton's check cross-fade, CollapsibleSectionView's expand/collapse, TimelineDiffView's scroll, and WatchlistView's list reorder. Reduce transparency: the single .thinMaterial capsule falls back to an opaque AppSurfaceElevated fill under accessibilityReduceTransparency. The SweepActivityController item from the plan is dropped: it is pure ActivityKit lifecycle with no animation, confirmed back in the issue triage. --- DomainDig/ContentView.swift | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) (limited to 'DomainDig/ContentView.swift') diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift index bc159d9..ea6ead8 100644 --- a/DomainDig/ContentView.swift +++ b/DomainDig/ContentView.swift @@ -2580,6 +2580,7 @@ struct SectionTrustMetadataView: View { struct LabeledValueRow: View { @Environment(\.appDensity) private var appDensity + @Environment(\.accessibilityDifferentiateWithoutColor) private var differentiateWithoutColor let row: InfoRowViewData var body: some View { @@ -2589,7 +2590,15 @@ struct LabeledValueRow: View { Text(row.label) .font(appDensity.font(.caption2)) .foregroundStyle(Color(.appTextSecondary)) - valueText + HStack(alignment: .firstTextBaseline, spacing: 4) { + if differentiateWithoutColor, let symbol = toneSymbol { + Image(systemName: symbol) + .font(appDensity.font(.caption2)) + .foregroundStyle(ResultColors.color(for: row.tone)) + .accessibilityHidden(true) + } + valueText + } } .frame(maxWidth: .infinity, alignment: .leading) .layoutPriority(1) @@ -2602,6 +2611,17 @@ struct LabeledValueRow: View { .frame(minHeight: appDensity.metrics.rowMinHeight, alignment: .topLeading) } + /// A leading symbol for warning/failure tones, shown only under Differentiate + /// Without Color so tone is not conveyed by text colour alone. Hidden from + /// VoiceOver — the value text already carries the meaning. + private var toneSymbol: String? { + switch row.tone { + case .warning: return "exclamationmark.triangle.fill" + case .failure: return "xmark.octagon.fill" + default: return nil + } + } + @ViewBuilder private var valueText: some View { let base = Text(row.value) @@ -3588,6 +3608,7 @@ private struct DataPortabilitySettingsView: View { private struct DataManagementSettingsView: View { @Bindable var viewModel: DomainViewModel + @Environment(\.accessibilityReduceTransparency) private var reduceTransparency @State private var showClearHistoryConfirmation = false @State private var showClearCacheConfirmation = false @@ -3695,7 +3716,12 @@ private struct DataManagementSettingsView: View { .foregroundStyle(Color(.appTextSecondary)) .padding(.horizontal, 14) .padding(.vertical, 10) - .background(.thinMaterial, in: Capsule()) + // Reduce Transparency swaps the blur for an opaque surface. + // On iOS 26+ the system also composites its own translucency + // that the app cannot declare — verify there too (Phase 6). + .background( + Capsule().fill(reduceTransparency ? AnyShapeStyle(Color(.appSurfaceElevated)) : AnyShapeStyle(.thinMaterial)) + ) .padding(.bottom, 8) .transition(.move(edge: .bottom).combined(with: .opacity)) } -- cgit v1.2.3