summaryrefslogtreecommitdiff
path: root/Docs/ACCESSIBILITY.md
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-21 20:00:46 -0500
committerChristian Cleberg <[email protected]>2026-07-21 20:38:45 -0500
commitdc479a3ba0d27fe86509d0eb2d27b01132c2b37f (patch)
tree4cc69b5fff2895e2ef794354fcdf89d56fa80e27 /Docs/ACCESSIBILITY.md
parente2da09fec3d3d52ac6108b9256b2419405f76dd3 (diff)
downloaddomain-dig-dc479a3ba0d27fe86509d0eb2d27b01132c2b37f.tar.gz
domain-dig-dc479a3ba0d27fe86509d0eb2d27b01132c2b37f.tar.bz2
domain-dig-dc479a3ba0d27fe86509d0eb2d27b01132c2b37f.zip
feat(a11y): VoiceOver labels, dense-row rotor content, announcements (#21 phase 4)
The audit count is unchanged at 11 dark, and that is the expected result: performAccessibilityAudit validates descriptions, traits, contrast, hit regions, and clipping, but exercises none of VoiceOver's speech, the More Content rotor, custom-content ordering, or announcements — which is the entire substance of this phase. It is verified by construction and stays green with no regressions; the manual VoiceOver pass is Phase 6. Icon-only controls (~14) get accessibilityLabel, obeying label-in-name: where a control has visible text the label keeps it, so Voice Control still works. The pin and bookmark toggles gain accessibilityValue and .isSelected; the audit and workflow checkboxes gain .isSelected and a hint. Decorative icons split out of Labels are hidden. AppStatusBadgeView now reads as one word ("Critical"), not "icon, Critical", via children: .ignore + label. SectionTitleView and CollapsibleSectionView headers get the .isHeader trait for rotor navigation; the collapsible header also exposes expanded/collapsed as a value with a hint. The header deliberately does NOT use children: .combine — its trailing() closure can hold Track/Pin controls, and combining would swallow them. Dense rows use combine-for-summary, custom-content-for-detail. BatchResultRowView (8 elements) and WatchlistRowView (up to 9) become a single element — domain as label, status as value — with risk, IP, timestamp, source, certificate, and monitoring on the More Content rotor, risk and certificate at .high importance. Reading all of it inline would make a long sweep unnavigable. The custom-content chains live in ViewModifiers because inlining six of them plus the layout broke the type-checker. The shorter 3-4 element portfolio rows are left to NavigationLink's automatic combine, per WWDC21-10121. Technical strings get a speechStyle field on InfoRowViewData: .technical applies speechAlwaysIncludesPunctuation and accessibilityTextContentType(.sourceCode), set on DNS record values and cipher suites so load-bearing punctuation is not swallowed. Completion announcements: the sweep posts from the view model; the single lookup posts from an onChange in the view, since resultsLoaded is derived from many loading flags and has no single view-model moment. Widget: each domain row was a silent 8pt status dot plus a bare "12d" countdown. Rows now read as one phrase ("example.com, critical, certificate expires in 12 days"); the count pills are labelled. Not verifiable by the suite: the dense rows and the widget never render in the audit (no tracked domains or batch results in the test simulator), same limit as the deferred Phase 3 row reflow. Documented in Docs/ACCESSIBILITY.md.
Diffstat (limited to 'Docs/ACCESSIBILITY.md')
-rw-r--r--Docs/ACCESSIBILITY.md43
1 files changed, 43 insertions, 0 deletions
diff --git a/Docs/ACCESSIBILITY.md b/Docs/ACCESSIBILITY.md
index 6032bf6..bc6f061 100644
--- a/Docs/ACCESSIBILITY.md
+++ b/Docs/ACCESSIBILITY.md
@@ -208,6 +208,49 @@ pre-commit that blocks every commit. A hook routinely bypassed with
needs `max(scaled, AppLayout.minimumTapTarget)` or it drops under 44pt for
users who prefer smaller text.
+## VoiceOver conventions
+
+- **Dense rows use combine-for-summary, custom-content-for-detail.**
+ `BatchResultRowView` and `WatchlistRowView` each hold 8–9 text elements.
+ Reading them inline makes a long sweep unnavigable, so each row is one element:
+ `.accessibilityElement(children: .ignore)` + domain label + status value, with
+ the rest on `.accessibilityCustomContent(...)`. `.high` importance is spoken
+ inline; everything else reaches the More Content rotor on a vertical swipe.
+ Rows with only 3–4 elements (the portfolio activity/attention/expiry rows) are
+ left to `NavigationLink`'s automatic combine — custom content is for the dense
+ case, per WWDC21-10121.
+- **The custom-content chain must live in a `ViewModifier`.** Inlined onto a row
+ body, six `.accessibilityCustomContent` calls plus the visual layout blow the
+ Swift type-checker's budget ("unable to type-check in reasonable time").
+ `BatchRowAccessibility` / `WatchlistRowAccessibility` exist for that reason.
+- **Splitting a `Label` exposes its icon; combining a header swallows its
+ trailing controls.** Two opposite traps. A decorative icon pulled out of a
+ `Label` needs `.accessibilityHidden(true)`. A header built as a `Button` must
+ *not* get `.accessibilityElement(children: .combine)` if its label contains
+ other controls (`CollapsibleSectionView`'s `trailing()` holds Track/Pin) —
+ combine would merge them into the header and make them unreachable.
+- **Label-in-name (WCAG 2.5.3).** Every `accessibilityLabel` added to a control
+ with visible text keeps that text, so Voice Control still works. Free-form
+ labels are used only where the control is genuinely icon-only.
+- **Technical strings** get `speechStyle: .technical` on `InfoRowViewData`, which
+ applies `.speechAlwaysIncludesPunctuation()` and
+ `.accessibilityTextContentType(.sourceCode)`. Set today on DNS record values
+ and cipher suites; extend it wherever the view model emits a fingerprint,
+ serial, or record string.
+
+### What the automated audit cannot check
+
+`performAccessibilityAudit()` validates descriptions, traits, contrast, hit
+regions, and clipping. It does **not** exercise VoiceOver speech, the More
+Content rotor, custom-content ordering, or announcements. Those are verified by
+construction and a manual VoiceOver pass (Phase 6), not by the suite. A green
+audit is necessary, not sufficient, for the row and speech work.
+
+Additionally, the dense rows (`BatchResultRowView`, `WatchlistRowView`) and the
+widget never render in the audit — the test simulator has no tracked domains or
+batch results. Their treatment is unverified by the suite for the same reason the
+Phase 3 `ViewThatFits` work was deferred: absence of findings is absence of data.
+
## Notes
- **Disabled controls are a false positive, and are suppressed.** WCAG 1.4.3