Skip to content

fix(a11y): accessibility audit - PortkeyDrop dialogs and main window#113

Merged
Orinks merged 8 commits intodevfrom
audit/portkeydrop-a11y-2026
Mar 22, 2026
Merged

fix(a11y): accessibility audit - PortkeyDrop dialogs and main window#113
Orinks merged 8 commits intodevfrom
audit/portkeydrop-a11y-2026

Conversation

@Orinks
Copy link
Owner

@Orinks Orinks commented Mar 22, 2026

Summary

Comprehensive accessibility audit of all PortkeyDrop dialogs and the main window, targeting NVDA (Windows) and VoiceOver (macOS) screen reader compatibility.

Issues fixed

SetName() misuse removed

  • SetName() sets an internal widget name that screen readers cannot read. Removed all dialog-level and panel-level SetName() calls across app.py, host_key_dialog, properties, quick_connect, settings, site_manager, transfer, migration_dialog, update_dialog.

StaticText labels added for wx.ListCtrl accessibility

  • NVDA resolves list control labels by walking backward through sibling HWNDs to find a preceding StaticText. Added "Local files:" and "Remote files:" labels in app.py, and "Transfer Queue:" in transfer.py. Removed the broken SetLabel()/SetName() calls on the list controls themselves.

SetLabelFor wired for programmatic label association

  • properties.py: Each field label now calls lbl.SetLabelFor(val).
  • quick_connect.py: _link() helper calls SetLabelFor for all form fields.
  • site_manager.py: SetLabelFor added in the fields loop.
  • import_connections.py: Path TextCtrl now has a visible label with SetLabelFor.

Default buttons fixed (Enter key activation)

  • host_key_dialog: Reject button is now the default — safest action for a security prompt.
  • import_connections: Next/Import button set as default per wizard step.
  • migration_dialog: "Migrate selected" set as default.
  • quick_connect: OK button set as default.
  • site_manager: Save button set as default.
  • transfer: Close button set as default.

Initial focus

  • host_key_dialog: Focus lands on Reject button (not the read-only detail text) so screen readers immediately announce the security decision.
  • migration_dialog: Focus on first checkbox.
  • quick_connect: Focus on protocol choice (first interactive field).
  • site_manager: Focus on site list via wx.CallAfter.
  • transfer: Focus on transfer list.
  • update_dialog: Focus + SetInsertionPoint(0) on release notes field.

Keyboard navigation

  • host_key_dialog: Escape → EndModal(REJECT).
  • migration_dialog: Escape handler via EVT_CHAR_HOOK.
  • site_manager: Escape handler; Close uses wx.ID_CANCEL.
  • update_dialog: SetEscapeId(wx.ID_CANCEL).
  • app.py: Ctrl+L focuses tb_host (toolbar visible) or remote_path_bar (toolbar hidden), with _announce() call.
  • transfer: Send-to-background returns focus to the parent window.

Descriptive button labels

  • Site manager Browse → "&Browse for key file..."
  • Transfer Retry → "&Retry Selected Transfer", Remove → "&Remove Transfer"

Test plan

  • All 777 tests pass (PYTHONPATH=src python -m pytest tests/ -q)
  • Updated all wx stubs (added SetDefault, SetFocus, SetEscapeId, SetLabelFor, WXK_ESCAPE, EndModal, Bind as needed)
  • Replaced test_dialog_sets_accessible_name (was checking SetName) with test_dialog_title_is_accessible_name
  • Replaced test_initial_focus_is_security_text with test_initial_focus_is_reject_button
  • Added _toolbar_panel MagicMock to _hydrate_frame in test_app.py

🤖 Generated with Claude Code

Orinks and others added 8 commits March 22, 2026 06:33
Issues found and fixed across all dialogs and the main window:

**SetName() misuse removed**
- Removed SetName() calls on dialog self, panels, and most controls throughout
  app.py, host_key_dialog, properties, quick_connect, settings, site_manager,
  transfer, migration_dialog, update_dialog. SetName() sets an internal widget
  name not read by screen readers.

**StaticText labels added for screen reader access**
- app.py: Added "Local files:" and "Remote files:" StaticText labels before
  each wx.ListCtrl (the correct NVDA label mechanism for list controls).
  Removed the broken SetLabel/SetName calls on the lists themselves.
- import_connections: Added "Configuration &path:" StaticText label before
  the path TextCtrl and wired SetLabelFor.
- transfer: Added "Transfer Queue:" StaticText label before the transfer list.

**SetLabelFor wired for programmatic label association**
- properties: Each field label now calls lbl.SetLabelFor(val) to create an
  explicit accessible link between label and read-only value control.
- quick_connect: Added _link() helper calling SetLabelFor for all fields.
- site_manager: Added SetLabelFor in the fields loop replacing bare SetName.

**Default buttons fixed**
- host_key_dialog: Reject button is now the default (safest action for a
  security prompt); previously no default was set.
- import_connections: Next/Import button is set as default per wizard step.
- migration_dialog: "Migrate selected" button set as default.
- quick_connect: OK button set as default via FindWindowById.
- site_manager: Save button set as default.
- transfer: Close button set as default.

**Initial focus fixed**
- host_key_dialog: Focus lands on Reject button (not the read-only detail
  TextCtrl) so screen readers announce the security decision immediately.
- migration_dialog: Focus lands on the first checkbox.
- quick_connect: Focus lands on protocol choice (first interactive field).
- site_manager: Focus lands on the site list via wx.CallAfter.
- transfer: Focus lands on the transfer list.
- update_dialog: release_notes TextCtrl receives focus and insertion point
  is set to 0 so screen readers read from the top.

**Keyboard navigation**
- host_key_dialog: Escape now calls EndModal(REJECT) via EVT_CHAR_HOOK.
- migration_dialog: Escape handler added via EVT_CHAR_HOOK.
- site_manager: Escape handler added; Close button uses wx.ID_CANCEL.
- update_dialog: SetEscapeId(wx.ID_CANCEL) set for standard Escape behaviour.
- app.py: Ctrl+L now focuses tb_host when toolbar is visible, or
  remote_path_bar when toolbar is hidden, with an _announce() call.
- transfer: Send-to-background returns focus to the parent window.

**Button labels made more descriptive**
- site_manager: Browse button → "&Browse for key file..."
- transfer: "Retry" → "&Retry Selected Transfer", "Remove" → "&Remove Transfer"

**Tests updated to match fixes**
- Updated stubs in all affected test files (SetDefault, SetFocus, SetEscapeId,
  SetLabelFor, WXK_ESCAPE, EndModal, Bind).
- Replaced test_dialog_sets_accessible_name (checked SetName) with
  test_dialog_title_is_accessible_name (checks dialog title).
- Replaced test_initial_focus_is_security_text with
  test_initial_focus_is_reject_button.
- Added _toolbar_panel MagicMock to _hydrate_frame in test_app.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this, pytest loaded portkeydrop from the main project's .pth
installation instead of the worktree's src/, causing test_initial_focus_is_reject_button
to load an older host_key_dialog.py that lacks reject_btn.SetDefault().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Orinks Orinks merged commit 67669db into dev Mar 22, 2026
6 checks passed
@Orinks Orinks deleted the audit/portkeydrop-a11y-2026 branch March 22, 2026 13:48
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