@eui/eds-wc 22.0.0-alpha.38 → 22.0.0-alpha.40

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.
@@ -1,933 +0,0 @@
1
- var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- };
7
- import { html, css, nothing } from 'lit';
8
- import { customElement, property, state, query } from 'lit/decorators.js';
9
- import { EdsBaseElement } from '../shared/eds-base.js';
10
- /**
11
- * `<eds-wc-menu>` — EDS navigation Menu Web Component.
12
- *
13
- * A data-driven hierarchical navigation menu with expand/collapse, optional icons,
14
- * markers, badges, action icons, an optional search filter, collapsed (icon-only) mode
15
- * and full WAI-ARIA `tree`/`treeitem` keyboard navigation (roving tabindex, Arrow keys,
16
- * Home, End, Enter, Space).
17
- *
18
- * Projected from the Angular `eds-menu` + `eds-menu-item` components. The `eds-menu-item`
19
- * Angular sub-component is not a separately-consumable element here — its structure is
20
- * rendered internally by this element so every ARIA relationship stays in one shadow root
21
- * (Rule A1).
22
- *
23
- * @example
24
- * ```html
25
- * <eds-wc-menu has-icons></eds-wc-menu>
26
- * <script>
27
- * document.querySelector('eds-wc-menu').items = [
28
- * { label: 'Home', url: '/home', icon: 'house' },
29
- * { label: 'Reports', icon: 'chart-bar', children: [
30
- * { label: 'Monthly', url: '/reports/monthly' },
31
- * ]},
32
- * ];
33
- * </script>
34
- * ```
35
- *
36
- * @fires is-click - `detail: { value: boolean }` — emitted (true) when any menu item is clicked.
37
- * @fires item-click - `detail: { item: EdsWcMenuItem }` — the clicked menu item.
38
- * @fires expand-toggle - `detail: { item: EdsWcMenuItem }` — a menu item's expand state was toggled.
39
- * @fires filter-change - `detail: { value: string }` — the search filter value changed.
40
- */
41
- let EdsWcMenu = class EdsWcMenu extends EdsBaseElement {
42
- constructor() {
43
- super(...arguments);
44
- /** Hierarchical array of menu items to display. */
45
- this.items = [];
46
- /** Label for the search filter input. */
47
- this.searchFilterLabel = 'Search filter';
48
- /** Current filter value for programmatic filtering. */
49
- this.filterValue = '';
50
- /** Collapses the menu to icon-only state. */
51
- this.isCollapsed = false;
52
- /** Shows initials when collapsed (instead of icons). */
53
- this.hasCollapsedInitials = false;
54
- /** Enables the search filter input. */
55
- this.hasFilter = false;
56
- /** Shows icons for menu items. */
57
- this.hasIcons = false;
58
- /** Shows labels underneath icons in collapsed mode. */
59
- this.hasIconsLabels = false;
60
- /** Alias for hasIconsLabels — shows labels underneath icons in collapsed mode. */
61
- this.hasCollapsedLabels = false;
62
- /** Enables tooltips on menu items (shown in collapsed mode). */
63
- this.hasTooltip = true;
64
- /** Shows tooltips even when menu is expanded. */
65
- this.hasTooltipOnExpanded = false;
66
- /** Only shows tooltip from the tooltip property (not auto-generated from label) when expanded. */
67
- this.hasTooltipOnlyOnExpanded = false;
68
- /** Expands all items that do not have an explicit expanded property. */
69
- this.expandAllItems = false;
70
- /** Applies flat styling without hierarchical indentation. */
71
- this.isFlat = false;
72
- /** Automatically scrolls to the active menu item on route changes. */
73
- this.hasScrollToItem = false;
74
- /** Applies bold font weight to root-level menu items. */
75
- this.hasBoldRootLevel = false;
76
- /** Internal configured items with parent refs, visibility, and filtering applied. */
77
- this._internalItems = [];
78
- /** Whether expand icons should be shown (hidden during filter). */
79
- this._hasExpandIcon = true;
80
- /** id of the item currently holding the roving tabindex focus. */
81
- this._activeId = null;
82
- }
83
- static { this.styles = css `
84
- :host,
85
- *,
86
- *::before,
87
- *::after {
88
- box-sizing: border-box;
89
- }
90
-
91
- :host {
92
- display: block;
93
- width: 100%;
94
- }
95
-
96
- .eds-menu__list {
97
- display: flex;
98
- flex-direction: column;
99
- gap: 0;
100
- list-style: none;
101
- margin: 0;
102
- padding: 0;
103
- width: 100%;
104
- }
105
-
106
- .eds-menu__list:focus-visible {
107
- outline: 2px solid var(--eds-c-border-focus);
108
- outline-offset: -2px;
109
- }
110
-
111
- .eds-menu__sublist {
112
- padding-left: var(--eds-sp-m);
113
- }
114
-
115
- :host([is-collapsed]) .eds-menu__sublist {
116
- padding-left: var(--eds-sp-xs);
117
- }
118
-
119
- :host([is-flat]) .eds-menu__sublist {
120
- padding-left: 0;
121
- }
122
-
123
- /* Filter */
124
- .eds-menu-filter {
125
- display: flex;
126
- padding: var(--eds-sp-s) var(--eds-sp-m);
127
- width: 100%;
128
- }
129
-
130
- .eds-menu-filter--collapsed,
131
- :host([is-collapsed]) .eds-menu-filter {
132
- padding: 0;
133
- height: 0;
134
- overflow: hidden;
135
- visibility: hidden;
136
- }
137
-
138
- .eds-menu-filter__input-wrapper {
139
- display: flex;
140
- align-items: center;
141
- gap: var(--eds-sp-xs);
142
- width: 100%;
143
- padding: var(--eds-sp-xs) var(--eds-sp-s);
144
- border: 1px solid var(--eds-c-border-divider);
145
- border-radius: var(--eds-br-s);
146
- background: var(--eds-c-surface-default);
147
- }
148
-
149
- .eds-menu-filter__icon {
150
- flex-shrink: 0;
151
- color: var(--eds-c-foreground-subtler);
152
- display: inline-flex;
153
- }
154
-
155
- .eds-menu-filter__icon svg {
156
- width: var(--eds-is-s);
157
- height: var(--eds-is-s);
158
- fill: currentColor;
159
- }
160
-
161
- .eds-menu-filter__input {
162
- border: none;
163
- outline: none;
164
- background: transparent;
165
- font: var(--eds-f-paragraph-s);
166
- color: var(--eds-c-foreground-default);
167
- width: 100%;
168
- }
169
-
170
- .eds-menu-filter__input::placeholder {
171
- color: var(--eds-c-foreground-subtler);
172
- }
173
-
174
- /* Menu item */
175
- .eds-menu-item {
176
- display: block;
177
- width: 100%;
178
- }
179
-
180
- .eds-menu-item:focus-visible {
181
- outline: 2px solid var(--eds-c-border-focus);
182
- outline-offset: -2px;
183
- }
184
-
185
- .eds-menu-item__content {
186
- display: flex;
187
- flex-direction: column;
188
- position: relative;
189
- width: 100%;
190
- }
191
-
192
- .eds-menu-item__link {
193
- display: flex;
194
- align-items: center;
195
- gap: var(--eds-sp-xs);
196
- padding: var(--eds-sp-xs) var(--eds-sp-l);
197
- font: var(--eds-f-paragraph-s);
198
- color: var(--eds-c-foreground-default);
199
- text-decoration: none;
200
- cursor: pointer;
201
- border: none;
202
- background: none;
203
- width: 100%;
204
- min-height: 40px;
205
- border-left: 3px solid transparent;
206
- transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
207
- }
208
-
209
- .eds-menu-item__link:hover:not(.eds-menu-item__link--disabled):not(.eds-menu-item__link--category) {
210
- background: var(--eds-c-surface-elevation-sunken);
211
- color: var(--eds-c-foreground-primary);
212
- }
213
-
214
- .eds-menu-item__link--active:not(.eds-menu-item--disabled) {
215
- background: var(--eds-c-surface-primary-subtle);
216
- color: var(--eds-c-foreground-primary);
217
- font-weight: var(--eds-f-w-semibold);
218
- border-left-color: var(--eds-c-border-primary);
219
- }
220
-
221
- .eds-menu-item__link--disabled {
222
- opacity: var(--eds-op-40);
223
- pointer-events: none;
224
- }
225
-
226
- .eds-menu-item__link--category {
227
- cursor: default;
228
- font: var(--eds-f-label-s-semibold);
229
- color: var(--eds-c-foreground-subtler);
230
- text-transform: uppercase;
231
- letter-spacing: 0.02em;
232
- padding: var(--eds-sp-m) var(--eds-sp-l) var(--eds-sp-xs);
233
- min-height: auto;
234
- border-left-color: transparent;
235
- }
236
-
237
- .eds-menu-item__link--category:hover {
238
- background: none;
239
- }
240
-
241
- .eds-menu-item__link-start-block {
242
- display: flex;
243
- align-items: center;
244
- gap: var(--eds-sp-xs);
245
- flex-shrink: 0;
246
- position: relative;
247
- }
248
-
249
- .eds-menu-item__link-start-block:empty {
250
- display: none;
251
- }
252
-
253
- .eds-menu-item__link-content-block {
254
- display: flex;
255
- align-items: center;
256
- gap: var(--eds-sp-xs);
257
- flex: 1;
258
- min-width: 0;
259
- }
260
-
261
- .eds-menu-item__link-label {
262
- white-space: nowrap;
263
- overflow: hidden;
264
- text-overflow: ellipsis;
265
- }
266
-
267
- .eds-menu-item__link-label--category {
268
- font: var(--eds-f-label-s-semibold);
269
- color: var(--eds-c-foreground-subtler);
270
- }
271
-
272
- .eds-menu-item__link-end-block {
273
- display: flex;
274
- align-items: center;
275
- gap: var(--eds-sp-xs);
276
- flex-shrink: 0;
277
- margin-left: auto;
278
- }
279
-
280
- .eds-menu-item__link-external-icon {
281
- flex-shrink: 0;
282
- color: var(--eds-c-foreground-subtler);
283
- display: inline-flex;
284
- }
285
-
286
- .eds-menu-item__icon {
287
- flex-shrink: 0;
288
- display: inline-flex;
289
- }
290
-
291
- .eds-menu-item__icon svg {
292
- width: var(--eds-is-s);
293
- height: var(--eds-is-s);
294
- fill: currentColor;
295
- }
296
-
297
- .eds-menu-item__link-external-icon svg {
298
- width: var(--eds-is-xs);
299
- height: var(--eds-is-xs);
300
- fill: currentColor;
301
- }
302
-
303
- /* Status/marker dot rendered as a proper CSS disc. */
304
- .eds-menu-item__marker {
305
- flex-shrink: 0;
306
- width: var(--eds-sp-xs);
307
- height: var(--eds-sp-xs);
308
- border-radius: 50%;
309
- background: currentColor;
310
- }
311
-
312
- .eds-menu-item__initials {
313
- display: flex;
314
- align-items: center;
315
- justify-content: center;
316
- font: var(--eds-f-label-m-semibold);
317
- width: 32px;
318
- height: 32px;
319
- border-radius: var(--eds-br-s);
320
- background: var(--eds-c-surface-elevation-sunken);
321
- border: 1px solid var(--eds-c-border-divider);
322
- flex-shrink: 0;
323
- }
324
-
325
- .eds-menu-item__icon-label {
326
- font: var(--eds-f-microcopy-2xs);
327
- color: var(--eds-c-foreground-subtler);
328
- white-space: nowrap;
329
- overflow: hidden;
330
- text-overflow: ellipsis;
331
- max-width: 100%;
332
- text-align: center;
333
- }
334
-
335
- .eds-menu-item__badge {
336
- display: inline-flex;
337
- align-items: center;
338
- justify-content: center;
339
- font: var(--eds-f-microcopy-xs);
340
- padding: 0 var(--eds-sp-2xs);
341
- border-radius: var(--eds-br-full);
342
- background: var(--eds-c-surface-neutral-subtle);
343
- color: var(--eds-c-foreground-default);
344
- }
345
-
346
- .eds-menu-item__badge--dot {
347
- position: absolute;
348
- top: -2px;
349
- right: -2px;
350
- width: var(--eds-sp-xs);
351
- height: var(--eds-sp-xs);
352
- padding: 0;
353
- border-radius: 50%;
354
- pointer-events: none;
355
- }
356
-
357
- .eds-menu-item__badge--info { background: var(--eds-c-surface-info); }
358
- .eds-menu-item__badge--success { background: var(--eds-c-surface-success); }
359
- .eds-menu-item__badge--warning { background: var(--eds-c-surface-warning); }
360
- .eds-menu-item__badge--critical { background: var(--eds-c-surface-critical); }
361
-
362
- .eds-menu-item__action-icon,
363
- .eds-menu-item__expand-toggle {
364
- display: inline-flex;
365
- align-items: center;
366
- justify-content: center;
367
- width: 24px;
368
- height: 24px;
369
- border: none;
370
- background: transparent;
371
- cursor: pointer;
372
- border-radius: var(--eds-br-s);
373
- color: var(--eds-c-foreground-subtle);
374
- flex-shrink: 0;
375
- font-family: inherit;
376
- }
377
-
378
- .eds-menu-item__expand-toggle {
379
- border-radius: var(--eds-br-full);
380
- }
381
-
382
- .eds-menu-item__action-icon:hover,
383
- .eds-menu-item__expand-toggle:hover {
384
- background: var(--eds-c-surface-neutral-subtle);
385
- }
386
-
387
- .eds-menu-item__action-icon:focus-visible,
388
- .eds-menu-item__expand-toggle:focus-visible {
389
- outline: 2px solid var(--eds-c-border-focus);
390
- outline-offset: -2px;
391
- }
392
-
393
- .eds-menu-item__action-icon svg,
394
- .eds-menu-item__expand-toggle svg {
395
- width: var(--eds-is-xs);
396
- height: var(--eds-is-xs);
397
- fill: currentColor;
398
- }
399
-
400
- .eds-menu-item__action-icon {
401
- opacity: 0;
402
- transition: opacity 0.15s ease;
403
- }
404
-
405
- .eds-menu-item__content:hover .eds-menu-item__action-icon {
406
- opacity: 1;
407
- }
408
-
409
- .eds-menu-item--disabled {
410
- opacity: var(--eds-op-40);
411
- pointer-events: none;
412
- }
413
-
414
- .eds-menu-item--expanded > .eds-menu-item__link {
415
- font-weight: var(--eds-f-w-medium);
416
- }
417
-
418
- .eds-menu-item--active > .eds-menu-item__link {
419
- background: var(--eds-c-surface-primary-subtle);
420
- color: var(--eds-c-foreground-primary);
421
- font-weight: var(--eds-f-w-semibold);
422
- border-left-color: var(--eds-c-border-primary);
423
- }
424
-
425
- /* Bold root level */
426
- .eds-menu-item--bold > .eds-menu-item__content > .eds-menu-item__link {
427
- font-weight: var(--eds-f-w-semibold);
428
- }
429
-
430
- /* Icon-label mode (collapsed with labels under icons) */
431
- .eds-menu-item--icon-label .eds-menu-item__link {
432
- flex-direction: column;
433
- align-items: center;
434
- justify-content: center;
435
- padding: var(--eds-sp-xs);
436
- gap: var(--eds-sp-4xs);
437
- border-left: none;
438
- border-radius: var(--eds-br-s);
439
- }
440
-
441
- .eds-menu-item--icon-label .eds-menu-item__link-content-block,
442
- .eds-menu-item--icon-label .eds-menu-item__link-end-block {
443
- display: none;
444
- }
445
-
446
- /* Collapsed host-level adjustments */
447
- :host([is-collapsed]) .eds-menu-item__link {
448
- justify-content: center;
449
- padding: var(--eds-sp-xs) var(--eds-sp-s);
450
- border-left: none;
451
- border-radius: var(--eds-br-s);
452
- margin: var(--eds-sp-4xs) 0;
453
- }
454
-
455
- :host([is-collapsed]) .eds-menu-item__link-content-block,
456
- :host([is-collapsed]) .eds-menu-item__link-end-block {
457
- display: none;
458
- }
459
- `; }
460
- willUpdate(changed) {
461
- if (changed.has('items') || changed.has('expandAllItems') || changed.has('hasCollapsedInitials')) {
462
- const configured = this._configureItems([...(this.items ?? [])], undefined, this.expandAllItems);
463
- this._internalItems = configured;
464
- this._hasExpandIcon = true;
465
- }
466
- if (changed.has('filterValue')) {
467
- if (this.hasFilter && this.filterValue) {
468
- this._applyFilter(this.filterValue);
469
- }
470
- }
471
- }
472
- updated() {
473
- // Ensure a single roving tab stop exists.
474
- if (!this._activeId) {
475
- const first = this._flatVisible()[0];
476
- if (first?.id) {
477
- this._activeId = first.id;
478
- }
479
- }
480
- }
481
- render() {
482
- return html `
483
- ${this.hasFilter
484
- ? html `
485
- <div class="eds-menu-filter ${this.isCollapsed ? 'eds-menu-filter--collapsed' : ''}"
486
- @click="${this._onFilterClick}">
487
- <div class="eds-menu-filter__input-wrapper">
488
- <span class="eds-menu-filter__icon">
489
- <svg aria-hidden="true"><use href="${this._icon('magnifying-glass', 'core')}"></use></svg>
490
- </span>
491
- <input
492
- class="eds-menu-filter__input"
493
- type="text"
494
- @input="${this._onFilter}"
495
- placeholder="${this.searchFilterLabel}"
496
- aria-label="${this.searchFilterLabel}"
497
- .value="${this.filterValue}" />
498
- </div>
499
- </div>`
500
- : nothing}
501
-
502
- <ul class="eds-menu__list"
503
- role="tree"
504
- aria-orientation="vertical"
505
- tabindex="0"
506
- @keydown="${this._onKeydown}">
507
- ${this._internalItems.map(item => this._renderItem(item, 0))}
508
- </ul>
509
- `;
510
- }
511
- // ---- rendering -------------------------------------------------------
512
- _renderItem(item, level) {
513
- if (!(item.visible && item.filtered)) {
514
- return nothing;
515
- }
516
- const isLabelItem = this._isLabelItem(item);
517
- const hasChildren = !!item.children?.length;
518
- const isActiveTab = this._activeId === item.id;
519
- const disabled = !!item.disabled;
520
- const isBold = !item.parent && this.hasBoldRootLevel;
521
- const iconLabelMode = this.isCollapsed && (this.hasIconsLabels || this.hasCollapsedLabels);
522
- const wrapperClasses = [
523
- 'eds-menu-item',
524
- iconLabelMode ? 'eds-menu-item--icon-label' : '',
525
- isBold ? 'eds-menu-item--bold' : '',
526
- ].filter(Boolean).join(' ');
527
- const contentClasses = [
528
- 'eds-menu-item__content',
529
- isLabelItem ? 'eds-menu-item__content--category' : '',
530
- disabled ? 'eds-menu-item--disabled' : '',
531
- item.expanded ? 'eds-menu-item--expanded' : '',
532
- item.active ? 'eds-menu-item--active' : '',
533
- ].filter(Boolean).join(' ');
534
- const linkClasses = [
535
- 'eds-menu-item__link',
536
- isLabelItem ? 'eds-menu-item__link--category' : '',
537
- item.active ? 'eds-menu-item__link--active' : '',
538
- disabled ? 'eds-menu-item__link--disabled' : '',
539
- hasChildren ? 'eds-menu-item__link--has-sub' : '',
540
- ].filter(Boolean).join(' ');
541
- const tooltip = this._tooltipText(item);
542
- return html `
543
- <li class="${wrapperClasses}"
544
- role="treeitem"
545
- data-item-id="${item.id ?? nothing}"
546
- data-e2e="${item.e2eAttr ?? 'eds-menu-item'}"
547
- tabindex="${isActiveTab ? '0' : '-1'}"
548
- aria-haspopup="${hasChildren ? 'true' : nothing}"
549
- aria-expanded="${hasChildren ? String(!!item.expanded) : nothing}"
550
- aria-disabled="${disabled ? 'true' : nothing}"
551
- aria-current="${item.active ? 'page' : nothing}"
552
- aria-label="${item.label ?? nothing}"
553
- @click="${(e) => this._onItemActivate(e, item)}">
554
- <div class="${contentClasses}" title="${tooltip ?? nothing}">
555
- <span class="${linkClasses}">
556
- <span class="eds-menu-item__link-start-block">
557
- ${this._renderIconBlock(item)}
558
- </span>
559
- <span class="eds-menu-item__link-content-block">
560
- <span class="eds-menu-item__link-label">${item.label ?? ''}</span>
561
- ${!isLabelItem && item.urlExternal && item.urlExternalTarget === '_blank'
562
- ? html `<span class="eds-menu-item__link-external-icon">
563
- <svg aria-hidden="true"><use href="${this._icon('arrow-square-out', 'core')}"></use></svg>
564
- </span>`
565
- : nothing}
566
- </span>
567
- ${!isLabelItem
568
- ? html `<span class="eds-menu-item__link-end-block">
569
- ${item.tagLabel && !this.isCollapsed
570
- ? html `<span class="eds-menu-item__badge ${this._badgeColorClass(item.tagTypeClass)}">${item.tagLabel}</span>`
571
- : nothing}
572
- ${item.actionIcon
573
- ? html `<button class="eds-menu-item__action-icon"
574
- type="button"
575
- aria-label="${item.actionIcon.label || 'Action'}"
576
- @click="${(e) => this._onActionIconClick(e, item)}">
577
- <svg aria-hidden="true"><use href="${this._icon(item.actionIcon.icon)}"></use></svg>
578
- </button>`
579
- : nothing}
580
- ${this._hasExpandIcon && hasChildren
581
- ? html `<button class="eds-menu-item__expand-toggle"
582
- type="button"
583
- aria-label="${item.expanded ? 'Collapse' : 'Expand'}"
584
- @click="${(e) => this._onExpandClick(e, item)}">
585
- <svg aria-hidden="true"><use href="${this._icon(item.expanded ? 'caret-up' : 'caret-down', 'core')}"></use></svg>
586
- </button>`
587
- : nothing}
588
- </span>`
589
- : nothing}
590
- </span>
591
- ${this._shouldRenderChild(item)
592
- ? html `<ul class="eds-menu__list eds-menu__sublist" role="group" aria-label="${item.label ?? nothing}">
593
- ${item.children.map(child => this._renderItem(child, level + 1))}
594
- </ul>`
595
- : nothing}
596
- </div>
597
- </li>
598
- `;
599
- }
600
- _renderIconBlock(item) {
601
- const markerColor = `var(--eds-c-foreground-${item.markerTypeClass || 'primary'})`;
602
- if (!this.isCollapsed) {
603
- if (this.hasIcons) {
604
- if (item.icon) {
605
- return html `<span class="eds-menu-item__icon"><svg aria-hidden="true"><use href="${this._icon(item.icon)}"></use></svg></span>`;
606
- }
607
- if (item.hasMarker) {
608
- return html `<span class="eds-menu-item__marker" style="color:${markerColor}"></span>`;
609
- }
610
- return nothing;
611
- }
612
- if (item.hasMarker) {
613
- return html `<span class="eds-menu-item__marker" style="color:${markerColor}"></span>`;
614
- }
615
- return nothing;
616
- }
617
- // Collapsed mode
618
- const badgeDot = item.tagLabel
619
- ? html `<span class="eds-menu-item__badge eds-menu-item__badge--dot ${this._badgeColorClass(item.tagTypeClass)}"></span>`
620
- : nothing;
621
- if (this.hasCollapsedInitials && item.initials) {
622
- return html `<span class="eds-menu-item__initials">${item.initials}</span>${badgeDot}`;
623
- }
624
- if (this.hasIcons && item.icon) {
625
- return html `<span class="eds-menu-item__icon"><svg aria-hidden="true"><use href="${this._icon(item.icon)}"></use></svg></span>${badgeDot}`;
626
- }
627
- if (item.hasMarker) {
628
- return html `<span class="eds-menu-item__marker" style="color:${markerColor}"></span>${badgeDot}`;
629
- }
630
- return html `<span class="eds-menu-item__initials">${item.label?.charAt(0) ?? ''}</span>${badgeDot}`;
631
- }
632
- // ---- icon / badge helpers -------------------------------------------
633
- _icon(name, defaultSet = 'regular') {
634
- const baseUrl = EdsBaseElement.assetsBaseUrl;
635
- const [iconName, set] = name.includes(':') ? name.split(':') : [name, defaultSet];
636
- return `${baseUrl}/eds/icons/sprites/${set}.svg#${iconName}`;
637
- }
638
- _badgeColorClass(tagTypeClass) {
639
- return tagTypeClass ? `eds-menu-item__badge--${tagTypeClass}` : '';
640
- }
641
- // ---- item type predicates -------------------------------------------
642
- _isLabelItem(item) {
643
- return !item.url && !item.urlExternal && !item.command && !item.children && !item.link;
644
- }
645
- _tooltipText(item) {
646
- if (!this.hasTooltip) {
647
- return null;
648
- }
649
- if (this.isCollapsed) {
650
- return item.tooltip || item.label || null;
651
- }
652
- if (this.hasTooltipOnExpanded) {
653
- return item.tooltip || item.label || null;
654
- }
655
- return null;
656
- }
657
- _shouldRenderChild(item) {
658
- return !!(item?.children && (item.expanded || (this.hasFilter && item.filtered && this._isFilterActive())));
659
- }
660
- // ---- interaction -----------------------------------------------------
661
- _onItemActivate(event, item) {
662
- event.preventDefault();
663
- event.stopPropagation();
664
- this._activeId = item.id ?? this._activeId;
665
- this._activate(item);
666
- }
667
- _activate(item) {
668
- if (!item.disabled) {
669
- if (item.urlExternal) {
670
- window.open(item.urlExternal, item.urlExternalTarget || '_blank');
671
- }
672
- else if (item.url) {
673
- // No Angular router in the WC — dispatch item-click so the consumer navigates.
674
- }
675
- else if (typeof item.command === 'function') {
676
- item.command();
677
- }
678
- else {
679
- this._toggleExpand(item);
680
- }
681
- }
682
- this.dispatchEvent(new CustomEvent('is-click', { bubbles: true, composed: true, detail: { value: true } }));
683
- this.dispatchEvent(new CustomEvent('item-click', { bubbles: true, composed: true, detail: { item } }));
684
- }
685
- _onExpandClick(event, item) {
686
- event.preventDefault();
687
- event.stopPropagation();
688
- this._toggleExpand(item);
689
- }
690
- _toggleExpand(item) {
691
- if (item.children) {
692
- item.expanded = !item.expanded;
693
- this.dispatchEvent(new CustomEvent('expand-toggle', { bubbles: true, composed: true, detail: { item } }));
694
- this._internalItems = [...this._internalItems];
695
- }
696
- }
697
- _onActionIconClick(event, item) {
698
- event.preventDefault();
699
- event.stopPropagation();
700
- item.actionIcon?.action?.(event);
701
- }
702
- _onFilterClick(event) {
703
- event.stopPropagation();
704
- }
705
- _onFilter(event) {
706
- const value = event.target.value;
707
- this.filterValue = value;
708
- this._applyFilter(value);
709
- this.dispatchEvent(new CustomEvent('filter-change', { bubbles: true, composed: true, detail: { value } }));
710
- }
711
- _applyFilter(value) {
712
- const items = this._filterMenuItems(this._internalItems, value);
713
- this._hasExpandIcon = !value;
714
- this._internalItems = [...items];
715
- }
716
- _isFilterActive() {
717
- return (this._filterInput?.value?.length ?? this.filterValue.length ?? 0) > 0;
718
- }
719
- _filterMenuItems(items, filterValue) {
720
- return items?.map((item) => {
721
- const found = (item.label?.toLowerCase().indexOf(filterValue.toLowerCase()) ?? -1) !== -1;
722
- if (item.children) {
723
- item.children = this._filterMenuItems(item.children, filterValue);
724
- item.filtered = item.children.some(child => child.filtered);
725
- }
726
- else {
727
- item.filtered = found;
728
- }
729
- return item;
730
- }) || [];
731
- }
732
- // ---- keyboard (WAI-ARIA tree pattern, roving tabindex) ---------------
733
- _onKeydown(event) {
734
- const flat = this._flatVisible();
735
- if (!flat.length) {
736
- return;
737
- }
738
- let index = flat.findIndex(i => i.id === this._activeId);
739
- if (index < 0) {
740
- index = 0;
741
- }
742
- const active = flat[index];
743
- switch (event.key) {
744
- case 'Enter':
745
- case ' ': {
746
- if (active && !active.disabled) {
747
- this._activate(active);
748
- event.preventDefault();
749
- }
750
- break;
751
- }
752
- case 'ArrowRight': {
753
- if (active?.children?.length) {
754
- if (!active.expanded) {
755
- this._toggleExpand(active);
756
- }
757
- else {
758
- const firstChild = active.children[0];
759
- if (firstChild?.id) {
760
- this._focusItem(firstChild.id);
761
- }
762
- }
763
- event.preventDefault();
764
- }
765
- break;
766
- }
767
- case 'ArrowLeft': {
768
- if (active?.children?.length && active.expanded) {
769
- this._toggleExpand(active);
770
- event.preventDefault();
771
- }
772
- else if (active?.parent?.id) {
773
- this._focusItem(active.parent.id);
774
- event.preventDefault();
775
- }
776
- break;
777
- }
778
- case 'ArrowDown': {
779
- event.preventDefault();
780
- this._moveFocus(flat, index, 1);
781
- break;
782
- }
783
- case 'ArrowUp': {
784
- event.preventDefault();
785
- this._moveFocus(flat, index, -1);
786
- break;
787
- }
788
- case 'Home': {
789
- event.preventDefault();
790
- if (flat[0]?.id) {
791
- this._focusItem(flat[0].id);
792
- }
793
- break;
794
- }
795
- case 'End': {
796
- event.preventDefault();
797
- const last = flat[flat.length - 1];
798
- if (last?.id) {
799
- this._focusItem(last.id);
800
- }
801
- break;
802
- }
803
- default:
804
- break;
805
- }
806
- }
807
- /** Move focus with wrap-around (matches Angular FocusKeyManager.withWrap()). */
808
- _moveFocus(flat, from, delta) {
809
- const len = flat.length;
810
- const next = (from + delta + len) % len;
811
- if (flat[next]?.id) {
812
- this._focusItem(flat[next].id);
813
- }
814
- }
815
- _focusItem(id) {
816
- this._activeId = id;
817
- this.updateComplete.then(() => {
818
- const el = this.renderRoot.querySelector(`li[data-item-id="${id}"]`);
819
- el?.focus();
820
- });
821
- }
822
- /** Flattened list of currently-rendered (visible + expanded) items, in DOM order. */
823
- _flatVisible() {
824
- const out = [];
825
- const walk = (items) => {
826
- for (const item of items) {
827
- if (!(item.visible && item.filtered)) {
828
- continue;
829
- }
830
- out.push(item);
831
- if (item.children && this._shouldRenderChild(item)) {
832
- walk(item.children);
833
- }
834
- }
835
- };
836
- walk(this._internalItems);
837
- return out;
838
- }
839
- // ---- item configuration ---------------------------------------------
840
- _configureItems(items, parent, expandAll) {
841
- return items?.map((item) => {
842
- if (item.children) {
843
- item.children = this._configureItems(item.children, item, expandAll);
844
- if (expandAll && item.expanded === undefined) {
845
- item.expanded = true;
846
- }
847
- }
848
- if (parent) {
849
- item.parent = parent;
850
- }
851
- if (!item.id) {
852
- item.id = (globalThis.crypto?.randomUUID?.() ?? `eds-menu-item-${Math.random().toString(36).slice(2)}`);
853
- }
854
- if (item.label && this.hasCollapsedInitials) {
855
- const words = item.label.split(' ');
856
- item.initials = words.length > 1
857
- ? `${words[0].charAt(0)}${words[1].charAt(0)}`
858
- : item.label.substring(0, 2);
859
- }
860
- item.visible = item.visible ?? true;
861
- item.filtered = item.filtered ?? true;
862
- return item;
863
- }) || [];
864
- }
865
- };
866
- __decorate([
867
- property({ type: Array })
868
- ], EdsWcMenu.prototype, "items", void 0);
869
- __decorate([
870
- property({ type: String, attribute: 'search-filter-label' })
871
- ], EdsWcMenu.prototype, "searchFilterLabel", void 0);
872
- __decorate([
873
- property({ type: String, attribute: 'fragment-id' })
874
- ], EdsWcMenu.prototype, "fragmentId", void 0);
875
- __decorate([
876
- property({ type: String, attribute: 'filter-value' })
877
- ], EdsWcMenu.prototype, "filterValue", void 0);
878
- __decorate([
879
- property({ type: Boolean, reflect: true, attribute: 'is-collapsed' })
880
- ], EdsWcMenu.prototype, "isCollapsed", void 0);
881
- __decorate([
882
- property({ type: Boolean, attribute: 'has-collapsed-initials' })
883
- ], EdsWcMenu.prototype, "hasCollapsedInitials", void 0);
884
- __decorate([
885
- property({ type: Boolean, attribute: 'has-filter' })
886
- ], EdsWcMenu.prototype, "hasFilter", void 0);
887
- __decorate([
888
- property({ type: Boolean, attribute: 'has-icons' })
889
- ], EdsWcMenu.prototype, "hasIcons", void 0);
890
- __decorate([
891
- property({ type: Boolean, attribute: 'has-icons-labels' })
892
- ], EdsWcMenu.prototype, "hasIconsLabels", void 0);
893
- __decorate([
894
- property({ type: Boolean, attribute: 'has-collapsed-labels' })
895
- ], EdsWcMenu.prototype, "hasCollapsedLabels", void 0);
896
- __decorate([
897
- property({ type: Boolean, attribute: 'has-tooltip' })
898
- ], EdsWcMenu.prototype, "hasTooltip", void 0);
899
- __decorate([
900
- property({ type: Boolean, attribute: 'has-tooltip-on-expanded' })
901
- ], EdsWcMenu.prototype, "hasTooltipOnExpanded", void 0);
902
- __decorate([
903
- property({ type: Boolean, attribute: 'has-tooltip-only-on-expanded' })
904
- ], EdsWcMenu.prototype, "hasTooltipOnlyOnExpanded", void 0);
905
- __decorate([
906
- property({ type: Boolean, attribute: 'expand-all-items' })
907
- ], EdsWcMenu.prototype, "expandAllItems", void 0);
908
- __decorate([
909
- property({ type: Boolean, reflect: true, attribute: 'is-flat' })
910
- ], EdsWcMenu.prototype, "isFlat", void 0);
911
- __decorate([
912
- property({ type: Boolean, attribute: 'has-scroll-to-item' })
913
- ], EdsWcMenu.prototype, "hasScrollToItem", void 0);
914
- __decorate([
915
- property({ type: Boolean, attribute: 'has-bold-root-level' })
916
- ], EdsWcMenu.prototype, "hasBoldRootLevel", void 0);
917
- __decorate([
918
- state()
919
- ], EdsWcMenu.prototype, "_internalItems", void 0);
920
- __decorate([
921
- state()
922
- ], EdsWcMenu.prototype, "_hasExpandIcon", void 0);
923
- __decorate([
924
- state()
925
- ], EdsWcMenu.prototype, "_activeId", void 0);
926
- __decorate([
927
- query('.eds-menu-filter__input')
928
- ], EdsWcMenu.prototype, "_filterInput", void 0);
929
- EdsWcMenu = __decorate([
930
- customElement('eds-wc-menu')
931
- ], EdsWcMenu);
932
- export { EdsWcMenu };
933
- //# sourceMappingURL=eds-wc-menu.js.map