@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.59 → 2.0.0-next.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/openbridge-webcomponents.bundle.js +5557 -4305
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +1204 -25
- package/dist/components/navigation-item/navigation-item.css.js +5 -0
- package/dist/components/navigation-item/navigation-item.css.js.map +1 -1
- package/dist/components/navigation-item/navigation-item.d.ts +18 -0
- package/dist/components/navigation-item/navigation-item.d.ts.map +1 -1
- package/dist/components/navigation-item/navigation-item.js +49 -1
- package/dist/components/navigation-item/navigation-item.js.map +1 -1
- package/dist/components/navigation-item-group/navigation-item-group.css.js +8 -0
- package/dist/components/navigation-item-group/navigation-item-group.css.js.map +1 -1
- package/dist/components/navigation-item-group/navigation-item-group.d.ts +22 -0
- package/dist/components/navigation-item-group/navigation-item-group.d.ts.map +1 -1
- package/dist/components/navigation-item-group/navigation-item-group.js +63 -1
- package/dist/components/navigation-item-group/navigation-item-group.js.map +1 -1
- package/dist/components/navigation-menu/navigation-menu.d.ts +29 -1
- package/dist/components/navigation-menu/navigation-menu.d.ts.map +1 -1
- package/dist/components/navigation-menu/navigation-menu.js +105 -0
- package/dist/components/navigation-menu/navigation-menu.js.map +1 -1
- package/dist/components/tree-navigation/tree-navigation.css.js +18 -0
- package/dist/components/tree-navigation/tree-navigation.css.js.map +1 -0
- package/dist/components/tree-navigation/tree-navigation.d.ts +74 -0
- package/dist/components/tree-navigation/tree-navigation.d.ts.map +1 -0
- package/dist/components/tree-navigation/tree-navigation.js +120 -0
- package/dist/components/tree-navigation/tree-navigation.js.map +1 -0
- package/dist/components/tree-navigation-group/tree-navigation-group.css.js +22 -0
- package/dist/components/tree-navigation-group/tree-navigation-group.css.js.map +1 -0
- package/dist/components/tree-navigation-group/tree-navigation-group.d.ts +94 -0
- package/dist/components/tree-navigation-group/tree-navigation-group.d.ts.map +1 -0
- package/dist/components/tree-navigation-group/tree-navigation-group.js +116 -0
- package/dist/components/tree-navigation-group/tree-navigation-group.js.map +1 -0
- package/dist/components/tree-navigation-item/tree-navigation-item.css.js +429 -0
- package/dist/components/tree-navigation-item/tree-navigation-item.css.js.map +1 -0
- package/dist/components/tree-navigation-item/tree-navigation-item.d.ts +160 -0
- package/dist/components/tree-navigation-item/tree-navigation-item.d.ts.map +1 -0
- package/dist/components/tree-navigation-item/tree-navigation-item.js +208 -0
- package/dist/components/tree-navigation-item/tree-navigation-item.js.map +1 -0
- package/dist/internal/tree-roving-navigator.d.ts +71 -0
- package/dist/internal/tree-roving-navigator.d.ts.map +1 -0
- package/dist/internal/tree-roving-navigator.js +172 -0
- package/dist/internal/tree-roving-navigator.js.map +1 -0
- package/package.json +1 -1
- package/src/components/navigation-item/navigation-item.css +5 -0
- package/src/components/navigation-item/navigation-item.ts +56 -1
- package/src/components/navigation-item-group/navigation-item-group.css +8 -0
- package/src/components/navigation-item-group/navigation-item-group.ts +71 -1
- package/src/components/navigation-menu/navigation-menu.stories.ts +54 -0
- package/src/components/navigation-menu/navigation-menu.ts +151 -0
- package/src/components/tree-navigation/tree-navigation.css +7 -0
- package/src/components/tree-navigation/tree-navigation.stories.ts +162 -0
- package/src/components/tree-navigation/tree-navigation.ts +188 -0
- package/src/components/tree-navigation-group/tree-navigation-group.css +11 -0
- package/src/components/tree-navigation-group/tree-navigation-group.ts +157 -0
- package/src/components/tree-navigation-item/tree-navigation-item.css +258 -0
- package/src/components/tree-navigation-item/tree-navigation-item.stories.ts +119 -0
- package/src/components/tree-navigation-item/tree-navigation-item.ts +307 -0
- package/src/internal/tree-roving-navigator.ts +231 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree-roving-navigator.js","sources":["../../src/internal/tree-roving-navigator.ts"],"sourcesContent":["import {ObcTreeNavigationItem} from '../components/tree-navigation-item/tree-navigation-item.js';\n\n/**\n * Host-specific row resolution. The two tree hosts differ only in row tags,\n * expand-state storage, and DOM shape, so injecting these hooks lets one\n * navigator drive both.\n */\nexport interface TreeRovingAdapter<Row extends HTMLElement = HTMLElement> {\n /** Top-level rows, in document order. */\n getRows(): Row[];\n /** Direct child rows of a row, in document order. */\n childRows(row: Row): Row[];\n isGroup(row: Row): boolean;\n isExpanded(row: Row): boolean;\n setExpanded(row: Row, expanded: boolean): void;\n /** The focusable `obc-tree-navigation-item` for a row (a leaf is itself). */\n innerItem(row: Row): ObcTreeNavigationItem | null;\n /** Defaults to false when omitted. */\n isDisabled?(row: Row): boolean;\n}\n\n/**\n * Implements the [WAI-ARIA Tree View](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/)\n * keyboard pattern for a tree host: a single tab stop (roving tabindex) plus\n * Up/Down, Left/Right (collapse/expand → parent/child), and Home/End. Type-ahead\n * and `*` are intentionally out of scope.\n *\n * The host owns the DOM and the event wiring; this class owns the navigation\n * logic. The host calls {@link handleKeydown} from its `keydown` listener and\n * {@link refresh} whenever the structure changes, and supplies a\n * {@link TreeRovingAdapter} for row resolution. Focus only moves on user keys;\n * `refresh` re-points the roving tabindex without stealing focus.\n */\nexport class TreeRovingNavigator<Row extends HTMLElement = HTMLElement> {\n private readonly adapter: TreeRovingAdapter<Row>;\n private readonly host: HTMLElement;\n\n /** The row that currently holds the single tab stop (roving tabindex). */\n private activeRow?: Row;\n\n constructor(host: HTMLElement, adapter: TreeRovingAdapter<Row>) {\n this.adapter = adapter;\n this.host = host;\n }\n\n private isDisabled(row: Row): boolean {\n return this.adapter.isDisabled?.(row) ?? false;\n }\n\n /**\n * Depth-first list of every visible row: descend into a group only when it is\n * expanded. Disabled rows are included so {@link refresh} can re-point to a\n * visible ancestor; arrow navigation filters them out via {@link navigableRows}.\n */\n private visibleRows(): Row[] {\n const out: Row[] = [];\n const walk = (rows: Row[]) => {\n for (const row of rows) {\n out.push(row);\n if (this.adapter.isGroup(row) && this.adapter.isExpanded(row)) {\n walk(this.adapter.childRows(row));\n }\n }\n };\n walk(this.adapter.getRows());\n return out;\n }\n\n /** Visible rows that arrow navigation can land on (enabled rows). */\n private navigableRows(): Row[] {\n return this.visibleRows().filter((row) => !this.isDisabled(row));\n }\n\n /** The parent row of a row within this tree, or undefined for a top-level row. */\n private parentRow(target: Row): Row | undefined {\n let found: Row | undefined;\n const walk = (rows: Row[], parent?: Row) => {\n for (const row of rows) {\n if (row === target) {\n found = parent;\n return;\n }\n if (this.adapter.isGroup(row)) {\n walk(this.adapter.childRows(row), row);\n if (found !== undefined) return;\n }\n }\n };\n walk(this.adapter.getRows(), undefined);\n return found;\n }\n\n /**\n * Rebuild the roving tabindex: exactly one navigable row is `focusable`. Keeps\n * the active row if still navigable; else falls back to its nearest navigable\n * ancestor, else the first navigable row. Never moves focus.\n */\n refresh(): void {\n const navigable = this.navigableRows();\n if (navigable.length === 0) {\n this.activeRow = undefined;\n return;\n }\n\n let next: Row | undefined;\n if (this.activeRow && navigable.includes(this.activeRow)) {\n next = this.activeRow;\n } else if (this.activeRow) {\n let ancestor = this.parentRow(this.activeRow);\n while (ancestor && !navigable.includes(ancestor)) {\n ancestor = this.parentRow(ancestor);\n }\n next = ancestor ?? navigable[0];\n } else {\n next = navigable[0];\n }\n\n this.setActiveRow(next, false);\n }\n\n private setActiveRow(row: Row, moveFocus: boolean): void {\n this.activeRow = row;\n for (const candidate of this.visibleRows()) {\n const item = this.adapter.innerItem(candidate);\n if (item) item.focusable = candidate === row;\n }\n if (!moveFocus) return;\n // A row's header may render asynchronously (e.g. right after expanding a\n // group), so fall back to a microtask if it isn't present yet.\n const item = this.adapter.innerItem(row);\n if (item) item.focus();\n else queueMicrotask(() => this.adapter.innerItem(row)?.focus());\n }\n\n /**\n * The originating tree row for a keydown. A group's header lives in the\n * group's own shadow root, so the host-root check resolves a header to its\n * group rather than the inner header item.\n */\n private rowFromEvent(event: KeyboardEvent): Row | undefined {\n // Read the root live: the navigator is constructed before the host connects.\n const root = this.host.getRootNode();\n for (const target of event.composedPath()) {\n if (\n target instanceof HTMLElement &&\n target.getRootNode() === root &&\n this.isRow(target)\n ) {\n return target as Row;\n }\n }\n return undefined;\n }\n\n /** Whether an element is one of this tree's rows (group or leaf). */\n private isRow(el: HTMLElement): boolean {\n const tops = this.adapter.getRows();\n const stack = [...tops];\n while (stack.length) {\n const row = stack.pop()!;\n if (row === el) return true;\n if (this.adapter.isGroup(row)) stack.push(...this.adapter.childRows(row));\n }\n return false;\n }\n\n /** Handle a `keydown`; returns true if the key was consumed. */\n handleKeydown(event: KeyboardEvent): boolean {\n const row = this.rowFromEvent(event);\n if (!row) return false;\n\n switch (event.key) {\n case 'ArrowDown':\n this.moveByOffset(row, 1);\n return true;\n case 'ArrowUp':\n this.moveByOffset(row, -1);\n return true;\n case 'ArrowRight':\n this.onArrowRight(row);\n return true;\n case 'ArrowLeft':\n this.onArrowLeft(row);\n return true;\n case 'Home':\n this.moveToEdge(true);\n return true;\n case 'End':\n this.moveToEdge(false);\n return true;\n default:\n return false;\n }\n }\n\n private moveByOffset(row: Row, offset: number): void {\n const rows = this.navigableRows();\n const index = rows.indexOf(row);\n if (index === -1) return;\n const target = rows[index + offset];\n if (target) this.setActiveRow(target, true);\n }\n\n private moveToEdge(first: boolean): void {\n const rows = this.navigableRows();\n if (rows.length === 0) return;\n this.setActiveRow(first ? rows[0] : rows[rows.length - 1], true);\n }\n\n private onArrowRight(row: Row): void {\n if (!this.adapter.isGroup(row)) return;\n if (!this.adapter.isExpanded(row)) {\n this.adapter.setExpanded(row, true);\n // Re-point the roving set for hosts that don't observe expansion themselves.\n this.refresh();\n return;\n }\n const child = this.adapter.childRows(row).find((c) => !this.isDisabled(c));\n if (child) this.setActiveRow(child, true);\n }\n\n private onArrowLeft(row: Row): void {\n if (this.adapter.isGroup(row) && this.adapter.isExpanded(row)) {\n this.adapter.setExpanded(row, false);\n this.refresh();\n return;\n }\n const parent = this.parentRow(row);\n if (parent) this.setActiveRow(parent, true);\n }\n}\n"],"names":["item"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -6,6 +6,12 @@ import {ifDefined} from 'lit/directives/if-defined.js';
|
|
|
6
6
|
import '../../icons/icon-arrow-flyout-google.js';
|
|
7
7
|
import {ObcNavigationMenuVariant} from '../navigation-menu/navigation-menu.js';
|
|
8
8
|
import {customElement} from '../../decorator.js';
|
|
9
|
+
import '../tree-navigation-item/tree-navigation-item.js';
|
|
10
|
+
import {
|
|
11
|
+
TreeBranchType,
|
|
12
|
+
TreeTerminalType,
|
|
13
|
+
} from '../tree-navigation-item/tree-navigation-item.js';
|
|
14
|
+
import {BadgeType} from '../badge/badge.js';
|
|
9
15
|
|
|
10
16
|
enum NavigationItemRole {
|
|
11
17
|
Button = 'button',
|
|
@@ -142,8 +148,32 @@ export class ObcNavigationItem extends LitElement {
|
|
|
142
148
|
|
|
143
149
|
@property({type: Boolean}) hasTrailingIcon = false;
|
|
144
150
|
|
|
151
|
+
/** Set by `obc-navigation-menu` in its Tree variant — renders the row as a tree item. */
|
|
152
|
+
@property({type: Boolean}) treeMode = false;
|
|
153
|
+
|
|
154
|
+
/** Indentation columns for tree mode, assigned by `obc-navigation-menu`. */
|
|
155
|
+
@property({type: Array}) treeBranches: TreeBranchType[] = [];
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Terminal type for the row in the Tree variant — one of `regular` (default),
|
|
159
|
+
* `aggregated-header`, or `group-header`. Has no effect in the flat variants.
|
|
160
|
+
*/
|
|
161
|
+
@property({type: String}) terminalType: string = TreeTerminalType.regular;
|
|
162
|
+
|
|
163
|
+
/** Whether a trailing alert counter badge is shown (Tree variant only). */
|
|
164
|
+
@property({type: Boolean}) hasAlertBadge = false;
|
|
165
|
+
|
|
166
|
+
/** The number shown in the alert badge when `hasAlertBadge` is true (Tree variant only). */
|
|
167
|
+
@property({type: Number}) alertCount = 0;
|
|
168
|
+
|
|
169
|
+
/** The severity/type of the alert badge — one of the `obc-badge` types (Tree variant only). */
|
|
170
|
+
@property({type: String}) alertType: string = BadgeType.alarm;
|
|
171
|
+
|
|
145
172
|
@query('a') private anchorElement?: HTMLAnchorElement;
|
|
146
173
|
|
|
174
|
+
// In tree mode the row renders an `obc-tree-navigation-item` instead of an `<a>`.
|
|
175
|
+
@query('obc-tree-navigation-item') private treeItemElement?: HTMLElement;
|
|
176
|
+
|
|
147
177
|
/**
|
|
148
178
|
* Fired when the navigation item is clicked (either as a link or button).
|
|
149
179
|
* @fires click {CustomEvent<void>}
|
|
@@ -165,7 +195,7 @@ export class ObcNavigationItem extends LitElement {
|
|
|
165
195
|
}
|
|
166
196
|
|
|
167
197
|
public override focus(options?: FocusOptions): void {
|
|
168
|
-
this.anchorElement?.focus(options);
|
|
198
|
+
(this.treeItemElement ?? this.anchorElement)?.focus(options);
|
|
169
199
|
}
|
|
170
200
|
|
|
171
201
|
private getItemRole(): NavigationItemRole | undefined {
|
|
@@ -188,6 +218,31 @@ export class ObcNavigationItem extends LitElement {
|
|
|
188
218
|
}
|
|
189
219
|
|
|
190
220
|
override render() {
|
|
221
|
+
if (this.treeMode) {
|
|
222
|
+
// Delegate the whole row to the tree item: it owns focus, keyboard
|
|
223
|
+
// activation, the checked-inert behavior, and href navigation. Forward its
|
|
224
|
+
// activation as this item's own `click` so selection wiring is unchanged.
|
|
225
|
+
return html`
|
|
226
|
+
<obc-tree-navigation-item
|
|
227
|
+
class="tree"
|
|
228
|
+
.label=${this.label}
|
|
229
|
+
.branches=${this.treeBranches}
|
|
230
|
+
?checked=${this.checked}
|
|
231
|
+
.hasLeadingIcon=${this.hasIcon}
|
|
232
|
+
.href=${this.href}
|
|
233
|
+
.terminalType=${this.terminalType}
|
|
234
|
+
?hasAlertBadge=${this.hasAlertBadge}
|
|
235
|
+
.alertCount=${this.alertCount}
|
|
236
|
+
.alertType=${this.alertType}
|
|
237
|
+
@click=${this.onClick}
|
|
238
|
+
>
|
|
239
|
+
${this.hasIcon
|
|
240
|
+
? html`<slot name="icon" slot="icon"></slot>`
|
|
241
|
+
: nothing}
|
|
242
|
+
</obc-tree-navigation-item>
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
|
|
191
246
|
const showFlyout =
|
|
192
247
|
this.group && this.variant !== ObcNavigationMenuVariant.IconOnly;
|
|
193
248
|
const isCompact = this.variant === ObcNavigationMenuVariant.Compact;
|
|
@@ -4,6 +4,12 @@ import compentStyle from './navigation-item-group.css?inline';
|
|
|
4
4
|
import {ObcNavigationMenuVariant} from '../navigation-menu/navigation-menu.js';
|
|
5
5
|
import {classMap} from 'lit/directives/class-map.js';
|
|
6
6
|
import {customElement} from '../../decorator.js';
|
|
7
|
+
import '../tree-navigation-item/tree-navigation-item.js';
|
|
8
|
+
import {
|
|
9
|
+
TreeBranchType,
|
|
10
|
+
TreeTerminalType,
|
|
11
|
+
} from '../tree-navigation-item/tree-navigation-item.js';
|
|
12
|
+
import {BadgeType} from '../badge/badge.js';
|
|
7
13
|
|
|
8
14
|
/**
|
|
9
15
|
* `<obc-navigation-item-group>` – A collapsible navigation group component for organizing related navigation items under a single expandable label.
|
|
@@ -90,9 +96,47 @@ export class ObcNavigationItemGroup extends LitElement {
|
|
|
90
96
|
|
|
91
97
|
@property({type: Boolean}) hasIcon = false;
|
|
92
98
|
|
|
99
|
+
/** Set by `obc-navigation-menu` in its Tree variant — renders the group as a tree row. */
|
|
100
|
+
@property({type: Boolean}) treeMode = false;
|
|
101
|
+
|
|
102
|
+
/** Indentation columns for tree mode, assigned by `obc-navigation-menu`. */
|
|
103
|
+
@property({type: Array}) treeBranches: TreeBranchType[] = [];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Terminal type for the group header in the Tree variant — one of `regular`
|
|
107
|
+
* (default), `aggregated-header`, or `group-header`. No effect in flat variants.
|
|
108
|
+
*/
|
|
109
|
+
@property({type: String}) terminalType: string = TreeTerminalType.regular;
|
|
110
|
+
|
|
111
|
+
/** Whether a trailing alert counter badge is shown on the header (Tree variant only). */
|
|
112
|
+
@property({type: Boolean}) hasAlertBadge = false;
|
|
113
|
+
|
|
114
|
+
/** The number shown in the header's alert badge when `hasAlertBadge` is true (Tree variant only). */
|
|
115
|
+
@property({type: Number}) alertCount = 0;
|
|
116
|
+
|
|
117
|
+
/** The severity/type of the header's alert badge — one of the `obc-badge` types (Tree variant only). */
|
|
118
|
+
@property({type: String}) alertType: string = BadgeType.alarm;
|
|
119
|
+
|
|
120
|
+
/** Whether the group starts expanded. Useful for trees that open by default. */
|
|
121
|
+
@property({type: Boolean}) defaultOpen = false;
|
|
122
|
+
|
|
93
123
|
@state() private openContainer = false;
|
|
94
124
|
|
|
95
|
-
|
|
125
|
+
// Flat mode renders an `obc-navigation-item` header; tree mode renders an
|
|
126
|
+
// `obc-tree-navigation-item`. Match whichever is present so `focus()` works in both.
|
|
127
|
+
@query('obc-navigation-item, obc-tree-navigation-item')
|
|
128
|
+
private groupItem?: HTMLElement;
|
|
129
|
+
|
|
130
|
+
override firstUpdated() {
|
|
131
|
+
if (this.defaultOpen) {
|
|
132
|
+
this.openContainer = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Whether the group is currently open (its children are disclosed). */
|
|
137
|
+
public get expanded(): boolean {
|
|
138
|
+
return this.openContainer;
|
|
139
|
+
}
|
|
96
140
|
|
|
97
141
|
private onClickGroup() {
|
|
98
142
|
if (this.openContainer) {
|
|
@@ -123,6 +167,32 @@ export class ObcNavigationItemGroup extends LitElement {
|
|
|
123
167
|
}
|
|
124
168
|
|
|
125
169
|
override render() {
|
|
170
|
+
if (this.treeMode) {
|
|
171
|
+
return html`
|
|
172
|
+
<obc-tree-navigation-item
|
|
173
|
+
part="header"
|
|
174
|
+
.label=${this.label}
|
|
175
|
+
.branches=${this.treeBranches}
|
|
176
|
+
expandable
|
|
177
|
+
?expanded=${this.openContainer}
|
|
178
|
+
?checked=${this.checked}
|
|
179
|
+
.hasLeadingIcon=${this.hasIcon}
|
|
180
|
+
.terminalType=${this.terminalType}
|
|
181
|
+
?hasAlertBadge=${this.hasAlertBadge}
|
|
182
|
+
.alertCount=${this.alertCount}
|
|
183
|
+
.alertType=${this.alertType}
|
|
184
|
+
@expand-toggle=${this.onClickGroup}
|
|
185
|
+
>
|
|
186
|
+
${this.hasIcon
|
|
187
|
+
? html`<slot name="icon" slot="icon"></slot>`
|
|
188
|
+
: nothing}
|
|
189
|
+
</obc-tree-navigation-item>
|
|
190
|
+
<div part="children" role="group" ?hidden=${!this.openContainer}>
|
|
191
|
+
<slot></slot>
|
|
192
|
+
</div>
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
|
|
126
196
|
return html`
|
|
127
197
|
<obc-navigation-item
|
|
128
198
|
@click=${this.onClickGroup}
|
|
@@ -252,3 +252,57 @@ export const TestDynamicElementsInSpan: Story = {
|
|
|
252
252
|
await expect(newItem.variant).toBe(ObcNavigationMenuVariant.Compact);
|
|
253
253
|
},
|
|
254
254
|
};
|
|
255
|
+
|
|
256
|
+
export const Tree: Story = {
|
|
257
|
+
args: {
|
|
258
|
+
variant: ObcNavigationMenuVariant.Tree,
|
|
259
|
+
},
|
|
260
|
+
render: (args) => {
|
|
261
|
+
return html`
|
|
262
|
+
<obc-navigation-menu
|
|
263
|
+
.variant=${args.variant}
|
|
264
|
+
style="position: fixed; top: 0; bottom: 0; left: 0;"
|
|
265
|
+
>
|
|
266
|
+
<obc-navigation-item-group
|
|
267
|
+
slot="main"
|
|
268
|
+
label="Vessel"
|
|
269
|
+
hasIcon
|
|
270
|
+
defaultOpen
|
|
271
|
+
>
|
|
272
|
+
<obi-placeholder slot="icon"></obi-placeholder>
|
|
273
|
+
<obc-navigation-item-group
|
|
274
|
+
label="Engine room"
|
|
275
|
+
hasIcon
|
|
276
|
+
defaultOpen
|
|
277
|
+
terminalType="aggregated-header"
|
|
278
|
+
>
|
|
279
|
+
<obi-placeholder slot="icon"></obi-placeholder>
|
|
280
|
+
<obc-navigation-item label="Main engine" hasIcon checked>
|
|
281
|
+
<obi-placeholder slot="icon"></obi-placeholder>
|
|
282
|
+
</obc-navigation-item>
|
|
283
|
+
<obc-navigation-item
|
|
284
|
+
label="Cooling system"
|
|
285
|
+
hasIcon
|
|
286
|
+
hasAlertBadge
|
|
287
|
+
.alertCount=${3}
|
|
288
|
+
>
|
|
289
|
+
<obi-placeholder slot="icon"></obi-placeholder>
|
|
290
|
+
</obc-navigation-item>
|
|
291
|
+
</obc-navigation-item-group>
|
|
292
|
+
<obc-navigation-item-group label="Bridge" hasIcon>
|
|
293
|
+
<obi-placeholder slot="icon"></obi-placeholder>
|
|
294
|
+
<obc-navigation-item label="Radar" hasIcon>
|
|
295
|
+
<obi-placeholder slot="icon"></obi-placeholder>
|
|
296
|
+
</obc-navigation-item>
|
|
297
|
+
<obc-navigation-item label="Autopilot" hasIcon>
|
|
298
|
+
<obi-placeholder slot="icon"></obi-placeholder>
|
|
299
|
+
</obc-navigation-item>
|
|
300
|
+
</obc-navigation-item-group>
|
|
301
|
+
<obc-navigation-item label="Deck" hasIcon>
|
|
302
|
+
<obi-placeholder slot="icon"></obi-placeholder>
|
|
303
|
+
</obc-navigation-item>
|
|
304
|
+
</obc-navigation-item-group>
|
|
305
|
+
</obc-navigation-menu>
|
|
306
|
+
`;
|
|
307
|
+
},
|
|
308
|
+
};
|
|
@@ -4,6 +4,29 @@ import compentStyle from './navigation-menu.css?inline';
|
|
|
4
4
|
import {ObcNavigationItemGroup} from '../navigation-item-group/navigation-item-group.js';
|
|
5
5
|
import {ObcNavigationItem} from '../navigation-item/navigation-item.js';
|
|
6
6
|
import {customElement} from '../../decorator.js';
|
|
7
|
+
import {
|
|
8
|
+
ObcTreeNavigationItem,
|
|
9
|
+
TreeBranchType,
|
|
10
|
+
} from '../tree-navigation-item/tree-navigation-item.js';
|
|
11
|
+
import {
|
|
12
|
+
TreeRovingNavigator,
|
|
13
|
+
TreeRovingAdapter,
|
|
14
|
+
} from '../../internal/tree-roving-navigator.js';
|
|
15
|
+
|
|
16
|
+
/** A nav-menu tree row is either a navigation item or a navigation item group. */
|
|
17
|
+
type NavTreeRow = ObcNavigationItem | ObcNavigationItemGroup;
|
|
18
|
+
|
|
19
|
+
const NAV_ITEM_TAG = 'obc-navigation-item';
|
|
20
|
+
const NAV_GROUP_TAG = 'obc-navigation-item-group';
|
|
21
|
+
|
|
22
|
+
function isNavGroup(el: Element): el is ObcNavigationItemGroup {
|
|
23
|
+
return el.tagName.toLowerCase() === NAV_GROUP_TAG;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isNavRow(el: Element): el is NavTreeRow {
|
|
27
|
+
const tag = el.tagName.toLowerCase();
|
|
28
|
+
return tag === NAV_ITEM_TAG || tag === NAV_GROUP_TAG;
|
|
29
|
+
}
|
|
7
30
|
|
|
8
31
|
/**
|
|
9
32
|
* `ObcNavigationMenuVariant` – Enumerates the available visual and behavioral variants for `<obc-navigation-menu>`.
|
|
@@ -12,6 +35,7 @@ import {customElement} from '../../decorator.js';
|
|
|
12
35
|
* - `IconOnly`: Compact menu showing only icons (should only be used when no flyouts/submenus are present).
|
|
13
36
|
* - `IconOnlyLarge`: Icon-only menu variant designed for use when flyouts/submenus are present.
|
|
14
37
|
* - `Compact`: Space-saving menu with reduced padding and layout.
|
|
38
|
+
* - `Tree`: Hierarchical tree — groups expand inline and rows are indented by depth.
|
|
15
39
|
*
|
|
16
40
|
* Use these variants to adapt the navigation menu to different layouts or device sizes.
|
|
17
41
|
*/
|
|
@@ -20,6 +44,7 @@ export enum ObcNavigationMenuVariant {
|
|
|
20
44
|
IconOnly = 'icon-only', // Should only be used when no flyouts are present in the navigation menu
|
|
21
45
|
IconOnlyLarge = 'icon-only-large', // Should be used when flyouts are present in the navigation menu
|
|
22
46
|
Compact = 'compact',
|
|
47
|
+
Tree = 'tree',
|
|
23
48
|
}
|
|
24
49
|
|
|
25
50
|
/**
|
|
@@ -49,6 +74,11 @@ export enum ObcNavigationMenuFlyoutVariant {
|
|
|
49
74
|
* - **IconOnly:** Shows only icons for a compact appearance. *Should only be used when no navigation items have sub-items or flyouts.*
|
|
50
75
|
* - **IconOnlyLarge:** Icon-only mode that supports flyouts/submenus. Use when navigation contains groups or nested items.
|
|
51
76
|
* - **Compact:** Reduces padding and overall width for a space-saving layout.
|
|
77
|
+
* - **Tree:** Renders the same `obc-navigation-item` / `obc-navigation-item-group`
|
|
78
|
+
* markup as a hierarchical tree. Groups expand inline (instead of as flyouts),
|
|
79
|
+
* rows are indented by depth, multiple branches can stay open at once, and the
|
|
80
|
+
* tree-row presentation (alert badge, terminal marker) is available via the
|
|
81
|
+
* items' `hasAlertBadge`/`alertCount`/`alertType`/`terminalType` properties.
|
|
52
82
|
* - **Responsive Layout:**
|
|
53
83
|
* - `smallScreen` property adapts the footer and logo layout for smaller viewports.
|
|
54
84
|
* - **Slot-based Content:**
|
|
@@ -71,6 +101,10 @@ export enum ObcNavigationMenuFlyoutVariant {
|
|
|
71
101
|
* - Use the `Full` variant for standard navigation with both icons and labels.
|
|
72
102
|
* - Use `IconOnly` only when there are no nested groups or flyouts; otherwise, use `IconOnlyLarge` for icon-only navigation with flyout support.
|
|
73
103
|
* - The `Compact` variant is suitable for layouts with limited space or when a minimal navigation appearance is desired.
|
|
104
|
+
* - Use the `Tree` variant for hierarchical navigation (file trees, nested
|
|
105
|
+
* sections). The existing item/group markup is reused unchanged — only the
|
|
106
|
+
* `variant` changes. Footer and logo slots remain flat. Mark a group with
|
|
107
|
+
* `defaultOpen` to have it start expanded.
|
|
74
108
|
* - Set `smallScreen` to `true` to optimize the layout for smaller devices or responsive breakpoints.
|
|
75
109
|
*
|
|
76
110
|
* **TODO(designer):** Provide additional guidance on when to use each variant and recommended slot content for best usability.
|
|
@@ -129,6 +163,7 @@ export class ObcNavigationMenu extends LitElement {
|
|
|
129
163
|
* - `icon-only`: Compact, icon-only menu (use only when no flyouts/groups are present).
|
|
130
164
|
* - `icon-only-large`: Icon-only menu supporting flyouts/groups.
|
|
131
165
|
* - `compact`: Minimal, space-saving menu.
|
|
166
|
+
* - `tree`: Hierarchical tree — groups expand inline and rows are indented by depth.
|
|
132
167
|
*/
|
|
133
168
|
@property({type: String}) variant: ObcNavigationMenuVariant =
|
|
134
169
|
ObcNavigationMenuVariant.Full;
|
|
@@ -148,6 +183,59 @@ export class ObcNavigationMenu extends LitElement {
|
|
|
148
183
|
private slotObservers: MutationObserver[] = [];
|
|
149
184
|
@state() private hasFooter = false;
|
|
150
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Roving-tabindex + arrow-key navigation for the Tree variant, sharing the
|
|
188
|
+
* navigator that drives `obc-tree-navigation`. Engaged only while
|
|
189
|
+
* `variant === Tree` (see `onTreeKeydown`).
|
|
190
|
+
*/
|
|
191
|
+
private readonly treeNavigator = new TreeRovingNavigator<NavTreeRow>(this, {
|
|
192
|
+
getRows: () => this.treeRootRows(),
|
|
193
|
+
childRows: (row) => this.treeChildRows(row),
|
|
194
|
+
isGroup: (row) => isNavGroup(row),
|
|
195
|
+
// Read the group's own synchronous open state, not the shadow header's
|
|
196
|
+
// attribute (which lags a render tick behind a keyboard expand/collapse).
|
|
197
|
+
isExpanded: (row) => isNavGroup(row) && row.expanded,
|
|
198
|
+
setExpanded: (row, expanded) => {
|
|
199
|
+
if (!isNavGroup(row)) return;
|
|
200
|
+
if (expanded) row.open();
|
|
201
|
+
else row.close();
|
|
202
|
+
},
|
|
203
|
+
innerItem: (row) => this.treeInnerItem(row),
|
|
204
|
+
} satisfies TreeRovingAdapter<NavTreeRow>);
|
|
205
|
+
|
|
206
|
+
/** Top-level tree rows of the main slot (mirrors `assignTreeBranches`'s filter). */
|
|
207
|
+
private treeRootRows(): NavTreeRow[] {
|
|
208
|
+
return Array.from(this.children).filter((child): child is NavTreeRow => {
|
|
209
|
+
if (!isNavRow(child)) return false;
|
|
210
|
+
const slot = child.getAttribute('slot');
|
|
211
|
+
return slot === null || slot === 'main';
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Direct tree-row children of a row, in document order. */
|
|
216
|
+
private treeChildRows(row: NavTreeRow): NavTreeRow[] {
|
|
217
|
+
return Array.from(row.children).filter(isNavRow);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The inline `obc-tree-navigation-item` a tree-mode row renders in its shadow root. */
|
|
221
|
+
private treeInnerItem(row: NavTreeRow): ObcTreeNavigationItem | null {
|
|
222
|
+
return (
|
|
223
|
+
row.shadowRoot?.querySelector<ObcTreeNavigationItem>(
|
|
224
|
+
'obc-tree-navigation-item'
|
|
225
|
+
) ?? null
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private onTreeKeydown = (event: KeyboardEvent): void => {
|
|
230
|
+
if (this.variant !== ObcNavigationMenuVariant.Tree) return;
|
|
231
|
+
if (this.treeNavigator.handleKeydown(event)) event.preventDefault();
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
override connectedCallback(): void {
|
|
235
|
+
super.connectedCallback();
|
|
236
|
+
this.addEventListener('keydown', this.onTreeKeydown);
|
|
237
|
+
}
|
|
238
|
+
|
|
151
239
|
findAllElements<T extends Element>(
|
|
152
240
|
el: Element,
|
|
153
241
|
tag: string,
|
|
@@ -212,6 +300,7 @@ export class ObcNavigationMenu extends LitElement {
|
|
|
212
300
|
registerGroup(groups: ObcNavigationItemGroup[]) {
|
|
213
301
|
groups.forEach((group) => {
|
|
214
302
|
group.addEventListener('open', () => {
|
|
303
|
+
if (this.variant === ObcNavigationMenuVariant.Tree) return;
|
|
215
304
|
groups.forEach((g) => {
|
|
216
305
|
if (g !== group) {
|
|
217
306
|
g.close();
|
|
@@ -296,6 +385,7 @@ export class ObcNavigationMenu extends LitElement {
|
|
|
296
385
|
override disconnectedCallback(): void {
|
|
297
386
|
super.disconnectedCallback();
|
|
298
387
|
this.cleanupSlotObservers();
|
|
388
|
+
this.removeEventListener('keydown', this.onTreeKeydown);
|
|
299
389
|
}
|
|
300
390
|
|
|
301
391
|
private handleSlotChange() {
|
|
@@ -303,7 +393,68 @@ export class ObcNavigationMenu extends LitElement {
|
|
|
303
393
|
this.setupSlotObservers();
|
|
304
394
|
}
|
|
305
395
|
|
|
396
|
+
private assignTreeBranches(el: Element, depth: number): void {
|
|
397
|
+
for (const child of el.children) {
|
|
398
|
+
const tag = child.tagName.toLowerCase();
|
|
399
|
+
const isItem = tag === 'obc-navigation-item';
|
|
400
|
+
const isGroup = tag === 'obc-navigation-item-group';
|
|
401
|
+
if (!isItem && !isGroup) continue;
|
|
402
|
+
// Top-level rows are slotted; only treeify the main slot. Nested rows have
|
|
403
|
+
// no slot attribute and always belong to their group's tree.
|
|
404
|
+
if (depth === 0) {
|
|
405
|
+
const slot = child.getAttribute('slot');
|
|
406
|
+
if (slot !== null && slot !== 'main') continue;
|
|
407
|
+
}
|
|
408
|
+
const branches: TreeBranchType[] = Array.from(
|
|
409
|
+
{length: depth},
|
|
410
|
+
() => TreeBranchType.blank
|
|
411
|
+
);
|
|
412
|
+
const row = child as ObcNavigationItem | ObcNavigationItemGroup;
|
|
413
|
+
row.treeMode = true;
|
|
414
|
+
row.treeBranches = branches;
|
|
415
|
+
if (isGroup) {
|
|
416
|
+
this.assignTreeBranches(child, depth + 1);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Reset tree mode on every descendant item/group so switching away from the
|
|
422
|
+
// Tree variant restores the flat rendering.
|
|
423
|
+
private clearTreeMode(el: Element): void {
|
|
424
|
+
for (const child of el.children) {
|
|
425
|
+
const tag = child.tagName.toLowerCase();
|
|
426
|
+
const isItem = tag === 'obc-navigation-item';
|
|
427
|
+
const isGroup = tag === 'obc-navigation-item-group';
|
|
428
|
+
if (!isItem && !isGroup) continue;
|
|
429
|
+
const row = child as ObcNavigationItem | ObcNavigationItemGroup;
|
|
430
|
+
row.treeMode = false;
|
|
431
|
+
row.treeBranches = [];
|
|
432
|
+
if (isGroup) {
|
|
433
|
+
this.clearTreeMode(child);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
306
438
|
private setupItems() {
|
|
439
|
+
if (this.variant === ObcNavigationMenuVariant.Tree) {
|
|
440
|
+
this.assignTreeBranches(this, 0);
|
|
441
|
+
// Footer and logo are not part of the tree — keep them as flat full-variant
|
|
442
|
+
// items (and clear any tree mode left over from the main slot's walk).
|
|
443
|
+
(['footer', 'logo'] as const).forEach((slot) => {
|
|
444
|
+
this.findAllItems(this, slot).forEach((item) => {
|
|
445
|
+
item.treeMode = false;
|
|
446
|
+
item.treeBranches = [];
|
|
447
|
+
item.variant = ObcNavigationMenuVariant.Full;
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
// Defer so the rows' inline tree-item headers exist before the roving
|
|
451
|
+
// tabindex is assigned to them.
|
|
452
|
+
queueMicrotask(() => this.treeNavigator.refresh());
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
this.clearTreeMode(this);
|
|
457
|
+
|
|
307
458
|
const hug =
|
|
308
459
|
this.variant !== ObcNavigationMenuVariant.Full ||
|
|
309
460
|
this.flyoutVariant === ObcNavigationMenuFlyoutVariant.Compact;
|