Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/InstancePicker.razor
T
Joseph Doherty a33a2f4456 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
2026-09-08 10:00:17 -04:00

59 lines
4.1 KiB
Plaintext

@*
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, 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.
Bootstrap only, no JavaScript: the transparent fixed backdrop closes the panel on
an outside click, the same pattern TreeView uses for its context menu.
*@
@namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
<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-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" />
<button type="button" class="btn btn-outline-secondary" tabindex="-1" aria-label="Browse instances"
disabled="@Disabled" data-test="@($"{DataTestPrefix}-toggle")" @onclick="Toggle">
<i class="bi @(_open ? "bi-chevron-up" : "bi-chevron-down")"></i>
</button>
</div>
@if (_open)
{
<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"
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)">@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>
</div>
}
</div>