@rivinia/menu 0.1.0 → 0.3.1

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,5 +1,5 @@
1
1
  import * as i0 from "@angular/core";
2
- import { Component, Directive, ElementRef, ViewEncapsulation, afterRenderEffect, computed, effect, inject, input, output, signal } from "@angular/core";
2
+ import { Component, Directive, ElementRef, ViewEncapsulation, afterRenderEffect, computed, contentChildren, effect, inject, input, output, signal } from "@angular/core";
3
3
  import { RiviniaMenu } from "@rivinia/menu";
4
4
  const MENU_GAP = 6;
5
5
  const EDGE = 8;
@@ -31,6 +31,7 @@ function computeMenuPosition(trigger, panel, placement, viewport, rtl, gap = 6)
31
31
  flipped
32
32
  };
33
33
  }
34
+ let nextPanelId = 0;
34
35
  var RiviniaMenuItem = class RiviniaMenuItem {
35
36
  id = input.required(...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
36
37
  disabled = input(false, {
@@ -38,6 +39,7 @@ var RiviniaMenuItem = class RiviniaMenuItem {
38
39
  transform: (v) => v === "" || v === true
39
40
  });
40
41
  checked = input(null, ...ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []);
42
+ host = inject(ElementRef, { optional: true });
41
43
  menu = inject(RiviniaMenuComponent, {
42
44
  optional: true,
43
45
  skipSelf: true
@@ -53,20 +55,41 @@ var RiviniaMenuItem = class RiviniaMenuItem {
53
55
  const checked = this.checked();
54
56
  return checked === null ? null : String(checked);
55
57
  }
58
+ isDisabled() {
59
+ if (this.disabled()) return true;
60
+ return this.host?.nativeElement?.disabled === true;
61
+ }
56
62
  pick() {
57
63
  this.menu?.pickItem(this.id());
58
64
  }
59
65
  hover() {
60
66
  this.menu?.hoverItem(this.id());
61
67
  }
68
+ registeredId = null;
69
+ constructor() {
70
+ effect(() => {
71
+ const id = this.id();
72
+ const menu = this.menu;
73
+ if (this.registeredId === null) return;
74
+ if (this.registeredId !== id) menu?.unregisterItem(this.registeredId);
75
+ menu?.registerItem({
76
+ id,
77
+ disabled: this.isDisabled()
78
+ });
79
+ this.registeredId = id;
80
+ });
81
+ }
62
82
  ngOnInit() {
83
+ this.registeredId = this.id();
63
84
  this.menu?.registerItem({
64
85
  id: this.id(),
65
- disabled: this.disabled()
86
+ disabled: this.isDisabled()
66
87
  });
67
88
  }
68
89
  ngOnDestroy() {
69
- this.menu?.unregisterItem(this.id());
90
+ if (this.registeredId === null) return;
91
+ this.menu?.unregisterItem(this.registeredId);
92
+ this.registeredId = null;
70
93
  }
71
94
  static ɵfac = i0.ɵɵngDeclareFactory({
72
95
  minVersion: "12.0.0",
@@ -114,7 +137,7 @@ var RiviniaMenuItem = class RiviniaMenuItem {
114
137
  "attr.role": "computedRole()",
115
138
  "attr.data-rv-id": "id()",
116
139
  "attr.tabindex": "tab()",
117
- "attr.aria-disabled": "disabled()",
140
+ "attr.aria-disabled": "isDisabled()",
118
141
  "attr.data-active": "active()",
119
142
  "attr.aria-checked": "checkedAttr()"
120
143
  }
@@ -136,7 +159,7 @@ i0.ɵɵngDeclareClassMetadata({
136
159
  "[attr.role]": "computedRole()",
137
160
  "[attr.data-rv-id]": "id()",
138
161
  "[attr.tabindex]": "tab()",
139
- "[attr.aria-disabled]": "disabled()",
162
+ "[attr.aria-disabled]": "isDisabled()",
140
163
  "[attr.data-active]": "active()",
141
164
  "[attr.aria-checked]": "checkedAttr()",
142
165
  "(click)": "pick()",
@@ -144,6 +167,7 @@ i0.ɵɵngDeclareClassMetadata({
144
167
  }
145
168
  }]
146
169
  }],
170
+ ctorParameters: () => [],
147
171
  propDecorators: {
148
172
  id: [{
149
173
  type: i0.Input,
@@ -174,12 +198,22 @@ i0.ɵɵngDeclareClassMetadata({
174
198
  var RiviniaMenuComponent = class RiviniaMenuComponent {
175
199
  menu = input(null, ...ngDevMode ? [{ debugName: "menu" }] : /* istanbul ignore next */ []);
176
200
  placement = input("bottom-start", ...ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []);
201
+ menuLabel = input(null, ...ngDevMode ? [{ debugName: "menuLabel" }] : /* istanbul ignore next */ []);
202
+ items = input(null, ...ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []);
203
+ panelClass = input(null, ...ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []);
204
+ itemClass = input(null, ...ngDevMode ? [{ debugName: "itemClass" }] : /* istanbul ignore next */ []);
177
205
  picked = output();
178
206
  opened = output();
179
207
  closed = output();
180
208
  owned = new RiviniaMenu();
181
209
  self = inject(ElementRef);
210
+ projectedItems = contentChildren(RiviniaMenuItem, {
211
+ ...ngDevMode ? { debugName: "projectedItems" } : /* istanbul ignore next */ {},
212
+ descendants: true
213
+ });
214
+ panelId = `rv-menu-panel-${++nextPanelId}`;
182
215
  focusPending = false;
216
+ pendingActivation = false;
183
217
  topLayerSupported = typeof HTMLElement !== "undefined" && "showPopover" in HTMLElement.prototype;
184
218
  positioned = signal(false, ...ngDevMode ? [{ debugName: "positioned" }] : /* istanbul ignore next */ []);
185
219
  flipped = signal(false, ...ngDevMode ? [{ debugName: "flipped" }] : /* istanbul ignore next */ []);
@@ -188,19 +222,38 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
188
222
  controller() {
189
223
  return this.menu() ?? this.owned;
190
224
  }
225
+ dataItems() {
226
+ return this.ambiguous() ? [] : this.items() ?? [];
227
+ }
228
+ checkedFor(id) {
229
+ const c = this.controller();
230
+ return typeof c.isSelected === "function" ? c.isSelected(id) : null;
231
+ }
232
+ ambiguous() {
233
+ return this.items() !== null && this.projectedItems().length > 0;
234
+ }
191
235
  registerItem(item) {
192
- this.controller().register(item);
236
+ const c = this.controller();
237
+ c.register(item);
238
+ if (this.pendingActivation && c.open() && c.activeId() === null && !item.disabled) {
239
+ c.activateFirst();
240
+ this.pendingActivation = false;
241
+ }
193
242
  }
194
243
  unregisterItem(id) {
195
244
  this.controller().unregister(id);
196
245
  }
197
246
  constructor() {
247
+ effect(() => {
248
+ if (!this.ambiguous()) return;
249
+ throw new Error("rv-menu: `[items]` cannot be combined with projected `rvMenuItem` children — pick one way to populate the panel.");
250
+ });
198
251
  effect((onCleanup) => {
199
252
  if (typeof window === "undefined") return;
200
253
  if (!this.controller().open()) return;
201
254
  const onPointer = (event) => {
202
255
  const host = this.self.nativeElement;
203
- if (host && !host.contains(event.target)) this.doClose(true);
256
+ if (host && !host.contains(event.target)) this.doClose(false);
204
257
  };
205
258
  document.addEventListener("pointerdown", onPointer);
206
259
  const onScroll = (event) => {
@@ -222,6 +275,7 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
222
275
  const trigger = this.triggerEl();
223
276
  trigger?.setAttribute("aria-haspopup", "menu");
224
277
  trigger?.setAttribute("aria-expanded", String(open));
278
+ trigger?.setAttribute("aria-controls", this.panelId);
225
279
  });
226
280
  afterRenderEffect(() => {
227
281
  if (this.controller().open() && !this.positioned()) this.showAndPosition();
@@ -279,10 +333,9 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
279
333
  }
280
334
  markOpening() {
281
335
  this.focusPending = true;
282
- if (typeof window !== "undefined" && this.topLayerSupported) {
283
- this.flipped.set(false);
284
- this.positioned.set(false);
285
- }
336
+ this.pendingActivation = true;
337
+ this.flipped.set(false);
338
+ this.positioned.set(false);
286
339
  }
287
340
  onTriggerClick() {
288
341
  const c = this.controller();
@@ -347,12 +400,23 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
347
400
  event.preventDefault();
348
401
  this.pickFocused();
349
402
  break;
350
- case "Tab":
403
+ case "Tab": {
404
+ event.preventDefault();
405
+ const backwards = event.shiftKey;
351
406
  this.doClose(false);
407
+ this.focusAdjacentToTrigger(backwards);
352
408
  break;
409
+ }
353
410
  default: this.onTypeahead(event);
354
411
  }
355
412
  }
413
+ resetTypeahead() {
414
+ this.typeBuffer = "";
415
+ if (this.typeTimer !== null) {
416
+ clearTimeout(this.typeTimer);
417
+ this.typeTimer = null;
418
+ }
419
+ }
356
420
  onTypeahead(event) {
357
421
  if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) return;
358
422
  const ch = event.key.toLocaleLowerCase();
@@ -381,8 +445,11 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
381
445
  return rows[0].id;
382
446
  }
383
447
  pickFocused() {
384
- const id = this.controller().pickActive();
448
+ const c = this.controller();
449
+ const id = c.pickActive();
385
450
  if (id === null) return;
451
+ const item = c.items().find((i) => i.id === id);
452
+ if (!item || item.disabled) return;
386
453
  this.doClose(false);
387
454
  this.picked.emit({ id });
388
455
  }
@@ -396,16 +463,22 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
396
463
  }
397
464
  doClose(restoreFocus) {
398
465
  const was = this.controller().open();
466
+ this.resetTypeahead();
399
467
  this.controller().closeMenu();
400
468
  if (was) this.closed.emit();
401
469
  if (restoreFocus) this.focusTrigger();
402
470
  }
403
471
  consumePendingFocus() {
404
- if (!this.controller().open() || !this.focusPending) return;
405
- this.focusPending = false;
472
+ if (!this.positioned() || !this.controller().open() || !this.focusPending) return;
406
473
  const root = this.self.nativeElement.querySelector(".rv-menu-panel");
474
+ if (!root) return;
407
475
  const active = this.activeDocumentElement();
408
- if (root && (!active || !root.contains(active))) this.focusActive();
476
+ if (active && root.contains(active)) {
477
+ this.focusPending = false;
478
+ return;
479
+ }
480
+ this.focusPending = false;
481
+ this.focusActive();
409
482
  }
410
483
  focusActive() {
411
484
  const id = this.controller().activeId();
@@ -428,6 +501,22 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
428
501
  if (!wrap) return null;
429
502
  return wrap.querySelector("button, a, [tabindex]") ?? wrap;
430
503
  }
504
+ focusAdjacentToTrigger(backwards) {
505
+ try {
506
+ const trigger = this.triggerEl();
507
+ const doc = trigger?.ownerDocument;
508
+ if (!trigger || !doc) return;
509
+ const stops = [...doc.querySelectorAll("a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])")].filter((el) => {
510
+ if (el.hasAttribute("disabled") || el.getAttribute("aria-hidden") === "true") return false;
511
+ if (el.closest(".rv-menu-panel")) return false;
512
+ return el.getClientRects().length > 0;
513
+ });
514
+ stops.sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
515
+ const at = stops.indexOf(trigger);
516
+ if (at === -1) return;
517
+ stops[backwards ? at - 1 : at + 1]?.focus({ preventScroll: true });
518
+ } catch {}
519
+ }
431
520
  focusTrigger() {
432
521
  try {
433
522
  this.triggerEl()?.focus({ preventScroll: true });
@@ -461,6 +550,34 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
461
550
  isSignal: true,
462
551
  isRequired: false,
463
552
  transformFunction: null
553
+ },
554
+ menuLabel: {
555
+ classPropertyName: "menuLabel",
556
+ publicName: "menuLabel",
557
+ isSignal: true,
558
+ isRequired: false,
559
+ transformFunction: null
560
+ },
561
+ items: {
562
+ classPropertyName: "items",
563
+ publicName: "items",
564
+ isSignal: true,
565
+ isRequired: false,
566
+ transformFunction: null
567
+ },
568
+ panelClass: {
569
+ classPropertyName: "panelClass",
570
+ publicName: "panelClass",
571
+ isSignal: true,
572
+ isRequired: false,
573
+ transformFunction: null
574
+ },
575
+ itemClass: {
576
+ classPropertyName: "itemClass",
577
+ publicName: "itemClass",
578
+ isSignal: true,
579
+ isRequired: false,
580
+ transformFunction: null
464
581
  }
465
582
  },
466
583
  outputs: {
@@ -468,16 +585,25 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
468
585
  opened: "opened",
469
586
  closed: "closed"
470
587
  },
588
+ queries: [{
589
+ propertyName: "projectedItems",
590
+ predicate: RiviniaMenuItem,
591
+ descendants: true,
592
+ isSignal: true
593
+ }],
471
594
  ngImport: i0,
472
595
  template: `
473
596
  <div class="rv-menu-trigger" (click)="onTriggerClick()" (keydown)="onTriggerKey($event)">
474
- <ng-content select="[rv-menu-trigger]"></ng-content>
597
+ <ng-content select="[rv-menu-trigger], [rvMenuTrigger]"></ng-content>
475
598
  </div>
476
599
  @if (controller().open()) {
477
600
  <div
478
601
  #panel
479
602
  class="rv-menu-panel"
603
+ [class]="panelClass()"
480
604
  role="menu"
605
+ [attr.id]="panelId"
606
+ [attr.aria-label]="menuLabel()"
481
607
  [attr.popover]="topLayerSupported ? 'manual' : null"
482
608
  [attr.data-placement]="placement()"
483
609
  [attr.data-flipped]="flipped()"
@@ -485,12 +611,35 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
485
611
  [attr.data-top-layer]="topLayerSupported"
486
612
  (keydown)="onPanelKey($event)"
487
613
  >
488
- <ng-content select="[rv-menu-panel]"></ng-content>
614
+ @if (dataItems().length > 0) {
615
+ @for (item of dataItems(); track item.id) {
616
+ <button
617
+ rvMenuItem
618
+ [id]="item.id"
619
+ [disabled]="item.disabled ?? false"
620
+ [checked]="checkedFor(item.id)"
621
+ [class]="itemClass()"
622
+ >
623
+ {{ item.label }}
624
+ </button>
625
+ }
626
+ }
627
+ <ng-content select="[rv-menu-panel], [rvMenuPanel]"></ng-content>
489
628
  </div>
490
629
  }
491
630
  `,
492
631
  isInline: true,
493
- styles: [".rv-menu-trigger{display:inline-flex;align-items:center}.rv-menu-panel{display:flex;flex-direction:column;gap:2px}.rv-menu-panel hr{border:0;border-top:1px solid var(--rv-menu-border);margin:6px 4px}rv-menu{position:relative;display:inline-block}.rv-menu-panel[data-top-layer=true]{position:fixed;inset:auto;margin:0}.rv-menu-panel[data-positioned=false]{animation:none}.rv-menu-panel[data-top-layer=true][data-placement^=bottom][data-flipped=true]{transform-origin:bottom center}.rv-menu-panel[data-top-layer=true][data-placement^=top][data-flipped=true]{transform-origin:top center}.rv-menu-panel{--rv-menu-bg: #ffffff;--rv-menu-fg: #111111;--rv-menu-muted: #555555;--rv-menu-border: #e0e0e0;--rv-menu-radius: 12px;--rv-menu-shadow: 0 16px 48px rgba(0, 0, 0, .22);--rv-menu-pad: 6px;--rv-menu-width: 16rem;--rv-menu-font: system-ui, -apple-system, \"Segoe UI\", sans-serif;position:absolute;z-index:var(--rv-menu-z, 500);min-width:var(--rv-menu-width);max-width:calc(100dvw - 2rem);max-height:min(20rem,calc(100dvh - 2rem));overflow-y:auto;background:var(--rv-menu-bg);color:var(--rv-menu-fg);font-family:var(--rv-menu-font);border:1px solid var(--rv-menu-border);border-radius:var(--rv-menu-radius);box-shadow:var(--rv-menu-shadow);padding:var(--rv-menu-pad)}@media(prefers-color-scheme:dark){.rv-menu-panel{--rv-menu-bg: #161616;--rv-menu-fg: #f2f2f2;--rv-menu-muted: #a8a8a8;--rv-menu-border: #333333}}.rv-menu-panel[data-placement=bottom-start]{inset-block-start:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=bottom-end]{inset-block-start:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel[data-placement=top-start]{inset-block-end:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=top-end]{inset-block-end:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel [rvmenuitem]{display:flex;align-items:center;gap:10px;width:100%;min-height:40px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;font-size:14px;line-height:1.4;text-align:start;padding:8px 10px;cursor:pointer}.rv-menu-panel [rvmenuitem][data-active=true]{background:color-mix(in srgb,currentColor 10%,transparent)}.rv-menu-panel [rvmenuitem]:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.rv-menu-panel [rvmenuitem]:focus-visible{outline:2px solid currentColor;outline-offset:-2px}.rv-menu-panel [rvmenuitem][aria-disabled=true]{opacity:.45;cursor:default}.rv-menu-panel [rvmenuitem] svg,.rv-menu-panel [rvmenuitem] [data-icon]{width:16px;height:16px;flex:none;color:var(--rv-menu-muted)}.rv-menu-panel [rvmenuitem][data-active=true] svg,.rv-menu-panel [rvmenuitem][data-active=true] [data-icon]{color:inherit}.rv-menu-panel{--rv-menu-duration: .16s;--rv-menu-easing: cubic-bezier(.16, 1, .3, 1);--rv-menu-from-y: -6px;transform-origin:top center;animation:rv-menu-in var(--rv-menu-duration) var(--rv-menu-easing)}.rv-menu-panel[data-placement=top-start],.rv-menu-panel[data-placement=top-end]{--rv-menu-from-y: 6px;transform-origin:bottom center}@media(prefers-reduced-motion:reduce){.rv-menu-panel{animation:none}}@keyframes rv-menu-in{0%{opacity:0;transform:translateY(var(--rv-menu-from-y)) scale(.96)}}\n"],
632
+ styles: [".rv-menu-trigger{display:inline-flex;align-items:center}.rv-menu-panel{display:flex;flex-direction:column;gap:2px}.rv-menu-panel hr{border:0;border-top:1px solid var(--rv-menu-border, #e0e0e0);margin:6px 4px}rv-menu{position:relative;display:inline-block}.rv-menu-panel[data-top-layer=true]{position:fixed;inset:auto;margin:0}.rv-menu-panel[data-positioned=false]{animation:none}.rv-menu-panel[data-top-layer=true][data-placement^=bottom][data-flipped=true]{transform-origin:bottom center}.rv-menu-panel[data-top-layer=true][data-placement^=top][data-flipped=true]{transform-origin:top center}.rv-menu-panel{position:absolute;z-index:var(--rv-menu-z, 500);min-width:var(--rv-menu-width, 16rem);max-width:calc(100dvw - 2rem);max-height:min(20rem,calc(100dvh - 2rem));overflow-y:auto;background:var(--rv-menu-bg, #ffffff);color:var(--rv-menu-fg, #111111);font-family:var(--rv-menu-font, system-ui, -apple-system, \"Segoe UI\", sans-serif);border:1px solid var(--rv-menu-border, #e0e0e0);border-radius:var(--rv-menu-radius, 12px);box-shadow:var(--rv-menu-shadow, 0 16px 48px rgba(0, 0, 0, .22));padding:var(--rv-menu-pad, 6px)}.rv-menu-panel[data-placement=bottom-start]{inset-block-start:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=bottom-end]{inset-block-start:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel[data-placement=top-start]{inset-block-end:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=top-end]{inset-block-end:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel [rvmenuitem]{display:flex;align-items:center;gap:10px;width:100%;min-height:40px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;font-size:14px;line-height:1.4;text-align:start;padding:8px 10px;cursor:pointer}.rv-menu-panel [rvmenuitem][data-active=true]{background:color-mix(in srgb,currentColor 10%,transparent)}.rv-menu-panel [rvmenuitem]:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.rv-menu-panel [rvmenuitem]:focus-visible{outline:2px solid currentColor;outline-offset:-2px}.rv-menu-panel [rvmenuitem][aria-disabled=true]{opacity:.45;cursor:default}.rv-menu-panel [rvmenuitem] svg,.rv-menu-panel [rvmenuitem] [data-icon]{width:16px;height:16px;flex:none;color:var(--rv-menu-muted)}.rv-menu-panel [rvmenuitem][data-active=true] svg,.rv-menu-panel [rvmenuitem][data-active=true] [data-icon]{color:inherit}.rv-menu-panel{transform-origin:top center;animation:rv-menu-in var(--rv-menu-duration, .16s) var(--rv-menu-easing, cubic-bezier(.16, 1, .3, 1))}.rv-menu-panel[data-placement=top-start],.rv-menu-panel[data-placement=top-end]{--rv-menu-from-y: 6px;transform-origin:bottom center}@media(prefers-reduced-motion:reduce){.rv-menu-panel{animation:none}}@keyframes rv-menu-in{0%{opacity:0;transform:translateY(var(--rv-menu-from-y, -6px)) scale(.96)}}\n"],
633
+ dependencies: [{
634
+ kind: "directive",
635
+ type: RiviniaMenuItem,
636
+ selector: "[rvMenuItem]",
637
+ inputs: [
638
+ "id",
639
+ "disabled",
640
+ "checked"
641
+ ]
642
+ }],
494
643
  encapsulation: i0.ViewEncapsulation.None
495
644
  });
496
645
  };
@@ -505,15 +654,19 @@ i0.ɵɵngDeclareClassMetadata({
505
654
  selector: "rv-menu",
506
655
  standalone: true,
507
656
  encapsulation: ViewEncapsulation.None,
657
+ imports: [RiviniaMenuItem],
508
658
  template: `
509
659
  <div class="rv-menu-trigger" (click)="onTriggerClick()" (keydown)="onTriggerKey($event)">
510
- <ng-content select="[rv-menu-trigger]"></ng-content>
660
+ <ng-content select="[rv-menu-trigger], [rvMenuTrigger]"></ng-content>
511
661
  </div>
512
662
  @if (controller().open()) {
513
663
  <div
514
664
  #panel
515
665
  class="rv-menu-panel"
666
+ [class]="panelClass()"
516
667
  role="menu"
668
+ [attr.id]="panelId"
669
+ [attr.aria-label]="menuLabel()"
517
670
  [attr.popover]="topLayerSupported ? 'manual' : null"
518
671
  [attr.data-placement]="placement()"
519
672
  [attr.data-flipped]="flipped()"
@@ -521,11 +674,24 @@ i0.ɵɵngDeclareClassMetadata({
521
674
  [attr.data-top-layer]="topLayerSupported"
522
675
  (keydown)="onPanelKey($event)"
523
676
  >
524
- <ng-content select="[rv-menu-panel]"></ng-content>
677
+ @if (dataItems().length > 0) {
678
+ @for (item of dataItems(); track item.id) {
679
+ <button
680
+ rvMenuItem
681
+ [id]="item.id"
682
+ [disabled]="item.disabled ?? false"
683
+ [checked]="checkedFor(item.id)"
684
+ [class]="itemClass()"
685
+ >
686
+ {{ item.label }}
687
+ </button>
688
+ }
689
+ }
690
+ <ng-content select="[rv-menu-panel], [rvMenuPanel]"></ng-content>
525
691
  </div>
526
692
  }
527
693
  `,
528
- styles: [".rv-menu-trigger{display:inline-flex;align-items:center}.rv-menu-panel{display:flex;flex-direction:column;gap:2px}.rv-menu-panel hr{border:0;border-top:1px solid var(--rv-menu-border);margin:6px 4px}rv-menu{position:relative;display:inline-block}.rv-menu-panel[data-top-layer=true]{position:fixed;inset:auto;margin:0}.rv-menu-panel[data-positioned=false]{animation:none}.rv-menu-panel[data-top-layer=true][data-placement^=bottom][data-flipped=true]{transform-origin:bottom center}.rv-menu-panel[data-top-layer=true][data-placement^=top][data-flipped=true]{transform-origin:top center}.rv-menu-panel{--rv-menu-bg: #ffffff;--rv-menu-fg: #111111;--rv-menu-muted: #555555;--rv-menu-border: #e0e0e0;--rv-menu-radius: 12px;--rv-menu-shadow: 0 16px 48px rgba(0, 0, 0, .22);--rv-menu-pad: 6px;--rv-menu-width: 16rem;--rv-menu-font: system-ui, -apple-system, \"Segoe UI\", sans-serif;position:absolute;z-index:var(--rv-menu-z, 500);min-width:var(--rv-menu-width);max-width:calc(100dvw - 2rem);max-height:min(20rem,calc(100dvh - 2rem));overflow-y:auto;background:var(--rv-menu-bg);color:var(--rv-menu-fg);font-family:var(--rv-menu-font);border:1px solid var(--rv-menu-border);border-radius:var(--rv-menu-radius);box-shadow:var(--rv-menu-shadow);padding:var(--rv-menu-pad)}@media(prefers-color-scheme:dark){.rv-menu-panel{--rv-menu-bg: #161616;--rv-menu-fg: #f2f2f2;--rv-menu-muted: #a8a8a8;--rv-menu-border: #333333}}.rv-menu-panel[data-placement=bottom-start]{inset-block-start:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=bottom-end]{inset-block-start:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel[data-placement=top-start]{inset-block-end:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=top-end]{inset-block-end:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel [rvmenuitem]{display:flex;align-items:center;gap:10px;width:100%;min-height:40px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;font-size:14px;line-height:1.4;text-align:start;padding:8px 10px;cursor:pointer}.rv-menu-panel [rvmenuitem][data-active=true]{background:color-mix(in srgb,currentColor 10%,transparent)}.rv-menu-panel [rvmenuitem]:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.rv-menu-panel [rvmenuitem]:focus-visible{outline:2px solid currentColor;outline-offset:-2px}.rv-menu-panel [rvmenuitem][aria-disabled=true]{opacity:.45;cursor:default}.rv-menu-panel [rvmenuitem] svg,.rv-menu-panel [rvmenuitem] [data-icon]{width:16px;height:16px;flex:none;color:var(--rv-menu-muted)}.rv-menu-panel [rvmenuitem][data-active=true] svg,.rv-menu-panel [rvmenuitem][data-active=true] [data-icon]{color:inherit}.rv-menu-panel{--rv-menu-duration: .16s;--rv-menu-easing: cubic-bezier(.16, 1, .3, 1);--rv-menu-from-y: -6px;transform-origin:top center;animation:rv-menu-in var(--rv-menu-duration) var(--rv-menu-easing)}.rv-menu-panel[data-placement=top-start],.rv-menu-panel[data-placement=top-end]{--rv-menu-from-y: 6px;transform-origin:bottom center}@media(prefers-reduced-motion:reduce){.rv-menu-panel{animation:none}}@keyframes rv-menu-in{0%{opacity:0;transform:translateY(var(--rv-menu-from-y)) scale(.96)}}\n"]
694
+ styles: [".rv-menu-trigger{display:inline-flex;align-items:center}.rv-menu-panel{display:flex;flex-direction:column;gap:2px}.rv-menu-panel hr{border:0;border-top:1px solid var(--rv-menu-border, #e0e0e0);margin:6px 4px}rv-menu{position:relative;display:inline-block}.rv-menu-panel[data-top-layer=true]{position:fixed;inset:auto;margin:0}.rv-menu-panel[data-positioned=false]{animation:none}.rv-menu-panel[data-top-layer=true][data-placement^=bottom][data-flipped=true]{transform-origin:bottom center}.rv-menu-panel[data-top-layer=true][data-placement^=top][data-flipped=true]{transform-origin:top center}.rv-menu-panel{position:absolute;z-index:var(--rv-menu-z, 500);min-width:var(--rv-menu-width, 16rem);max-width:calc(100dvw - 2rem);max-height:min(20rem,calc(100dvh - 2rem));overflow-y:auto;background:var(--rv-menu-bg, #ffffff);color:var(--rv-menu-fg, #111111);font-family:var(--rv-menu-font, system-ui, -apple-system, \"Segoe UI\", sans-serif);border:1px solid var(--rv-menu-border, #e0e0e0);border-radius:var(--rv-menu-radius, 12px);box-shadow:var(--rv-menu-shadow, 0 16px 48px rgba(0, 0, 0, .22));padding:var(--rv-menu-pad, 6px)}.rv-menu-panel[data-placement=bottom-start]{inset-block-start:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=bottom-end]{inset-block-start:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel[data-placement=top-start]{inset-block-end:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=top-end]{inset-block-end:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel [rvmenuitem]{display:flex;align-items:center;gap:10px;width:100%;min-height:40px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;font-size:14px;line-height:1.4;text-align:start;padding:8px 10px;cursor:pointer}.rv-menu-panel [rvmenuitem][data-active=true]{background:color-mix(in srgb,currentColor 10%,transparent)}.rv-menu-panel [rvmenuitem]:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.rv-menu-panel [rvmenuitem]:focus-visible{outline:2px solid currentColor;outline-offset:-2px}.rv-menu-panel [rvmenuitem][aria-disabled=true]{opacity:.45;cursor:default}.rv-menu-panel [rvmenuitem] svg,.rv-menu-panel [rvmenuitem] [data-icon]{width:16px;height:16px;flex:none;color:var(--rv-menu-muted)}.rv-menu-panel [rvmenuitem][data-active=true] svg,.rv-menu-panel [rvmenuitem][data-active=true] [data-icon]{color:inherit}.rv-menu-panel{transform-origin:top center;animation:rv-menu-in var(--rv-menu-duration, .16s) var(--rv-menu-easing, cubic-bezier(.16, 1, .3, 1))}.rv-menu-panel[data-placement=top-start],.rv-menu-panel[data-placement=top-end]{--rv-menu-from-y: 6px;transform-origin:bottom center}@media(prefers-reduced-motion:reduce){.rv-menu-panel{animation:none}}@keyframes rv-menu-in{0%{opacity:0;transform:translateY(var(--rv-menu-from-y, -6px)) scale(.96)}}\n"]
529
695
  }]
530
696
  }],
531
697
  ctorParameters: () => [],
@@ -546,6 +712,38 @@ i0.ɵɵngDeclareClassMetadata({
546
712
  required: false
547
713
  }]
548
714
  }],
715
+ menuLabel: [{
716
+ type: i0.Input,
717
+ args: [{
718
+ isSignal: true,
719
+ alias: "menuLabel",
720
+ required: false
721
+ }]
722
+ }],
723
+ items: [{
724
+ type: i0.Input,
725
+ args: [{
726
+ isSignal: true,
727
+ alias: "items",
728
+ required: false
729
+ }]
730
+ }],
731
+ panelClass: [{
732
+ type: i0.Input,
733
+ args: [{
734
+ isSignal: true,
735
+ alias: "panelClass",
736
+ required: false
737
+ }]
738
+ }],
739
+ itemClass: [{
740
+ type: i0.Input,
741
+ args: [{
742
+ isSignal: true,
743
+ alias: "itemClass",
744
+ required: false
745
+ }]
746
+ }],
549
747
  picked: [{
550
748
  type: i0.Output,
551
749
  args: ["picked"]
@@ -557,6 +755,13 @@ i0.ɵɵngDeclareClassMetadata({
557
755
  closed: [{
558
756
  type: i0.Output,
559
757
  args: ["closed"]
758
+ }],
759
+ projectedItems: [{
760
+ type: i0.ContentChildren,
761
+ args: [i0.forwardRef(() => RiviniaMenuItem), {
762
+ descendants: true,
763
+ isSignal: true
764
+ }]
560
765
  }]
561
766
  }
562
767
  });