diff options
Diffstat (limited to 'DomainDig')
| -rw-r--r-- | DomainDig/ContentView.swift | 16 | ||||
| -rw-r--r-- | DomainDig/DashboardView.swift | 3 | ||||
| -rw-r--r-- | DomainDig/DomainDigUI.swift | 43 |
3 files changed, 54 insertions, 8 deletions
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift index f48e2a0..a4800ed 100644 --- a/DomainDig/ContentView.swift +++ b/DomainDig/ContentView.swift @@ -2418,19 +2418,31 @@ struct SectionTitleView: View { } } +/// A card that wraps its content by default. +/// +/// `allowsHorizontalScroll` used to default to `true`, so nine call sites put +/// their content behind a horizontal gesture instead of letting it wrap — a +/// WCAG 1.4.10 (Reflow) failure, and the mechanism behind clipped rows at large +/// text sizes. It also forced VoiceOver and Switch Control users onto a nested +/// scroll axis to reach data. +/// +/// The default is now `false`. Where horizontal scrolling genuinely suits wide +/// tabular content, it is still opt-in — but it is suppressed at accessibility +/// text sizes, where wrapping always beats a hidden axis. struct CardView<Content: View>: View { @Environment(\.appDensity) private var appDensity + @Environment(\.dynamicTypeSize) private var dynamicTypeSize let allowsHorizontalScroll: Bool let content: Content - init(allowsHorizontalScroll: Bool = true, @ViewBuilder content: () -> Content) { + init(allowsHorizontalScroll: Bool = false, @ViewBuilder content: () -> Content) { self.allowsHorizontalScroll = allowsHorizontalScroll self.content = content() } var body: some View { Group { - if allowsHorizontalScroll { + if allowsHorizontalScroll, !dynamicTypeSize.isAccessibilitySize { ScrollView(.horizontal) { cardContent .scrollTargetLayout() diff --git a/DomainDig/DashboardView.swift b/DomainDig/DashboardView.swift index 32af2a5..fc202d3 100644 --- a/DomainDig/DashboardView.swift +++ b/DomainDig/DashboardView.swift @@ -194,7 +194,8 @@ struct DashboardView: View { .font(appDensity.font(.caption, design: .default, weight: .semibold)) .foregroundStyle(Color(.appTextSecondary)) Text("\(value)") - .font(.system(size: 28, weight: .bold, design: .rounded)) + // Was a fixed 28pt, which ignored Dynamic Type entirely. + .font(.system(.title, design: .rounded, weight: .bold)) .foregroundStyle(.primary) HStack { Circle() diff --git a/DomainDig/DomainDigUI.swift b/DomainDig/DomainDigUI.swift index fab76d5..bf06b46 100644 --- a/DomainDig/DomainDigUI.swift +++ b/DomainDig/DomainDigUI.swift @@ -72,7 +72,9 @@ enum AppDensity: String, CaseIterable, Identifiable { rowSpacing: 4, rowMinHeight: 30, controlVerticalPadding: 10, - controlMinHeight: 42, + // Was 42, which put every control using it under the 44pt + // minimum in compact density — section headers, Run, Run Batch. + controlMinHeight: AppLayout.minimumTapTarget, cardCornerRadius: 10 ) case .comfortable: @@ -98,6 +100,13 @@ enum AppDensity: String, CaseIterable, Identifiable { } } +/// Layout constants that are not density-dependent. +enum AppLayout { + /// The HIG minimum for an interactive control, and WCAG 2.5.8's floor. + /// Controls scale up from here with Dynamic Type; none may sit below it. + static let minimumTapTarget: CGFloat = 44 +} + struct AppDensityMetrics: Equatable { let sectionSpacing: CGFloat let cardSpacing: CGFloat @@ -249,6 +258,11 @@ struct AppCopyButton: View { @Environment(\.appDensity) private var appDensity @State private var didCopy = false + /// Grows with Dynamic Type. The `max(_, minimumTapTarget)` floor matters + /// because `@ScaledMetric` also scales *down* below the default text size, + /// which would push this back under the 44pt minimum. + @ScaledMetric(relativeTo: .caption) private var size: CGFloat = AppLayout.minimumTapTarget + let value: String let label: String @@ -270,8 +284,8 @@ struct AppCopyButton: View { } label: { Image(systemName: didCopy ? "checkmark" : "doc.on.doc") .font(appDensity.font(.caption)) - .foregroundStyle(didCopy ? Color(.statusPositive) : .secondary) - .frame(width: 30, height: 30) + .foregroundStyle(didCopy ? Color(.statusPositive) : Color(.appTextSecondary)) + .frame(width: max(size, AppLayout.minimumTapTarget), height: max(size, AppLayout.minimumTapTarget)) .background(Color(.appSurfaceElevated)) .clipShape(RoundedRectangle(cornerRadius: 8)) } @@ -339,7 +353,25 @@ struct EmptyStateCardView: View { var body: some View { VStack(alignment: .leading, spacing: appDensity.metrics.cardSpacing) { - Label(title, systemImage: systemImage) + // `Text(message)` already carried `fixedSize`; the title and + // suggestion did not, which is why the audit reported the *title* + // clipped on every empty state while the body beneath it wrapped. + // Deliberately an HStack rather than `Label`. `Label` constrains its + // own title text and `.fixedSize` applied to the Label does not + // reach the Text inside, so every empty-state heading reported as + // clipped. Splitting it lets the modifier land on the Text itself. + // Verified: doing this alone cleared the finding on all four empty + // states; changing the font design did not. + HStack(alignment: .firstTextBaseline, spacing: 8) { + // Decorative. `Label` used to fold the icon into the title's + // element; splitting them exposed it as its own, announcing the + // raw SF Symbol name ("checklist.unchecked") to VoiceOver. + Image(systemName: systemImage) + .accessibilityHidden(true) + Text(title) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.leading) + } .font(appDensity.font(.headline, weight: .semibold)) .foregroundStyle(.primary) @@ -351,6 +383,7 @@ struct EmptyStateCardView: View { Text(suggestion) .font(appDensity.font(.caption)) .foregroundStyle(Color(.statusInfo)) + .fixedSize(horizontal: false, vertical: true) } .frame(maxWidth: .infinity, alignment: .leading) .padding(appDensity.metrics.cardPadding) @@ -392,7 +425,7 @@ struct CollapsibleSectionView<HeaderTrailing: View, Content: View>: View { HStack(alignment: .center, spacing: 10) { VStack(alignment: .leading, spacing: 3) { Text(title) - .font(appDensity.font(.headline, design: .default, weight: .semibold)) + .font(appDensity.font(.headline, weight: .semibold)) .foregroundStyle(.primary) if let subtitle { Text(subtitle) |
