# TreeView Component ## Purpose A reusable, generic Blazor Server component that renders hierarchical data as an expandable/collapsible tree. The component is data-agnostic — it accepts any tree-shaped data via type parameters and render fragments, following the same pattern as the existing `DataTable` shared component. ## Location `src/ScadaLink.CentralUI/Components/Shared/TreeView.razor` ## Primary Use Case: Instance Hierarchy The motivating use case is displaying instances organized by site and area: ``` - Site A + Area 1 - Sub Area 1 Instance 1 Instance 2 + Area 2 + Site B + Site C ``` **Hierarchy**: Site → Area → Sub Area (recursive) → Instance (leaf) Nodes at each level may be expandable (branches) or plain items (leaves). Leaf nodes have no expand/collapse toggle. ## Requirements ### R1 — Generic Type Parameter The component accepts a single type parameter `TItem` representing any node in the tree. The consumer provides: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `Items` | `IReadOnlyList` | Yes | Root-level items | | `ChildrenSelector` | `Func>` | Yes | Returns children for a given node | | `HasChildrenSelector` | `Func` | Yes | Whether the node can be expanded (branch vs. leaf) | | `KeySelector` | `Func` | Yes | Unique key per node (for state tracking) | ### R2 — Render Fragments | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `NodeContent` | `RenderFragment` | Yes | Renders the label/content for each node | | `EmptyContent` | `RenderFragment?` | No | Shown when `Items` is empty | The `NodeContent` fragment receives the `TItem` and is responsible for rendering the node's display (text, icons, badges, action buttons, etc.). The tree component only renders the structural chrome (indentation, expand/collapse toggle, vertical guide lines). ### R3 — Expand/Collapse Behavior - Each branch node displays a toggle indicator: `+` when collapsed, `−` when expanded. - Clicking the **toggle icon** expands/collapses the node. Clicking the **content area** does **not** toggle expansion (it is reserved for selection — see R5). - Leaf nodes (where `HasChildrenSelector` returns `false`) display no toggle — they are indented inline with sibling branch nodes. - Expand/collapse state is tracked internally by the component using `KeySelector` for identity. - All nodes start collapsed by default unless `InitiallyExpanded` is set. - **Session persistence**: When the user navigates away and returns, previously expanded nodes are restored (see R11). | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `InitiallyExpanded` | `Func?` | No | Predicate — nodes matching this start expanded (first load only, before any persisted state exists) | ### R4 — Indentation and Visual Structure The component renders the structural chrome: indent gutters per depth, the toggle slot, and ancestor guide lines. Leaf nodes render an empty toggle placeholder so labels align across siblings. The exact tokens (indent unit, toggle glyph, guide-line treatment) are specified in **V2** of the Visual Design Guide. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `IndentPx` | `int` | No | Pixels per indent level. Default: 24 | | `ShowGuideLines` | `bool` | No | Show vertical connector lines. Default: true | ### R5 — Selection | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `Selectable` | `bool` | No | Enable click-to-select. Default: false | | `SelectedKey` | `object?` | No | Currently selected node key (two-way binding) | | `SelectedKeyChanged` | `EventCallback` | No | Fires when selection changes | | `SelectedCssClass` | `string` | No | CSS class for selected node. Default: `"bg-primary bg-opacity-10"` | When `Selectable` is true, clicking a node row selects it (highlighted). Clicking the expand/collapse toggle does **not** change selection — only clicking the content area does. ### R6 — Lazy Loading (Deferred) Future enhancement. For now, all children are provided synchronously via `ChildrenSelector`. A future version may support `Func>>` for on-demand loading with a spinner placeholder. ### R7 — Keyboard Navigation (Deferred) Future enhancement. Arrow keys for navigation, Enter/Space for expand/collapse, Home/End for first/last. ### R8 — External Filtering The tree component itself does **not** implement filter UI. Filtering is driven externally by the consuming page — for example, a site dropdown that filters the tree to show only the selected site's hierarchy. **How it works:** - The consumer filters `Items` (and/or adjusts `ChildrenSelector` results) and passes the filtered list to the component. - When `Items` changes (Blazor re-render), the component re-renders the tree with the new data. - **Expansion state is preserved across filter changes.** Nodes that were expanded before filtering remain expanded if they reappear after the filter changes. The component tracks expanded keys independently of the current `Items` — keys are never purged when items disappear, so re-adding a previously expanded node restores its expanded state. - Selection is cleared if the selected node is no longer present after filtering. **Example — site filter on the instances page:** ```razor ... @code { private int? _selectedSiteId; private List GetFilteredRoots() { if (_selectedSiteId == null) return _allRoots; return _allRoots.Where(r => r.SiteId == _selectedSiteId).ToList(); } } ``` This keeps filter logic in the page (domain-specific) while the component handles rendering whatever it receives. ### R9 — Styling - Uses Bootstrap 5 utility classes and CSS variables. No third-party Blazor component frameworks. - Adds one icon-library dependency: **Bootstrap Icons** (static files at `wwwroot/lib/bootstrap-icons/`). Distribution rules in **V4** of the Visual Design Guide. - Hardcoded colors are forbidden; use Bootstrap utility classes (`bg-primary bg-opacity-10`, `text-muted`) or CSS variables (`var(--bs-tertiary-bg)`, `var(--bs-border-color)`). - Component-local CSS lives in `TreeView.razor.css` (Blazor CSS isolation). - All visual tokens (row density, indent, state visuals, glyphs, labels, badges) are specified in the **Visual Design Guide** (V1–V7). This requirement is non-normative summary; the Guide is authoritative. ### R10 — No Internal Scrolling The tree renders inline in the page flow. The consuming page is responsible for placing it in a scrollable container if needed (e.g., `overflow-auto` with `max-height`). ### R11 — Session-Persistent Expansion State When a user expands nodes, navigates away (e.g., clicks an instance link to the configure page), and returns to the page, the tree must restore the same expansion state. **Mechanism:** - The component requires a `StorageKey` parameter — a unique string identifying this tree instance (e.g., `"instances-tree"`, `"data-connections-tree"`). - Expanded node keys are stored in browser `sessionStorage` under the key `treeview:{StorageKey}`. - On mount (`OnAfterRenderAsync` first render), the component reads `sessionStorage` and expands any nodes whose keys are present. This takes precedence over `InitiallyExpanded`. - On every expand/collapse toggle, the component writes the updated set of expanded keys to `sessionStorage`. - `sessionStorage` is scoped to the browser tab — each tab has independent state. State is cleared when the tab is closed. **Implementation note:** Blazor Server requires `IJSRuntime` to access `sessionStorage`. The component injects `IJSRuntime` and uses a small JS interop helper (inline or in a shared `.js` file) for `getItem`/`setItem`. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `StorageKey` | `string?` | No | Key for sessionStorage persistence. If null, expansion state is not persisted (in-memory only). | ### R12 — Expand All / Collapse All The component exposes methods that the consumer can call via `@ref`: ```csharp /// Expands all branch nodes in the tree (recursive). public void ExpandAll(); /// Collapses all branch nodes in the tree. public void CollapseAll(); ``` **Usage:** ```razor @code { private TreeView _tree = default!; } ``` Both methods update sessionStorage if `StorageKey` is set. `ExpandAll` requires walking the full tree via `ChildrenSelector` to collect all branch node keys. ### R13 — Programmatic Expand-to-Node The component exposes a method to reveal a specific node by expanding all of its ancestors: ```csharp /// Expands all ancestor nodes so that the node with the given key becomes visible. /// Optionally selects the node and scrolls it into view. public void RevealNode(object key, bool select = false); ``` This requires the component to build a parent lookup (key → parent key) from the tree data. When called: 1. Walk from the target node's key up to the root, collecting ancestor keys. 2. Expand all ancestors. 3. If `select` is true, set the node as selected and fire `SelectedKeyChanged`. 4. After rendering, scroll the node element into view via JS interop (`element.scrollIntoView({ block: 'nearest' })`). **Use case:** Search box on the instances page — user types "Motor-1", results list shows matching instances. Clicking a result calls `_tree.RevealNode(instanceKey, select: true)` to expand the Site → Area path and highlight the instance. ### R14 — Accessibility (ARIA) The component renders semantic ARIA attributes for screen reader support: - The root `
    ` has `role="tree"`. - Each node `
  • ` has `role="treeitem"`. - Branch nodes have `aria-expanded="true"` or `aria-expanded="false"`. - Child `
      ` containers have `role="group"`. - When `Selectable` is true, the selected node has `aria-selected="true"`. - Each node row has a unique `id` derived from `KeySelector` for anchor targeting. This is baseline accessibility — no keyboard navigation yet (deferred in R7), but screen readers can understand the tree structure. ### R15 — Context Menu The component supports an optional right-click context menu on nodes, defined by the consumer via a render fragment. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `ContextMenu` | `RenderFragment?` | No | Menu content rendered when a node is right-clicked. Receives the right-clicked `TItem`. | **Behavior:** - Right-clicking a node renders the `ContextMenu` fragment for that node. The component checks whether the fragment produces any content — **if the fragment renders nothing (empty markup), no menu is shown and the browser default context menu is used.** This is how per-node-type menus work: the consumer uses `@if` blocks in the fragment, and nodes that don't match any condition simply produce no output. - When content is produced, the browser's default context menu is suppressed (`@oncontextmenu:preventDefault`) and a floating menu is shown at the cursor. - The menu is rendered as a Bootstrap dropdown: `