fix(central-ui): InstancePicker — keyboard activation selects/toggles through TreeView selection; aria-label/InputId; aria-controls only while open
ArrowDown moved focus into the tree, but the picker passed no Selectable /
SelectedKeyChanged, so Enter/Space on a treeitem only moved the roving tabindex —
the pick lived on the label span's @onclick, i.e. mouse only. The tree now runs in
Selectable mode and BOTH gestures arrive at one handler (OnTreeKeyActivatedAsync)
through TreeView's SelectedKeyChanged: an `i:{id}` key selects that instance, an
`a:{id}` key toggles the branch. The label's own @onclick is gone — the click
already bubbles into TreeView's content span, so a second handler there would fire
the callback twice. SelectedKey stays unbound (a combobox shows its pick in the
field, not as a row highlight, and binding it would arm TreeView's
selected-key-not-in-tree reset on every render while the pick is filtered out);
SelectedCssClass is emptied to match.
a11y: the field gains an accessible name via a new AriaLabel parameter (default
"Instance") and an optional InputId so a page can point a <label for> at it;
aria-controls is rendered only while the panel exists, instead of always pointing
at an id that is absent when closed.
Also drops the unused @using of Commons.Entities.Instances from the .razor.
Tests: keyboard activation on an instance row and on an area row, a
one-selection-per-gesture guard across mouse and keyboard, and the labelling
assertions. CentralUI suite 1331 passed / 0 failed; build 0 warnings.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
Instance Picker — a combobox whose dropdown is the site's area tree (design
|
||||
2026-09-08). The field shows the selected instance; the panel hosts the shared
|
||||
TreeView over InstanceTreeBuilder's forest with every area collapsed. Clicking an
|
||||
area row toggles it, clicking an instance row selects it and closes the panel.
|
||||
area row toggles it, activating an instance row selects it and closes the panel —
|
||||
and "activating" is one path for both input modes, because the tree runs in
|
||||
Selectable mode and the mouse click reaches it through TreeView's own content span.
|
||||
Typing filters the forest to the matching instances and their ancestors; Enter picks
|
||||
the first visible match, ArrowDown hands keyboard navigation to the tree and Escape
|
||||
closes without changing the selection.
|
||||
@@ -10,14 +12,13 @@
|
||||
an outside click, the same pattern TreeView uses for its context menu.
|
||||
*@
|
||||
@namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances
|
||||
|
||||
<div class="ip-wrap position-relative" data-test="@($"{DataTestPrefix}-picker")">
|
||||
@* ip-field lifts the field above the open panel's backdrop — see the .razor.css note. *@
|
||||
<div class="input-group input-group-sm ip-field">
|
||||
<input type="text" class="form-control" role="combobox" autocomplete="off"
|
||||
id="@_inputId" aria-autocomplete="list" aria-haspopup="tree"
|
||||
aria-expanded="@(_open ? "true" : "false")" aria-controls="@_panelId"
|
||||
id="@_inputId" aria-autocomplete="list" aria-haspopup="tree" aria-label="@AriaLabel"
|
||||
aria-expanded="@(_open ? "true" : "false")" aria-controls="@(_open ? _panelId : null)"
|
||||
placeholder="@Placeholder" disabled="@Disabled"
|
||||
data-test="@($"{DataTestPrefix}-input")"
|
||||
value="@_text" @onfocus="Open" @oninput="OnInput" @onkeydown="OnInputKeyDown" />
|
||||
@@ -30,14 +31,25 @@
|
||||
{
|
||||
<div class="ip-backdrop" data-test="@($"{DataTestPrefix}-backdrop")" @onclick="Close"></div>
|
||||
<div class="ip-panel border rounded bg-body shadow-sm" id="@_panelId" data-test="@($"{DataTestPrefix}-panel")">
|
||||
@* Selectable is what makes the tree's own Enter/Space activation reach us: it
|
||||
routes BOTH a click on TreeView's content span and a key press on the
|
||||
treeitem through SelectedKeyChanged, so mouse and keyboard share one path
|
||||
(the row label below therefore carries no @onclick of its own — a second
|
||||
handler there would fire twice on a click, since the click bubbles into the
|
||||
content span). SelectedKey is deliberately left unbound: a combobox shows
|
||||
its pick in the field, not as a row highlight, and binding it would also arm
|
||||
TreeView's "selected key no longer in the tree" reset — which fires on every
|
||||
render while the pick is filtered out, and this component would never clear
|
||||
it. SelectedCssClass is emptied to match; with no SelectedKey nothing is ever
|
||||
marked selected, so it is belt-and-braces. *@
|
||||
<TreeView @ref="_tree" TItem="InstanceTreeNode" Items="_roots"
|
||||
ChildrenSelector="n => n.Children" HasChildrenSelector="n => n.HasChildren"
|
||||
KeySelector="n => n.Key" IndentPx="16">
|
||||
KeySelector="n => n.Key" IndentPx="16"
|
||||
Selectable="true" SelectedCssClass="" SelectedKeyChanged="OnTreeKeyActivatedAsync">
|
||||
<NodeContent Context="node">
|
||||
<span class="tv-glyph"><i class="bi @(node.IsInstance ? "bi-box" : "bi-diagram-3")"></i></span>
|
||||
<span class="tv-label @(node.IsInstance ? "ip-instance" : "ip-area")" title="@node.Label"
|
||||
data-test="@($"{DataTestPrefix}-row")" data-instance-id="@(node.Instance?.Id)"
|
||||
@onclick="() => OnRowClickAsync(node)">@RowLabel(node)</span>
|
||||
data-test="@($"{DataTestPrefix}-row")" data-instance-id="@(node.Instance?.Id)">@RowLabel(node)</span>
|
||||
</NodeContent>
|
||||
<EmptyContent><span class="text-muted fst-italic small px-2">@(_hasFilter ? "No instances match." : "No instances at this site.")</span></EmptyContent>
|
||||
</TreeView>
|
||||
|
||||
Reference in New Issue
Block a user