Skip to content

JAWS no longer repeats a closed dialog's text - #250

Merged
kellylford merged 2 commits into
mainfrom
fix/jaws-stray-dialog-text
Nov 8, 2026
Merged

kellylford merged 2 commits into
mainfrom
fix/jaws-stray-dialog-text

Conversation

@kellylford

@kellylford kellylford commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

What Kelly heard

With JAWS, after any dialog closed, its first line of text came back after the main window's title every time he returned to Scores or the screen changed: Alt+Tab back, Escape to Live Scores, Escape to home. From a game's News: "Live Scores - Sports Scores", then "News Headlines (🎯 = Game-specific, Enter or double-click to open in browser):". From a league's News: "Press Enter or double-click a headline to open in your browser:", heard on screens with no news at all. 1.1.1 does the same (Kelly confirmed it, and a 1.1.1 take is in the report), so it isn't a regression. NVDA never says it, which is why the NVDA runs missed it.

Why

Measured with JAWS 2027.2610.428 in the test VM, using JAWS's exact speech, read from its own speech history through its COM API (FreedomSci.JawsApi: ClearSpeechHistory before each key, CopySpeechHistoryToClipboard after it).

As a dialog opens, JAWS reads the static text that comes before its first focusable control and keeps it with the window that owns the dialog, which here is the main window. After the dialog closes, JAWS says that text after the owner's title whenever it announces the owner, until another dialog replaces it. The dialog and its view are long gone by then (it's still said on the home screen), and nothing in the UIA or MSAA trees holds the text any more.

A minimal PyQt6 app (one main window, one QDialog(self) with a label above a list) reproduces it. Its variants pin down the cause:

Variant Repeated after close?
as built yes
WA_DeleteOnClose yes
label is the list's buddy yes
NotifyWinEvent(EVENT_OBJECT_REORDER) on the main window yes
the main window has a static text of its own yes (JAWS says both)
dialog with no owner / a hidden owner no, but Alt+Tab while the dialog is open no longer leaves the app
label after the list no
label after the list in children order only (raise_(), nothing moves on screen) no
the instruction as a QGroupBox title no (still read on open as the group name)
a dialog with no text opened afterwards clears it

The fix

dialog_text.DialogTextOrder, installed app-wide next to ButtonKeys, puts every part of a dialog that holds nothing Tab can reach after the parts that do, in Qt's accessibility order (raise_()), as each dialog shows and when it's activated again. It also looks inside tab pages and scroll areas. Nothing moves on screen or in the tab order. An exception in it is logged, not allowed to end the app.

Trade-off: as a dialog opens, JAWS says its title and the control you're on, but no longer reads the heading or instruction above the first control by itself. A message box's message is still read on open (JAWS reads it as an alert; checked in the harness).

JAWS before and after (exact speech)

Before is the 1.2.0 candidate. After is this branch.

Path, key Before After
Live Scores > Lazio at Napoli > News > Esc, Esc back to Live Scores "Live Scores - Sports Scores" / "News Headlines (🎯 = Game-specific, ...)" / "Live Scores - All Sports: ..." "Live Scores - Sports Scores" / "Live Scores - All Sports: ..."
same, Alt+Tab back "Live Scores - Sports Scores" / "News Headlines (🎯 ...)" "Live Scores - Sports Scores" / "Live Scores - Last updated ..."
same, Esc to home "News Headlines (🎯 ...)" / ... "Sports Scores" / "Select a League:" ...
NBA > News Headlines > Enter > Esc, Alt+Tab back "NBA, Sunday, November 08, 2026 - Sports Scores" / "Press Enter or double-click a headline to open in your browser:" / "Date: ..." "NBA, Sunday, November 08, 2026 - Sports Scores" / "Date: ..."
same, Esc to home and Alt+Tab back "Sports Scores" / "Press Enter or double-click a headline ..." / "Select a League:" "Sports Scores" / "Select a League:"
NBA > Statistics > Enter > Esc, Alt+Tab back, Esc to home "NBA Statistics Selection" after the title each time not said

1.1.1 (JAWS, same Statistics path): "NBA Statistics Selection" after the title on Alt+Tab back and on Escape to home, as in the candidate.

NVDA 2026.2 (its own log) on this branch: as each dialog opens, NVDA still reads the dialog's text ("News Headlines - NBA, dialog, Press Enter or double-click a headline to open in your browser:"), since it doesn't go by order. After the dialog closes, it says only the title and the list, as before.

Code review (code-reviewer): fixed what it found. Only Tab-reachable widgets count as controls, so the result doesn't depend on the style; tab pages and scroll-area contents are reordered too; the walk stops at child windows; errors are logged; more tests. The JAWS takes above were rerun on the reviewed build.

Logs, steps and the harness are in Kelly's testing folder (jaws-stray-dialog-text-2026-11-08).

Tests

tests/unit/test_dialog_text.py (offscreen): order after show and after re-activation, nested containers, tab pages and scroll areas, hidden controls, click focus alone, child dialogs, button boxes, stacked widgets, a dialog with nothing focusable, message boxes, an unchanged focus chain and geometry, errors logged, main-window views untouched. Full suite: 5103 passed, 4 xfailed.

🤖 Generated with Claude Code

kellylford and others added 2 commits November 8, 2026 07:31
As a dialog opens, JAWS reads its static text (what comes before the first
control that takes focus) and keeps it with the window that owns the dialog.
After the dialog closed, it said that text again after the main window's
title on Alt+Tab back, Escape to home and every view change: "Press Enter or
double-click a headline to open in your browser:" from a league's News, the
game News label from Live Scores. 1.1.1 did the same. NVDA keeps nothing.

dialog_text.DialogTextOrder, installed app-wide, puts each part of a dialog
that holds nothing focusable after the parts that do, in Qt's accessibility
order (raise_ moves a widget to the end of its parent's children), as the
dialog shows and when it's activated again. Nothing moves on screen or in the
tab order. Measured with JAWS 2027 speech history in the VM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Count only what Tab reaches as a control (click focus alone is static text
  to a screen reader), so a message box's message follows its buttons in
  every style, not only Fusion.
- Look inside a tab widget's pages and a scroll area's contents, which are
  focusable containers.
- Walk children ourselves and stop at child windows.
- An exception in the filter is logged, not allowed to end the app.
- Tests for tab pages, scroll areas, button boxes, stacked widgets, a dialog
  with nothing focusable, the whole focus chain, and the error path.
- CHANGELOG and CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kellylford
kellylford deployed to azure-signing November 8, 2026 14:18 — with GitHub Actions Active
@kellylford
kellylford merged commit b41dd9c into main Nov 8, 2026
1 check passed
@kellylford
kellylford deleted the fix/jaws-stray-dialog-text branch November 8, 2026 14:59
kellylford added a commit that referenced this pull request Nov 8, 2026
The first JAWS run of the app: exact JAWS speech from its speech history
through its COM API, the before and after for #250, the PyQt harness of the
variants tried, and how to run it again.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
azure-signing — 3aece411 Deployed Nov 8, 2026 by kellylford via build #364
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