Skip to content

Commit 474271b

Browse files
authored
fix(tabs): keep the editor tabs reachable to accessibility over the pointer's owner (#2576)
Claude-Session: https://claude.ai/code/session_01L7uaHbJBPV1LaWL5QXzxyp
1 parent 4716cd8 commit 474271b

2 files changed

Lines changed: 18 additions & 2 deletions

File tree

TablePro/Views/Main/EditorTabInteractionView.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ internal final class EditorTabInteractionView: NSView {
3131
/// the content.
3232
internal var onRowCountChanged: ((Int) -> Void)?
3333

34+
private var isResolvingAccessibilityHit = false
3435
private var hoverTrackingArea: NSTrackingArea?
3536
private var lastActivatedTabId: UUID?
3637

@@ -110,12 +111,27 @@ internal final class EditorTabInteractionView: NSView {
110111

111112
/// Claims the track and nothing else, so a press on a tab is this view's and a press on the
112113
/// new-tab button, on the band's insets or on the chrome below the track is not.
114+
///
115+
/// The claim is for the pointer alone. Accessibility resolves a screen point through this same
116+
/// method, so claiming it unconditionally answered "the strip" for every tab and took all of
117+
/// them out of reach of VoiceOver, Switch Control, Voice Control and XCUITest at once: this
118+
/// view publishes nothing, so there was no element under a tab to speak, press or click. That
119+
/// shipped in #2571 and turned the whole UI suite red from the commit that merged it.
113120
override internal func hitTest(_ point: NSPoint) -> NSView? {
121+
guard !isResolvingAccessibilityHit else { return super.hitTest(point) }
114122
let local = convert(point, from: superview)
115123
guard trackRect.contains(local) else { return super.hitTest(point) }
116124
return self
117125
}
118126

127+
/// Answers from the SwiftUI tree, which is where the tabs publish themselves. The pointer's
128+
/// claim is lifted for the length of the question.
129+
override internal func accessibilityHitTest(_ point: NSPoint) -> Any? {
130+
isResolvingAccessibilityHit = true
131+
defer { isResolvingAccessibilityHit = false }
132+
return super.accessibilityHitTest(point)
133+
}
134+
119135
private func tabIndex(atViewPoint point: CGPoint) -> Int? {
120136
EditorTabRunLayoutBuilder.index(at: contentPoint(fromViewPoint: point), in: interaction.run)
121137
}

TableProUITests/EditorTabDetachUITests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ final class EditorTabDetachUITests: UITestCase {
8080
let target = try XCTUnwrap(tabLabels(in: window).last)
8181
tab(named: target, in: window).rightClick()
8282

83-
let item = app.menuItems["Move Tab to New Window"]
83+
let item = app.menuItems.matching(identifier: "Move Tab to New Window").firstMatch
8484
XCTAssertTrue(item.waitToExist(timeout: 5), "The command must be listed")
8585
XCTAssertTrue(item.isEnabled, "It must be offered on an idle tab among others")
8686
app.typeKey(XCUIKeyboardKey.escape, modifierFlags: [])
@@ -92,7 +92,7 @@ final class EditorTabDetachUITests: UITestCase {
9292
let target = tab(named: name, in: window)
9393
XCTAssertTrue(waitUntilHittable(target, timeout: 20), "The tab must be hittable")
9494
target.rightClick()
95-
let item = app.menuItems["Move Tab to New Window"]
95+
let item = app.menuItems.matching(identifier: "Move Tab to New Window").firstMatch
9696
XCTAssertTrue(item.waitToExist(timeout: 5), "The command must be listed")
9797
item.click()
9898
}

0 commit comments

Comments
 (0)