Speak the runway facts that only color was carrying - #19
Merged
Conversation
RISCfuture
force-pushed
the
told/a11y-runway-rows
branch
from
September 3, 2026 01:22
ca86de4 to
5e8a49f
Compare
A runway row had no accessibility modifiers at all, so VoiceOver read it as one run-on concatenation and heard none of the three warnings the row draws in color: a distance a NOTAM has shortened, tinted warning; a wind component past the tailwind or crosswind limit, turned red; and a takeoff distance past the runway available, turned red. Color was the only signal for each, so a VoiceOver user was told a runway was usable while the screen was saying it was not. accessibilityCustomContent adds the facts as labelled items without disturbing the label the row already synthesizes. That matters: the UI suite queries staticTexts by identifier and parses the numbers out of element.label, so substituting a label would have broken it. The row also has to combine its children. Custom content propagates to every leaf it covers rather than to the row as a whole, so an uncombined row hung all of it on each of the six leaves the name, distance and wind components produce. A NavigationLink hid that on the takeoff and landing forms by combining anyway, but RunwayPicker — a List row that gets its button trait from a modifier — combines nothing, and that is the screen where runways are compared: VoiceOver stopped six times per runway and repeated every high-importance item at each stop. The identifier and the button trait are applied outside the row and still land on the combined element. Contamination, a shortening, a displaced threshold and each exceeded wind limit are .high, so VoiceOver speaks them without being asked; the declared distances, the surface and the slope are .default and wait in the rotor. A shortening is described as the reduction it is rather than as a closure, because a closure is not something NOTAM can express — a runway closed outright reaches the model only as a shortening the length of the runway. The row reads runway.notam, the relationship RunwayDistances already reaches through, and the unused notam property it declared instead is gone: no caller passed it and nothing read it, so it only promised data the row never had. The landing distance view gets its available distance and no sufficiency verdict. LandingPerformanceViewModel exposes no available landing distance and the view passes InterpolationView no maximum, so no exceedance exists to report to either audience — stating one would invent it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EdB27epdLAZ1x23zkSJ8LT
RISCfuture
force-pushed
the
told/a11y-runway-rows
branch
from
September 3, 2026 04:54
5e8a49f to
ad18f8c
Compare
RISCfuture
added a commit
that referenced
this pull request
Sep 3, 2026
A runway row had no accessibility modifiers at all, so VoiceOver read it as one run-on concatenation and heard none of the three warnings the row draws in color: a distance a NOTAM has shortened, tinted warning; a wind component past the tailwind or crosswind limit, turned red; and a takeoff distance past the runway available, turned red. Color was the only signal for each, so a VoiceOver user was told a runway was usable while the screen was saying it was not. accessibilityCustomContent adds the facts as labelled items without disturbing the label the row already synthesizes. That matters: the UI suite queries staticTexts by identifier and parses the numbers out of element.label, so substituting a label would have broken it. The row also has to combine its children. Custom content propagates to every leaf it covers rather than to the row as a whole, so an uncombined row hung all of it on each of the six leaves the name, distance and wind components produce. A NavigationLink hid that on the takeoff and landing forms by combining anyway, but RunwayPicker — a List row that gets its button trait from a modifier — combines nothing, and that is the screen where runways are compared: VoiceOver stopped six times per runway and repeated every high-importance item at each stop. The identifier and the button trait are applied outside the row and still land on the combined element. Contamination, a shortening, a displaced threshold and each exceeded wind limit are .high, so VoiceOver speaks them without being asked; the declared distances, the surface and the slope are .default and wait in the rotor. A shortening is described as the reduction it is rather than as a closure, because a closure is not something NOTAM can express — a runway closed outright reaches the model only as a shortening the length of the runway. The row reads runway.notam, the relationship RunwayDistances already reaches through, and the unused notam property it declared instead is gone: no caller passed it and nothing read it, so it only promised data the row never had. The landing distance view gets its available distance and no sufficiency verdict. LandingPerformanceViewModel exposes no available landing distance and the view passes InterpolationView no maximum, so no exceedance exists to report to either audience — stating one would invent it. Claude-Session: https://claude.ai/code/session_01EdB27epdLAZ1x23zkSJ8LT Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RunwayRowhad no accessibility modifiers at all, so VoiceOver read it as one run-on concatenation of runway name, distances, "(turf)" and wind components. Worse, three safety-relevant facts were conveyed by color alone and were unreachable:Color.ui.warning(RunwayDistances.swift:56)WindComponents.swift:70,82,93)InterpolationView.swift:189-214)accessibilityCustomContentadds these as labelled items without disturbing the label each view already synthesizes.What is attached
RunwayRow— operation-sensitive, matching whatRunwayDistancesdraws:.high.high.high.high.high.default.default.default.default.defaultTakeoffDistanceView—.high"Runway": "Available takeoff distance insufficient" / "…sufficient", the wording already shipping in the widget (RunwayGridItem.swift:16,20)..default"Takeoff distance available".LandingDistanceView—.default"Landing distance available" only. There is deliberately no sufficiency verdict: the view passesInterpolationViewnomaximum:andLandingPerformanceViewModelhas onlyavailableLandingRun, so no exceedance exists to report to either audience.A shortening is described as the reduction it is, never as a closure —
NOTAMcannot express a closure, and a runway closed outright reaches the model only as a shortening the length of the runway.The row has to combine its children
Custom content propagates to every accessibility leaf it covers, not to the row as a whole. Uncombined, all of it hung on each of the six leaves the row produces.
NavigationLinkhid this on the takeoff and landing forms by combining anyway, butRunwayPicker— aListrow whose button trait comes from a modifier — combines nothing, so VoiceOver stopped six times per runway and repeated every.highitem at each stop. That is the screen where runways are actually compared..accessibilityElement(children: .combine)is applied insideRunwayRow, before the custom content, so the content lands once on the combined element. Measured on the real view, hosted in a real window, readingaccessibilityCustomContentoff each element viaresponds(to:)+value(forKey:):Five items rather than ten because this runway has no NOTAM, no displaced threshold, and TORA == TODA — the guards suppress the rest, as intended.
Also
notam: NOTAM?property onRunwayRow. No caller passed it and nothing read it; the row now reads theRunway.notamrelationship thatRunwayDistancesalready reaches through.FormatStyle.slope(percent, one fraction digit) alongside the other format styles informatters.swift.#Previewblocks now cover takeoff and a NOTAMed, contaminated landing row.The UI suite
No
.accessibilityLabelanywhere, and.accessibilityElementonly onRunwayRow— the two distance views are untouched in that respect, since the suite parseselement.labelfor numeric values ontakeoffDistanceValue/landingDistanceValue.The combine preserves what the suite queries:
runwayRow-30still resolves,isButtonsurvives, and the label stays non-empty (verified in the probe output above).swiftlint --stricton the changed files: clean (the 5 repo-wide violations are pre-existinglegacy_swiftui_aspect_ratiofindings in untouched files)swift-format lint --stricton the changed files: cleanxcodebuild build: succeededtestLandingResultsValueCorrectness(parseslandingDistanceValue, asserts 1769 ±40) andtestRunwayPickerShowsCorrectWindComponents(drives the runway picker, readsrunwayRowLabel("28R")): both passed🤖 Generated with Claude Code
https://claude.ai/code/session_01EdB27epdLAZ1x23zkSJ8LT