Realign code with the 2026-07-29 accessibility rulings
Three ruled behavior changes (DESIGN/10, resolution session 2026-07-29): - The board-change digest covers the trash while View > Show Trash is on: BoardDiff.between gains includingTrash, keying its card index by ItemPath so foreign purges, restores, and Empty Trash join the digest; crossings of the trash boundary still read deleted/restored, never moved, on both sides of the toggle. BoardStore.land passes the store's own isTrashVisible - no new injection seam. - A vanished head with surviving co-selection is still named: naming and recovery are independent axes, so BoardAnnouncer's vanished-focus rung fires on all branches while the survivors-veto now gates only the recovery half (recovery implies vanished, no longer both-or-neither). - Banner-row buttons are literal FKA Tab stops: BannerRow.controls is the row's testable button inventory, BannerRowView renders from it with .focusable() on each button; the combined VoiceOver element stays unconditional - custom actions and Tab stops are independent surfaces. 19 tests added, 2 expectations updated to the rulings. 1607 green on both schemes. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -238,6 +238,58 @@ public enum BannerRow: Identifiable, Sendable {
|
||||
case .readOnlyLock, .reloadBreakage, .historySuspended, .inProgress: nil
|
||||
}
|
||||
}
|
||||
|
||||
/// **This row's buttons, in the order Tab visits them** — Cancel, then Dismiss.
|
||||
///
|
||||
/// It exists because 10-accessibility.md ▸ Full Keyboard Access rules the banner's buttons in by
|
||||
/// name (2026-07-29): "'Every control' is literal and includes banner-row buttons — a Dismiss or
|
||||
/// Cancel on a banner must be a Tab stop … Cancel on an in-progress operation is exactly the
|
||||
/// control that cannot require a pointer". A claim about *which* controls a row has is then a
|
||||
/// fact about the row's data rather than about a view's `if` ladder, so it can be pinned
|
||||
/// headlessly and the strip can render straight from it (`BannerStripView`) — which is the same
|
||||
/// posture the rest of this type already takes ("the per-kind affordances hang off the row's
|
||||
/// data, not off separate views").
|
||||
///
|
||||
/// No row has both today: the two conditions are disjoint by construction (only an in-progress
|
||||
/// row cancels, and an in-progress row is never dismissable). The order is stated anyway, since
|
||||
/// it is the Tab order the moment one does.
|
||||
public var controls: [BannerRowControl] {
|
||||
var controls: [BannerRowControl] = []
|
||||
if case let .inProgress(operation) = self, let cancel = operation.cancel {
|
||||
controls.append(.cancel(cancel))
|
||||
}
|
||||
if let dismissID {
|
||||
controls.append(.dismiss(dismissID))
|
||||
}
|
||||
return controls
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - A row's buttons
|
||||
|
||||
/// One button on a banner row — the strip's whole vocabulary of per-row controls, as data.
|
||||
///
|
||||
/// **Identified by its label**, which is legitimate rather than lazy here: a row carries at most one
|
||||
/// of each kind, the label is the user-facing name of exactly that kind, and it is what both
|
||||
/// surfaces the ruling cares about need — the button's title (Cancel) or its accessibility label
|
||||
/// (Dismiss, whose face is an ✕ glyph).
|
||||
public enum BannerRowControl: Identifiable, Sendable {
|
||||
|
||||
/// Stop the operation this row is reporting and remove its partial work — carried by cancelable
|
||||
/// in-progress rows only (`InProgressOperation`: "safe copies only").
|
||||
case cancel(@MainActor @Sendable () -> Void)
|
||||
|
||||
/// Clear this row, by the id `BannerCenter.dismiss(_:)` takes.
|
||||
case dismiss(UUID)
|
||||
|
||||
public var label: String {
|
||||
switch self {
|
||||
case .cancel: "Cancel"
|
||||
case .dismiss: "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
public var id: String { label }
|
||||
}
|
||||
|
||||
// MARK: - BannerCenter
|
||||
|
||||
Reference in New Issue
Block a user