Member Junction
    Preparing search index...

    <mj-left-nav> — Canonical left rail for MJ Explorer dashboards that have an internal section-nav (Admin shells, KH Config, KH Analytics, AI Analytics, Communication, Credentials, APIKeys, etc.).

    Replaces the bespoke .{name}-nav / .{name}-nav-item patterns each of those dashboards used to declare in their own CSS files.

    Supports:

    • Plain items (icon + label)
    • Items with a muted secondary description line (Admin shells pattern)
    • Items with a count badge (APIKeys pattern)
    • Sections with uppercase headers (Communication / Credentials pattern)
    • Optional [header] and [footer] content slots (KH Config logo header, etc.)
    • Responsive mobile drawer (≤700px): the rail hides off-canvas and a compact section switcher takes its place; tapping it slides the full rail in over a scrim. The rail keeps its full content (trees, slots, descriptions, badges) inside the drawer — no flattening. Set [MobileTitle] for the drawer label.
    sections: MJLeftNavSection[] = [
    {
    items: [
    { id: 'users', icon: 'fa-solid fa-users', label: 'Users', description: 'Manage user accounts' },
    { id: 'roles', icon: 'fa-solid fa-user-shield', label: 'Roles', description: 'Define roles and assignments' },
    ]
    }
    ];
    activeId = 'users';
    <mj-left-nav
    [Sections]="sections"
    [ActiveId]="activeId"
    (ItemClicked)="onItemClicked($event)">
    </mj-left-nav>

    Responsive — at ≤700px the rail becomes an off-canvas drawer (see the feature list above). The consumer should still switch the parent's flex-direction from row → column at the same breakpoint (e.g. <mj-page-body> Direction="row" with a @media override) so the in-flow section switcher stacks above the content pane rather than competing with it for horizontal space.

    Implements

    • OnDestroy
    Index

    Constructors

    Properties

    ActiveId: string | null = null

    The id of the currently active item. Items matching this id get the active styling and aria-current="page".

    Collapsed: boolean = false

    Whether the rail is currently collapsed to icons. Two-way bindable via [(Collapsed)].

    CollapsedChange: EventEmitter<boolean> = ...

    Emitted when the user clicks the collapse toggle. Consumer updates + persists the state.

    CollapsedWidth: number = 60

    Width of the collapsed icons-only strip. The default 60 is derived from the rail's own numbers — 8+8 container padding, 12+12 item padding, 18px icon, 1px border — so icons stay centred; retune it if those change.

    Collapsible: boolean = false

    Opt-in desktop collapse: renders a compact double-angle toggle chip at the top of the rail and, when Collapsed, presents the rail as an icons-only strip (CollapsedWidth wide) — labels visually hidden but kept in the a11y tree, section labels folded to divider lines, badges docked on the icon corner, and per-item tooltips/aria auto-enabled.

    The component owns the PRESENTATION only — the consumer owns the STATE (bind [(Collapsed)] and persist it however the app persists preferences), the same split as ExpandedIds. Desktop-only: ≤700px the rail is the off-canvas drawer and the toggle hides.

    Two things to know when the rail's content is richer than a flat icon list:

    • Tree sections fold to their top level while collapsed (see ShowTreeStructure); a top-level item stands in as active for an active descendant, and the tree returns intact on expand. Items with no icon render a monogram so they stay visible and hittable.
    • The [header] / [footer] slots are yours. The rail can't restyle projected content, so a wide header (logo lockup + wordmark, a filter box) will overflow a CollapsedWidth strip. Bind the same collapsed flag in the consumer and project a compact variant.

    Deliberately NOT included: hover-to-peek (auto-expand on pointer hover) — removed by ruling (2026-07-30): a rail that expands when the pointer grazes it moves the target the user was aiming at. Deliberate toggling beats clever hovering.

    ExpandedIds: string[] = []

    Ids of currently-expanded tree items. Items whose id appears here render with a down-chevron and their children are visible; items not in this list render with a right-chevron and their children stay collapsed. Consumer owns the state — toggle in response to (ItemToggled).

    Only applies to items where children !== undefined; flat items ignore it.

    IconOnly: boolean = false

    Set by a consumer that has visually collapsed the rail to an icons-only strip (via Width plus its own CSS hiding the labels). When true, each item gains a native title tooltip AND an aria-label carrying its full text.

    Why it is opt-in rather than always-on:

    • Tooltip: with labels visible, a tooltip that just repeats the label is redundant noise on every hover. Icon-only rails everywhere (VS Code's activity bar, Slack, Linear, Jira, Figma) show one; expanded rails do not.
    • aria-label: when the label IS visible it already names the button, and adding an identical aria-label is at best a no-op. When the consumer's CSS hides .mj-left-nav__text, the button's accessible name goes EMPTY — a screen reader announces "button" with no destination. That is the hole this closes.

    Leaving it false reproduces the previous markup exactly, so existing consumers are unaffected.

    ItemClicked: EventEmitter<MJLeftNavItem> = ...

    Emitted when a non-disabled item is clicked.

    ItemToggled: EventEmitter<MJLeftNavItem> = ...

    Emitted when a tree item's chevron is clicked. Consumer should toggle the item's id in / out of ExpandedIds. Does NOT also fire ItemClicked.

    MobileNavOpen: boolean = false

    Whether the mobile off-canvas drawer is open. Component-owned, transient UI state — no effect above the 700px breakpoint where the rail is always inline.

    MobileTitle: string = 'Menu'

    Label shown on the mobile drawer header and used as the switcher fallback label when no item is active. Defaults to 'Menu'. Set to something domain-specific (e.g. 'Sections', 'Test Suites') for clearer context.

    NavRegionId: string = ...

    DOM id of the <aside>, so the collapse toggle's aria-expanded can name the region it expands via aria-controls. Per-instance because a page may host more than one rail.

    Sections: MJLeftNavSection[] = []

    The sections + items that make up the rail. Sections render top-to-bottom in array order; items inside each section render in array order.

    Width: number = 240

    Width of the rail in pixels. Defaults to 240. Set to a smaller value (e.g., 200) for denser shells. Only applies to the desktop rail — on mobile the rail becomes a fixed-width off-canvas drawer.

    Accessors

    • get CollapseToggleMarginPx(): number

      Left margin that pins the toggle chip at the collapsed band's centre in BOTH modes, so the chip never relocates on toggle (the user never re-aims). Derived, not magic: (CollapsedWidth − 38px chip)/2 from the rail edge, minus the rail's 8px container padding. 38 = the 18px icon box + 2×10px chip padding.

      Returns number

    • get EffectiveIconOnly(): boolean

      Icon-only presentation is on when the consumer forces it OR the rail is natively collapsed.

      IconOnly is deliberately NOT viewport-gated: that flag means the CONSUMER's own CSS has narrowed the rail, and we can't know which breakpoints they gated it to. Withdrawing the accessible names on their behalf could leave a genuinely icons-only rail unnamed — the exact hole IconOnly exists to close. Native collapse is ours to gate; theirs is theirs.

      Returns boolean

    • get ShowTreeStructure(): boolean

      Whether tree structure (chevrons, depth indentation, expanded children) renders at all.

      An icons-only rail is ~60px wide and shows nothing but a centred glyph per row, which leaves a tree with no way to express itself: the chevron would eat a third of the width and shove the icon off-centre, each depth level would push its icon 14px further right until the rail clipped it, and a child's icon (optional, and usually absent on tree leaves) would say nothing about which branch it belongs to. So while icons-only, each section renders its TOP-LEVEL items only — the same choice every icon rail makes (VS Code's activity bar shows one icon per container and defers the tree to the expanded pane).

      Nothing is lost on the way back: ExpandedIds is consumer-owned and untouched here, so the tree returns exactly as the user left it the moment the rail expands.

      Returns boolean

    Methods

    • Whether any top-level item in this section actually has children to expand. The template calls this once per section; the result is threaded through the recursive itemTpl so leaf nodes only reserve a chevron-column placeholder when a sibling will actually render a chevron.

      Top-level check is sufficient — if a deeply-nested item has children, its parent's children.length > 0 already qualifies the section as expandable.

      Parameters

      Returns boolean