feat(central-ui): InstancePicker search filter + keyboard (Enter/Escape/ArrowDown)

Typing in the field is the filter: the forest is rebuilt to the matching
instances plus their ancestor areas and force-expanded so every match is on
screen; clearing the text restores the full forest, collapsed. Both bulk
expansions are deferred to OnAfterRenderAsync, because the TreeView only exists
once the panel has rendered.

Enter picks the first instance the operator can actually SEE — document order,
descending into a branch only when the tree reports it expanded — so with areas
collapsed it picks the first root instance, and with a filter (every branch
expanded) it degrades to plain document order. Escape closes and restores the
field text from the current selection; ArrowDown hands keyboard navigation to
the tree via a new additive TreeView.FocusFirstVisibleAsync(), which reuses the
existing roving-tabindex _focusNeedsApply path rather than adding a second
focus mechanism.

No preventDefault: Blazor's flag is all-or-nothing per element, and neither
default is harmful (Enter in a text input outside a form does nothing,
ArrowDown just moves the caret) while suppressing Tab would trap the control.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
Joseph Doherty
2026-09-08 09:37:44 -04:00
parent 05e4f3ca0f
commit 621d6bfd94
4 changed files with 309 additions and 2 deletions
@@ -3,6 +3,9 @@
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.
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.
*@
@@ -16,7 +19,7 @@
aria-expanded="@(_open ? "true" : "false")" aria-controls="@_panelId"
placeholder="@Placeholder" disabled="@Disabled"
data-test="@($"{DataTestPrefix}-input")"
value="@_text" @onfocus="Open" />
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>