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:
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user