Skip to content

feat(macos): Emit geometry notifications so VoiceOver tracks moved content - #826

Open
William-Selna wants to merge 2 commits into
AccessKit:mainfrom
William-Selna:macos-geometry-notifications-v2
Open

William-Selna wants to merge 2 commits into
AccessKit:mainfrom
William-Selna:macos-geometry-notifications-v2

Conversation

@William-Selna

Copy link
Copy Markdown

When content moves under a stationary VoiceOver cursor (for example a scroll with the cursor on an element inside the viewport), the macOS adapter emits nothing, so the cursor highlight stays at the element's old frame until something unrelated makes VoiceOver re-read it.

This queues one NSAccessibilityLayoutChangedNotification per tree update that changed any exposed node's bounds, posted on the focused element without user info, and adds Adapter::scroll_settled_event so a toolkit can re-post the focus notification once per scroll gesture, which is what actually makes VoiceOver re-anchor its cursor.

Although Chromium, WebKit and Firefox post nothing on scroll and live with the stale highlight, the Unix adapter already emits BoundsChanged, so this would create parity between platforms.
Although Chromium, WebKit and Firefox post nothing on scroll and live with the stale highlight.

I verified the implementation live with VoiceOver on MacOS

Also moves QueuedEvents construction out of EventGenerator so the batching logic is unit-testable without AppKit.

William-Selna and others added 2 commits October 15, 2026 23:47
…ntent

When content moves under a stationary VoiceOver cursor -- the canonical case
being a scroll while the cursor sits on an element inside the viewport -- the
macOS adapter gave VoiceOver no signal that geometry had changed, so the cursor
highlight stayed drawn at the element's old frame until some unrelated trigger
made VoiceOver re-read it. The element's accessibilityFrame was correct the
whole time; nothing prompted a re-read.

Two complementary additions fix that:

* The event generator compares each updated node's bounding box and queues a
  single NSAccessibilityLayoutChangedNotification per tree update, posted on
  the focused element (root fallback) with no user info. Both details matter:
  NSAccessibilityUIElementsKey marks elements as added or changed, which makes
  VoiceOver move focus and re-announce, and posting one notification per moved
  node fires at scroll rate with the same effect.

* Adapter::scroll_settled_event, surfaced on SubclassingAdapter, re-posts the
  focus notification on the still-focused node. That is the documented trigger
  that makes VoiceOver re-read an element's frame and re-anchor its cursor.
  Toolkits call it once per scroll gesture, after the scroll offset settles,
  never once per frame.

This brings macOS closer to the Unix adapter, which already emits
ObjectEvent::BoundsChanged on a bounds and transform delta.

EventGenerator no longer holds the Context. It only used it to build the
QueuedEvents at the end, so the Adapter now does that instead, which lets the
event generator be unit tested without AppKit and therefore without pinning
the tests to the main thread.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant