fix(windows): screen-reader accessibility for context menus and channel tree

- ContextMenuStrip: select first item on Opened so keyboard-invoked menus
  (Shift+F10 / Apps key) raise the UIA focus event immediately instead of
  staying silent until the first arrow key.
- Name the SplitContainer/SplitterPanel containers so screen readers announce
  orientation instead of a stack of anonymous pane nodes.
- Take the resize splitters out of the Tab cycle (TabStop = false) so focus
  moves control-to-control.
- RefreshChannelTree: restore keyboard focus and re-announce the current node
  after a Nodes.Clear()/rebuild, so the tree no longer loses focus when
  channels/users change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 13:34:26 +02:00
parent 483f889910
commit 725bd8e925
2 changed files with 42 additions and 0 deletions

View File

@@ -140,6 +140,13 @@ partial class MainForm
splitLeft.Panel1MinSize = 100; splitLeft.Panel1MinSize = 100;
splitLeft.Panel2MinSize = 80; splitLeft.Panel2MinSize = 80;
splitLeft.TabIndex = 0; splitLeft.TabIndex = 0;
// Keep the resize splitter out of the Tab cycle so focus moves control-to-control.
splitLeft.TabStop = false;
// Name the otherwise-anonymous "pane" containers so screen readers announce
// orientation instead of a stack of unnamed panes.
splitLeft.AccessibleName = "Channels and users";
splitLeft.Panel1.AccessibleName = "Channels";
splitLeft.Panel2.AccessibleName = "Users";
splitLeft.Panel1.Controls.Add(tvChannels); splitLeft.Panel1.Controls.Add(tvChannels);
splitLeft.Panel1.Controls.Add(lblChannels); splitLeft.Panel1.Controls.Add(lblChannels);
splitLeft.Panel2.Controls.Add(lstUsers); splitLeft.Panel2.Controls.Add(lstUsers);
@@ -214,6 +221,10 @@ partial class MainForm
splitMain.Dock = DockStyle.Fill; splitMain.Dock = DockStyle.Fill;
splitMain.Panel1MinSize = 150; splitMain.Panel1MinSize = 150;
splitMain.TabIndex = 1; splitMain.TabIndex = 1;
splitMain.TabStop = false;
splitMain.AccessibleName = "Main";
splitMain.Panel1.AccessibleName = "Channels and users";
splitMain.Panel2.AccessibleName = "Chat and activity";
splitMain.Panel1.Controls.Add(splitLeft); splitMain.Panel1.Controls.Add(splitLeft);
splitMain.Panel2.Controls.Add(tblRight); splitMain.Panel2.Controls.Add(tblRight);

View File

@@ -201,6 +201,7 @@ public partial class MainForm : Form
ctx.Items.Add("&Delete channel...", null, (_, _) => DeleteSelectedChannel()); ctx.Items.Add("&Delete channel...", null, (_, _) => DeleteSelectedChannel());
} }
}; };
ctx.Opened += (_, _) => SelectFirstMenuItem(ctx);
tvChannels.ContextMenuStrip = ctx; tvChannels.ContextMenuStrip = ctx;
} }
@@ -247,9 +248,26 @@ public partial class MainForm : Form
} }
} }
}; };
ctx.Opened += (_, _) => SelectFirstMenuItem(ctx);
lstUsers.ContextMenuStrip = ctx; lstUsers.ContextMenuStrip = ctx;
} }
// Work around the WinForms ContextMenuStrip accessibility bug: when opened by
// keyboard (Shift+F10 / Apps key) the focused item is not set, so screen readers
// stay silent until the first arrow key. Selecting the first item ourselves on
// Opened raises the UIA focus event immediately.
private static void SelectFirstMenuItem(ContextMenuStrip ctx)
{
foreach (ToolStripItem item in ctx.Items)
{
if (item is ToolStripMenuItem && item.Enabled)
{
item.Select();
break;
}
}
}
// ── Event dispatch ──────────────────────────────────────────────────────── // ── Event dispatch ────────────────────────────────────────────────────────
private void OnEvent(VoiceCatEvent ev) private void OnEvent(VoiceCatEvent ev)
@@ -468,6 +486,7 @@ public partial class MainForm : Form
private void RefreshChannelTree() private void RefreshChannelTree()
{ {
uint toSelect = tvChannels.SelectedNode?.Tag is uint s ? s : _currentChannelId; uint toSelect = tvChannels.SelectedNode?.Tag is uint s ? s : _currentChannelId;
bool hadFocus = tvChannels.Focused;
tvChannels.BeginUpdate(); tvChannels.BeginUpdate();
tvChannels.Nodes.Clear(); tvChannels.Nodes.Clear();
@@ -493,6 +512,18 @@ public partial class MainForm : Form
tvChannels.ExpandAll(); tvChannels.ExpandAll();
SeekAndSelect(tvChannels.Nodes, toSelect); SeekAndSelect(tvChannels.Nodes, toSelect);
tvChannels.EndUpdate(); tvChannels.EndUpdate();
// A Nodes.Clear()/rebuild can drop keyboard focus and leave the screen reader
// without a current node. If the tree was focused before the refresh, restore
// focus and re-announce the now-current node (null-then-reselect forces UIA to
// fire a fresh focus event).
if (hadFocus && tvChannels.SelectedNode != null)
{
var node = tvChannels.SelectedNode;
tvChannels.Focus();
tvChannels.SelectedNode = null;
tvChannels.SelectedNode = node;
}
} }
private bool SeekAndSelect(TreeNodeCollection nodes, uint channelId) private bool SeekAndSelect(TreeNodeCollection nodes, uint channelId)