Build the VoiceOver tree and actions
The board window's accessibility tree per DESIGN/10: lanes are containers labeled "<title>, lane, N cards" (filter-aware count = renderedCards, the badge's own collection); cards are one flattened element each — label = title or the untitled placeholder, value = attachment count + "cut, pending paste", selection via trait; face icon, stripe, and paperclip are decorative and hidden. Masonry never leaks into traversal: slots carry order-keyed accessibilitySortPriority, so a wide lane reads by card order, not column-major. Lane titles carry the heading trait for the rotor. VO-Space is the ⌘-click analogue routed through the existing BoardStore.click funnel (SelectionGrammar stays the single answer for toggle and container-boundary rules) — cards and lane headers both. Context-menu rows double as custom accessibility actions, each calling the same private method as its menu row so the surfaces cannot drift; trash cards expose Delete and Reveal in Finder and never Open. The trash column is pinned last via sort priority 0, its label/value re-routed through the new AccessibilityPhrases seam; toggling trash visibility posts a one-line announcement from the store seam (both command faces). The invisible lane-resize drag strip leaves the tree — the stepper and menu items are the accessible width path. AccessibilityPhrases is the pure vocabulary seam (labels, values, plural folding shared with TrashModel.phrase), pinned by its own test suite. Both schemes build; 1466 unit tests green. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -117,8 +117,10 @@ struct TrashLaneView: View {
|
||||
// stay individually reachable — combining them would collapse the container the design asks
|
||||
// VoiceOver to enter.
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityLabel("Trash")
|
||||
.accessibilityValue(TrashModel.phrase(renderedCards.count))
|
||||
.accessibilityLabel(AccessibilityPhrases.trashLabel)
|
||||
// The **rendered** count, like a lane's: the shown trash's cards participate in the filter,
|
||||
// so a query narrows the spoken count exactly as it narrows the badge and the column itself.
|
||||
.accessibilityValue(AccessibilityPhrases.trashValue(cards: renderedCards.count))
|
||||
}
|
||||
|
||||
/// The cards the column shows.
|
||||
@@ -257,7 +259,7 @@ struct TrashLaneView: View {
|
||||
// has scrolled to, so an unbuilt row is invisible to both. The constraint is affordable
|
||||
// because a trash is small: it holds one board's deletions, and Empty Trash… exists.
|
||||
MasonryLayout(columns: 1, spacing: cardSpacing) {
|
||||
ForEach(slots) { slot in
|
||||
ForEach(Array(slots.enumerated()), id: \.element.id) { index, slot in
|
||||
Group {
|
||||
switch slot {
|
||||
case let .card(card):
|
||||
@@ -282,6 +284,12 @@ struct TrashLaneView: View {
|
||||
// should read alike from either side of the strip. The transaction is the
|
||||
// reload's, like the lanes' (`Motion.reloadAnimates`).
|
||||
.transition(Motion.cardTransition(reduced: reduceMotion))
|
||||
// `order`-keyed traversal, `LaneView`'s rule on the trash side. The column is
|
||||
// one masonry column, so geometry and `order` agree here and the priority is
|
||||
// belt over braces — written anyway because the *reason* it agrees is the
|
||||
// column's fixed one width unit, which is a layout fact rather than a
|
||||
// traversal guarantee, and a two-unit trash would silently read column-major.
|
||||
.accessibilitySortPriority(Double(slots.count - index))
|
||||
// The scroll target — `LaneView`'s rule, and outermost for its reason.
|
||||
.id(slot.id)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user