Comments, phase 3 — search, the thread find, announcements, and a11y
Board search reaches comment bodies through a search-owned transient
index: the first live-query keystroke sweeps comments/*/index.md
off-actor (.draft and comments/.trash excluded), keystrokes re-filter
in memory, the index discards on clear — the snapshot stays O(cards).
⌘F routes by focus: the comments pane gets an app-owned find bar
spanning the whole rendered thread (next/prev cross rows with
wraparound); body and composer keep NSTextFinder; Find Next/Previous
graduate from FutureCommands. Foreign comment changes speak
path-shaped beside the announcer's ladder ("New comment on 'X'",
plural folds), narrowed by EchoLedger receipts consumed through
CommentPath.classify — and that read fixed a latent footprint bug
where a comment receipt resolved against the card's attachment
listing, read .absent, and classified the user's own write as
foreign. The pane completes its a11y story: flattened comment
elements with Edit/Delete/Reveal custom actions (un-flattening
during inline edit), phrase-table vocabulary, labeled composer and
sort control, and an audit over the open pane on a comment-seeded
fixture (runnable only where automation permission exists).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -50,14 +50,38 @@ struct CardCommentsPane: View {
|
||||
.padding(.horizontal, CardWindowMetrics.gutter(bodyPointSize: pointSize))
|
||||
.padding(.top, CardWindowMetrics.gutter(bodyPointSize: pointSize))
|
||||
|
||||
// **The find bar sits above the thread it searches** — `findBarPosition = .aboveContent`,
|
||||
// which is where every other find in this window puts its bar (`CardBodySurface`).
|
||||
if comments.find.isShowing {
|
||||
CommentFindBar(find: comments.find)
|
||||
.padding(.top, CardWindowMetrics.previewPadding(bodyPointSize: pointSize))
|
||||
}
|
||||
|
||||
thread
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
// 10-accessibility.md's container label for the pane ("Comments, N"). The elements inside it
|
||||
// and their custom actions are phase 3's; the container is here because the pane would
|
||||
// otherwise be an unnamed region the moment it exists.
|
||||
// 10-accessibility.md's container label for the pane ("Comments, N") — the count is the
|
||||
// thread's, so the spoken container and the visible header can never disagree.
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityLabel("Comments, \(comments.thread.comments.count)")
|
||||
.accessibilityLabel(AccessibilityPhrases.commentsContainerLabel(count: comments.thread.comments.count))
|
||||
// The find's model of the rendered thread, rebuilt only while the bar is up: the search runs
|
||||
// over every comment, mounted or not (`CommentThreadFind`), and building it costs a render
|
||||
// pass the pane has no reason to spend on a window nobody is searching.
|
||||
.onChange(of: FindThreadKey(isShowing: comments.find.isShowing, thread: comments.thread), initial: true) { _, key in
|
||||
guard key.isShowing else { return }
|
||||
comments.find.setThread(key.thread.comments, pointSize: pointSize)
|
||||
}
|
||||
// **A find cannot outlive the surface it searches.** The pane unmounts on View ▸ Show Comments
|
||||
// and on the raw-source swap, and a session left showing would keep ⌘F and ⌘G pointed at a bar
|
||||
// nobody can see (`CardWindowFind.route`'s open-bar clause).
|
||||
.onDisappear { comments.find.dismiss() }
|
||||
}
|
||||
|
||||
/// The pair the find's rebuild is keyed on. A value rather than two `onChange`s so the bar opening
|
||||
/// and the thread changing take the same path, and so the rebuild cannot run twice for one update.
|
||||
private struct FindThreadKey: Equatable {
|
||||
let isShowing: Bool
|
||||
let thread: CommentThread
|
||||
}
|
||||
|
||||
// MARK: - Header
|
||||
@@ -88,7 +112,7 @@ struct CardCommentsPane: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help(direction.controlLabel)
|
||||
.accessibilityLabel("Sort")
|
||||
.accessibilityLabel(AccessibilityPhrases.commentSortLabel)
|
||||
.accessibilityValue(direction.controlLabel)
|
||||
}
|
||||
|
||||
@@ -129,6 +153,15 @@ struct CardCommentsPane: View {
|
||||
.onChange(of: comments.focusComposerRequests) { _, _ in
|
||||
proxy.scrollTo(Self.composerAnchor, anchor: direction.placesComposerFirst ? .top : .bottom)
|
||||
}
|
||||
// **A find that steps off screen brings the reader with it** — the half of "it reads as one
|
||||
// find" that the highlight alone cannot do. Scrolling to the comment rather than to the
|
||||
// range is what a row-shaped thread allows: rows are the scroll targets
|
||||
// (`ForEach(...).id(comment.id)`), and a comment is short enough that its top is the hit's
|
||||
// neighbourhood.
|
||||
.onChange(of: comments.find.currentMatch) { _, match in
|
||||
guard let match else { return }
|
||||
proxy.scrollTo(match.comment, anchor: .center)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user