@rivinia/menu 0.3.1 → 0.3.2
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/README.md
CHANGED
|
@@ -250,11 +250,12 @@ yourself from `picked`, or drive it with a controller.
|
|
|
250
250
|
|
|
251
251
|
### `rvMenuItem` (directive)
|
|
252
252
|
|
|
253
|
-
| Input | Type | Default | Required | Purpose
|
|
254
|
-
| ---------- | ----------------- | ------- | -------- |
|
|
255
|
-
| `id` | `string` | — | **yes** | Identity. Also emitted in `picked`, and used for `data-rv-id`.
|
|
256
|
-
| `disabled` | `boolean` | `false` | no | Skipped by keyboard navigation, refused on click, `aria-disabled="true"`.
|
|
257
|
-
| `checked` | `boolean \| null` | `null` | no | `null` → `role="menuitem"`. `true/false` → `role="menuitemcheckbox"` + `aria-checked`.
|
|
253
|
+
| Input | Type | Default | Required | Purpose |
|
|
254
|
+
| ---------- | ----------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
255
|
+
| `id` | `string` | — | **yes** | Identity. Also emitted in `picked`, and used for `data-rv-id`. |
|
|
256
|
+
| `disabled` | `boolean` | `false` | no | Skipped by keyboard navigation, refused on click, `aria-disabled="true"`. |
|
|
257
|
+
| `checked` | `boolean \| null` | `null` | no | `null` → `role="menuitem"`. `true/false` → `role="menuitemcheckbox"` + `aria-checked`. |
|
|
258
|
+
| `label` | `string \| null` | `null` | no | Explicit typeahead label. When set, typeahead matches on this instead of the row's `textContent`. Defaults to `null` → falls back to `textContent`. |
|
|
258
259
|
|
|
259
260
|
Items register on init, **stay in sync when `id` or `disabled` change**, and
|
|
260
261
|
unregister on destroy — no manual bookkeeping, and `@for`-driven collections
|
|
@@ -570,8 +571,8 @@ Other limitations:
|
|
|
570
571
|
- Outside scroll closes the menu rather than re-positioning it.
|
|
571
572
|
- No `ControlValueAccessor` — wire forms yourself (e.g. `[value]` + a
|
|
572
573
|
`(selectionChange)`-style handler on your own component).
|
|
573
|
-
- Typeahead matches
|
|
574
|
-
its visible text.
|
|
574
|
+
- Typeahead matches the item's `label` input when set, otherwise its
|
|
575
|
+
`textContent`. Keep an item's accessible name aligned with its visible text.
|
|
575
576
|
- Very large lists (hundreds of items) are not virtualized.
|
|
576
577
|
|
|
577
578
|
## 20. SSR / hydration
|
|
@@ -39,6 +39,7 @@ var RiviniaMenuItem = class RiviniaMenuItem {
|
|
|
39
39
|
transform: (v) => v === "" || v === true
|
|
40
40
|
});
|
|
41
41
|
checked = input(null, ...ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []);
|
|
42
|
+
label = input(null, ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
|
|
42
43
|
host = inject(ElementRef, { optional: true });
|
|
43
44
|
menu = inject(RiviniaMenuComponent, {
|
|
44
45
|
optional: true,
|
|
@@ -126,6 +127,13 @@ var RiviniaMenuItem = class RiviniaMenuItem {
|
|
|
126
127
|
isSignal: true,
|
|
127
128
|
isRequired: false,
|
|
128
129
|
transformFunction: null
|
|
130
|
+
},
|
|
131
|
+
label: {
|
|
132
|
+
classPropertyName: "label",
|
|
133
|
+
publicName: "label",
|
|
134
|
+
isSignal: true,
|
|
135
|
+
isRequired: false,
|
|
136
|
+
transformFunction: null
|
|
129
137
|
}
|
|
130
138
|
},
|
|
131
139
|
host: {
|
|
@@ -136,6 +144,7 @@ var RiviniaMenuItem = class RiviniaMenuItem {
|
|
|
136
144
|
properties: {
|
|
137
145
|
"attr.role": "computedRole()",
|
|
138
146
|
"attr.data-rv-id": "id()",
|
|
147
|
+
"attr.data-rv-label": "label()",
|
|
139
148
|
"attr.tabindex": "tab()",
|
|
140
149
|
"attr.aria-disabled": "isDisabled()",
|
|
141
150
|
"attr.data-active": "active()",
|
|
@@ -158,6 +167,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
158
167
|
host: {
|
|
159
168
|
"[attr.role]": "computedRole()",
|
|
160
169
|
"[attr.data-rv-id]": "id()",
|
|
170
|
+
"[attr.data-rv-label]": "label()",
|
|
161
171
|
"[attr.tabindex]": "tab()",
|
|
162
172
|
"[attr.aria-disabled]": "isDisabled()",
|
|
163
173
|
"[attr.data-active]": "active()",
|
|
@@ -192,6 +202,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
192
202
|
alias: "checked",
|
|
193
203
|
required: false
|
|
194
204
|
}]
|
|
205
|
+
}],
|
|
206
|
+
label: [{
|
|
207
|
+
type: i0.Input,
|
|
208
|
+
args: [{
|
|
209
|
+
isSignal: true,
|
|
210
|
+
alias: "label",
|
|
211
|
+
required: false
|
|
212
|
+
}]
|
|
195
213
|
}]
|
|
196
214
|
}
|
|
197
215
|
});
|
|
@@ -435,7 +453,7 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
|
|
|
435
453
|
const enabled = new Set(this.controller().items().filter((i) => !i.disabled).map((i) => i.id));
|
|
436
454
|
const rows = [...root.querySelectorAll("[data-rv-id]")].map((el) => ({
|
|
437
455
|
id: el.getAttribute("data-rv-id") ?? "",
|
|
438
|
-
text: (el.textContent ?? "").trim().toLocaleLowerCase()
|
|
456
|
+
text: (el.getAttribute("data-rv-label") ?? el.textContent ?? "").trim().toLocaleLowerCase()
|
|
439
457
|
})).filter((r) => enabled.has(r.id) && r.text.startsWith(buffer));
|
|
440
458
|
if (rows.length === 0) return null;
|
|
441
459
|
if (new Set(buffer).size === 1) {
|
|
@@ -637,7 +655,8 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
|
|
|
637
655
|
inputs: [
|
|
638
656
|
"id",
|
|
639
657
|
"disabled",
|
|
640
|
-
"checked"
|
|
658
|
+
"checked",
|
|
659
|
+
"label"
|
|
641
660
|
]
|
|
642
661
|
}],
|
|
643
662
|
encapsulation: i0.ViewEncapsulation.None
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rivinia-menu-ui.mjs","names":[],"sources":["../../../projects/menu/ui/src/lib/menu-position.ts","../../../projects/menu/ui/src/lib/menu-panel.ts"],"sourcesContent":["import type { RiviniaMenuPlacement } from '@rivinia/menu';\n\n/** Trigger rect in viewport coordinates. */\nexport interface MenuTriggerRect {\n readonly left: number;\n readonly top: number;\n readonly width: number;\n readonly height: number;\n}\n\n/** Measured panel size. */\nexport interface MenuPanelSize {\n readonly width: number;\n readonly height: number;\n}\n\n/** Viewport size. */\nexport interface MenuViewport {\n readonly width: number;\n readonly height: number;\n}\n\n/** Fixed-position coordinates plus whether the vertical side flipped. */\nexport interface MenuCoordinates {\n readonly left: number;\n readonly top: number;\n readonly flipped: boolean;\n}\n\n/** Gap between trigger and panel. */\nexport const MENU_GAP = 6;\n/** Minimum distance kept from viewport edges. */\nconst EDGE = 8;\n\n/**\n * Pure top-layer position for a menu panel: anchors to the trigger per\n * `placement` (logical, RTL-aware), flips to the opposite vertical side when\n * the preferred side overflows but the other fits, then clamps inside the\n * viewport. Zero DOM access — unit-testable with fake rects; the component\n * only measures and applies.\n */\nexport function computeMenuPosition(\n trigger: MenuTriggerRect,\n panel: MenuPanelSize,\n placement: RiviniaMenuPlacement,\n viewport: MenuViewport,\n rtl: boolean,\n gap = MENU_GAP,\n): MenuCoordinates {\n const [vertical, horizontal] = placement.split('-');\n const below = vertical !== 'top';\n // Logical start: left edge in LTR, right edge in RTL.\n const alignLeft = (horizontal !== 'end') !== rtl;\n\n let left = alignLeft ? trigger.left : trigger.left + trigger.width - panel.width;\n let top = below ? trigger.top + trigger.height + gap : trigger.top - gap - panel.height;\n let flipped = false;\n\n const fitsBelow = trigger.top + trigger.height + gap + panel.height <= viewport.height - EDGE;\n const fitsAbove = trigger.top - gap - panel.height >= EDGE;\n if (below && top + panel.height > viewport.height - EDGE && fitsAbove) {\n top = trigger.top - gap - panel.height;\n flipped = true;\n } else if (!below && top < EDGE && fitsBelow) {\n top = trigger.top + trigger.height + gap;\n flipped = true;\n }\n\n const fitsLeft = trigger.left + trigger.width - panel.width >= EDGE;\n const fitsRight = trigger.left + panel.width <= viewport.width - EDGE;\n if (alignLeft && left + panel.width > viewport.width - EDGE && fitsLeft) {\n left = trigger.left + trigger.width - panel.width;\n } else if (!alignLeft && left < EDGE && fitsRight) {\n left = trigger.left;\n }\n\n left = Math.min(Math.max(left, EDGE), Math.max(EDGE, viewport.width - panel.width - EDGE));\n top = Math.min(Math.max(top, EDGE), Math.max(EDGE, viewport.height - panel.height - EDGE));\n return { left: Math.round(left), top: Math.round(top), flipped };\n}\n","import {\n Component,\n Directive,\n ElementRef,\n ViewEncapsulation,\n afterRenderEffect,\n computed,\n contentChildren,\n effect,\n inject,\n input,\n output,\n signal,\n} from '@angular/core';\nimport type { RiviniaMenuController, RiviniaMenuItemData } from '@rivinia/menu';\nimport { RiviniaMenu } from '@rivinia/menu';\nimport type { RiviniaMenuUiPlacement } from './ui-menu-types';\nimport { computeMenuPosition } from './menu-position';\n\n/** Payload of {@link RiviniaMenuComponent.picked}. */\nexport interface MenuPick {\n readonly id: string;\n}\n\n/** Per-instance panel id counter (client-side; SSR renders closed). */\nlet nextPanelId = 0;\n\n/**\n * Marker + behavior for one navigable row. Registers itself with the\n * enclosing menu on init (unregisters on destroy), exposes `data-rv-id`\n * for focus targeting, and keeps `tabindex`/`aria-disabled` in sync.\n * Non-`rvMenuItem` content (headers, dividers, badges) is skipped by\n * keyboard navigation automatically.\n */\n@Directive({\n selector: '[rvMenuItem]',\n standalone: true,\n host: {\n '[attr.role]': 'computedRole()',\n '[attr.data-rv-id]': 'id()',\n '[attr.tabindex]': 'tab()',\n '[attr.aria-disabled]': 'isDisabled()',\n '[attr.data-active]': 'active()',\n '[attr.aria-checked]': 'checkedAttr()',\n '(click)': 'pick()',\n '(pointerenter)': 'hover()',\n },\n})\nexport class RiviniaMenuItem {\n readonly id = input.required<string>();\n readonly disabled = input(false, {\n transform: (v: boolean | string) => v === '' || v === true,\n });\n /**\n * Tri-state selection (e.g. multi-select options). `null` (default) renders\n * a plain `menuitem`; a boolean renders `menuitemcheckbox` + `aria-checked`\n * so screen readers announce the state (the visual check alone is silent).\n */\n readonly checked = input<boolean | null>(null);\n\n private readonly host = inject(ElementRef<HTMLElement>, { optional: true });\n private readonly menu = inject(RiviniaMenuComponent, { optional: true, skipSelf: true });\n private readonly active = computed(() => this.menu?.controller().activeId() === this.id());\n\n protected tab(): string {\n return this.active() ? '0' : '-1';\n }\n\n protected computedRole(): string {\n return this.checked() === null ? 'menuitem' : 'menuitemcheckbox';\n }\n\n protected checkedAttr(): string | null {\n const checked = this.checked();\n return checked === null ? null : String(checked);\n }\n\n /**\n * `disabled` collides with the native `<button disabled>` property, so a\n * static attribute on a native button element can land on the DOM instead\n * of this input (measured: `<button rvMenuItem disabled>` reported\n * `aria-disabled=\"false\"`). Accept both so the two spellings agree.\n */\n private isDisabled(): boolean {\n if (this.disabled()) return true;\n return this.host?.nativeElement?.disabled === true;\n }\n\n /** Mouse pick: disabled items are ignored (controller refuses them). */\n protected pick(): void {\n this.menu?.pickItem(this.id());\n }\n\n /** Mouse hover follows keyboard focus so highlight tracks the pointer. */\n protected hover(): void {\n this.menu?.hoverItem(this.id());\n }\n\n /** Id currently present in the registry, so renames can be de-registered. */\n private registeredId: string | null = null;\n\n constructor() {\n // `[disabled]` and `id` are ordinary bindings that change at runtime.\n // Registering once in ngOnInit left the registry stale — measured:\n // toggling `[disabled]` on a focused row still let Enter activate it.\n //\n // The initial registration stays in ngOnInit (synchronous) because an\n // effect-based first pass lands after the opening change-detection tick,\n // which raced the menu's own open and produced intermittently empty\n // panels. The effect only handles SUBSEQUENT changes.\n effect(() => {\n const id = this.id();\n const menu = this.menu;\n if (this.registeredId === null) return;\n if (this.registeredId !== id) menu?.unregisterItem(this.registeredId);\n // Re-register unconditionally: covers a `disabled` flip as well as an\n // id rename (register is idempotent by id).\n menu?.registerItem({ id, disabled: this.isDisabled() });\n this.registeredId = id;\n });\n }\n\n ngOnInit(): void {\n this.registeredId = this.id();\n this.menu?.registerItem({ id: this.id(), disabled: this.isDisabled() });\n }\n\n ngOnDestroy(): void {\n if (this.registeredId === null) return;\n this.menu?.unregisterItem(this.registeredId);\n this.registeredId = null;\n }\n}\n\n/**\n * Ready-made dropdown menu. Binds a {@link RiviniaMenu} controller (or owns\n * one when omitted) and projects trigger + panel:\n *\n * ```html\n * <rv-menu [menu]=\"menu\" [placement]=\"'bottom-start'\" (picked)=\"go($event)\">\n * <button rv-menu-trigger><img src=\"avatar.png\" alt=\"Account\" /></button>\n * <div rv-menu-panel>\n * <div class=\"head\">Sophie · sophie@ui.live</div>\n * <button rvMenuItem id=\"profile\">Profile</button>\n * <button rvMenuItem id=\"logout\">Sign out</button>\n * </div>\n * </rv-menu>\n * ```\n *\n * Keyboard: Enter/Space/ArrowDown opens; Escape closes and refocuses the\n * trigger; arrows/Home/End move through enabled items; Tab closes and moves\n * on (non-modal by design). Outside pointer-down closes. SSR renders closed.\n */\n@Component({\n selector: 'rv-menu',\n standalone: true,\n // Deliberately unencapsulated: projected item buttons belong to the\n // consumer's template, and emulated descendant selectors never match\n // projected content (measured in Chrome: `display` stayed `inline-block`).\n // Every rule below is scoped under `.rv-menu-panel` / `.rv-menu-trigger`,\n // so nothing leaks into application styles.\n encapsulation: ViewEncapsulation.None,\n // The declarative `[items]` mode renders its own rows through the SAME\n // directive consumers use, so registration, keyboard and click handling have\n // exactly one implementation.\n imports: [RiviniaMenuItem],\n template: `\n <div class=\"rv-menu-trigger\" (click)=\"onTriggerClick()\" (keydown)=\"onTriggerKey($event)\">\n <ng-content select=\"[rv-menu-trigger], [rvMenuTrigger]\"></ng-content>\n </div>\n @if (controller().open()) {\n <div\n #panel\n class=\"rv-menu-panel\"\n [class]=\"panelClass()\"\n role=\"menu\"\n [attr.id]=\"panelId\"\n [attr.aria-label]=\"menuLabel()\"\n [attr.popover]=\"topLayerSupported ? 'manual' : null\"\n [attr.data-placement]=\"placement()\"\n [attr.data-flipped]=\"flipped()\"\n [attr.data-positioned]=\"positioned()\"\n [attr.data-top-layer]=\"topLayerSupported\"\n (keydown)=\"onPanelKey($event)\"\n >\n @if (dataItems().length > 0) {\n @for (item of dataItems(); track item.id) {\n <button\n rvMenuItem\n [id]=\"item.id\"\n [disabled]=\"item.disabled ?? false\"\n [checked]=\"checkedFor(item.id)\"\n [class]=\"itemClass()\"\n >\n {{ item.label }}\n </button>\n }\n }\n <ng-content select=\"[rv-menu-panel], [rvMenuPanel]\"></ng-content>\n </div>\n }\n `,\n styles: [\n `\n .rv-menu-trigger {\n display: inline-flex;\n align-items: center;\n }\n .rv-menu-panel {\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n .rv-menu-panel hr {\n border: 0;\n border-top: 1px solid var(--rv-menu-border, #e0e0e0);\n margin: 6px 4px;\n }\n /* NOTE: the :host pseudo-class does not exist without encapsulation,\n so the host element is styled directly (still rv-prefixed).\n Relative positioning anchors the no-popover fallback panel. */\n rv-menu {\n position: relative;\n display: inline-block;\n }\n /* Top-layer mode (popover-capable browsers): the panel escapes every\n clipping ancestor and stacking context. Coordinates come from the\n component (fixed + measured + flipped); the absolute placement\n classes below stay as the no-popover fallback. */\n .rv-menu-panel[data-top-layer='true'] {\n position: fixed;\n inset: auto;\n margin: 0;\n }\n .rv-menu-panel[data-positioned='false'] {\n animation: none;\n }\n /* Flipped panels grow out of the opposite edge (matches coordinates). */\n .rv-menu-panel[data-top-layer='true'][data-placement^='bottom'][data-flipped='true'] {\n transform-origin: bottom center;\n }\n .rv-menu-panel[data-top-layer='true'][data-placement^='top'][data-flipped='true'] {\n transform-origin: top center;\n }\n .rv-menu-panel {\n position: absolute;\n z-index: var(--rv-menu-z, 500);\n min-width: var(--rv-menu-width, 16rem);\n max-width: calc(100dvw - 2rem);\n /* Long lists scroll inside the panel instead of leaving the viewport. */\n max-height: min(20rem, calc(100dvh - 2rem));\n overflow-y: auto;\n background: var(--rv-menu-bg, #ffffff);\n color: var(--rv-menu-fg, #111111);\n font-family: var(--rv-menu-font, system-ui, -apple-system, 'Segoe UI', sans-serif);\n border: 1px solid var(--rv-menu-border, #e0e0e0);\n border-radius: var(--rv-menu-radius, 12px);\n box-shadow: var(--rv-menu-shadow, 0 16px 48px rgba(0, 0, 0, 0.22));\n padding: var(--rv-menu-pad, 6px);\n }\n .rv-menu-panel[data-placement='bottom-start'] {\n inset-block-start: calc(100% + 6px);\n inset-inline-start: 0;\n }\n .rv-menu-panel[data-placement='bottom-end'] {\n inset-block-start: calc(100% + 6px);\n inset-inline-end: 0;\n }\n .rv-menu-panel[data-placement='top-start'] {\n inset-block-end: calc(100% + 6px);\n inset-inline-start: 0;\n }\n .rv-menu-panel[data-placement='top-end'] {\n inset-block-end: calc(100% + 6px);\n inset-inline-end: 0;\n }\n .rv-menu-panel [rvmenuitem] {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n min-height: 40px;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 14px;\n line-height: 1.4;\n text-align: start;\n padding: 8px 10px;\n cursor: pointer;\n }\n .rv-menu-panel [rvmenuitem][data-active='true'] {\n background: color-mix(in srgb, currentColor 10%, transparent);\n }\n .rv-menu-panel [rvmenuitem]:hover {\n background: color-mix(in srgb, currentColor 8%, transparent);\n }\n .rv-menu-panel [rvmenuitem]:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: -2px;\n }\n .rv-menu-panel [rvmenuitem][aria-disabled='true'] {\n opacity: 0.45;\n cursor: default;\n }\n /* Optional icons: consumer projects any svg/icon content; the UI only\n owns sizing, alignment and tone so rows stay consistent. */\n .rv-menu-panel [rvmenuitem] svg,\n .rv-menu-panel [rvmenuitem] [data-icon] {\n width: 16px;\n height: 16px;\n flex: none;\n color: var(--rv-menu-muted);\n }\n .rv-menu-panel [rvmenuitem][data-active='true'] svg,\n .rv-menu-panel [rvmenuitem][data-active='true'] [data-icon] {\n color: inherit;\n }\n /* Opening motion (researched: Radix/shadcn/HeadlessUI pattern):\n fade + scale from the trigger edge + a small slide toward rest.\n Origin follows the vertical side so the panel grows out of the\n trigger; duration/easing stay overridable via vars. */\n .rv-menu-panel {\n transform-origin: top center;\n animation: rv-menu-in var(--rv-menu-duration, 160ms)\n var(--rv-menu-easing, cubic-bezier(0.16, 1, 0.3, 1));\n }\n .rv-menu-panel[data-placement='top-start'],\n .rv-menu-panel[data-placement='top-end'] {\n --rv-menu-from-y: 6px;\n transform-origin: bottom center;\n }\n @media (prefers-reduced-motion: reduce) {\n .rv-menu-panel {\n animation: none;\n }\n }\n @keyframes rv-menu-in {\n from {\n opacity: 0;\n transform: translateY(var(--rv-menu-from-y, -6px)) scale(0.96);\n }\n }\n `,\n ],\n})\nexport class RiviniaMenuComponent {\n /**\n * External controller (shared/test double). Omitted → the component owns\n * a private one (simplest embedding).\n */\n readonly menu = input<RiviniaMenuController | null>(null);\n /** Panel placement relative to the trigger. @default 'bottom-start' */\n readonly placement = input<RiviniaMenuUiPlacement>('bottom-start');\n /**\n * Accessible name for the menu panel (`aria-label`). Menus should be named\n * so screen-reader users know what they chose from; omitted → no label.\n *\n * Named `menuLabel` (not `label`) so it cannot be confused with the\n * per-item `label` in {@link RiviniaMenuItemData}.\n */\n readonly menuLabel = input<string | null>(null);\n /**\n * Declarative items. When provided, the panel renders these rows itself —\n * no `rvMenuItem` markup and no controller needed. Mutually exclusive with\n * projected `rvMenuItem` children (see `ambiguous`).\n */\n readonly items = input<readonly RiviniaMenuItemData[] | null>(null);\n /** Extra class(es) on the panel, alongside `.rv-menu-panel`. */\n readonly panelClass = input<string | null>(null);\n /** Extra class(es) on data-driven rows only (projected rows use your markup). */\n readonly itemClass = input<string | null>(null);\n\n /** Fired when the user picks an enabled item (id). Closes first. */\n readonly picked = output<MenuPick>();\n readonly opened = output<void>();\n readonly closed = output<void>();\n\n private readonly owned = new RiviniaMenu();\n private readonly self = inject(ElementRef<HTMLElement>);\n /** Item directives the consumer projected (not the ones we render). */\n private readonly projectedItems = contentChildren(RiviniaMenuItem, { descendants: true });\n /**\n * Stable panel id for `aria-controls`. Instance-unique via module counter;\n * SSR-safe because the panel (and its id) only renders client-side on open.\n */\n protected readonly panelId = `rv-menu-panel-${++nextPanelId}`;\n /** Deferred initial focus: set on open paths, consumed after render. */\n private focusPending = false;\n /** True between an open and the first row registered for it. */\n private pendingActivation = false;\n /**\n * Manual popover support (top-layer). Feature-detected: modern browsers\n * escape clipping ancestors; older ones keep the absolute fallback.\n */\n protected readonly topLayerSupported =\n typeof HTMLElement !== 'undefined' && 'showPopover' in HTMLElement.prototype;\n /** Coordinates applied (gates the enter animation until measured). */\n protected readonly positioned = signal(false);\n /** Whether the panel flipped to the opposite vertical side. */\n protected readonly flipped = signal(false);\n /** Printable-character search buffer; reset by timer after 500ms quiet. */\n private typeBuffer = '';\n private typeTimer: ReturnType<typeof setTimeout> | null = null;\n\n /** Effective controller: external input or the owned fallback. */\n controller(): RiviniaMenuController {\n return this.menu() ?? this.owned;\n }\n\n /** Data-driven rows to render, or empty when the consumer projects markup. */\n protected dataItems(): readonly RiviniaMenuItemData[] {\n return this.ambiguous() ? [] : (this.items() ?? []);\n }\n\n /**\n * Checkbox semantics only when a consumer supplied an external controller\n * with `multi`-style values; a single-select menu keeps plain `menuitem`\n * roles. Kept a method (not a computed) so it stays cheap and reactive via\n * template reads.\n */\n protected checkedFor(id: string): boolean | null {\n const c = this.controller() as RiviniaMenuController & {\n isSelected?: (id: string) => boolean;\n };\n return typeof c.isSelected === 'function' ? c.isSelected(id) : null;\n }\n\n /**\n * `[items]` and projected `rvMenuItem` children are two ways to populate the\n * same panel. Supplying both is ambiguous: we throw instead of silently\n * picking a winner, because a silently-ignored list is a data-loss bug.\n */\n protected ambiguous(): boolean {\n return this.items() !== null && this.projectedItems().length > 0;\n }\n\n /** Called by {@link RiviniaMenuItem} directives. */\n registerItem(item: { id: string; disabled: boolean }): void {\n const c = this.controller();\n c.register(item);\n // Rows can register AFTER the panel opened — the declarative `[items]`\n // path only renders them once `open()` is true, so at open time the\n // registry was still empty and `openMenu('first')` had nothing to\n // activate (measured: `activeId` stayed null, ArrowDown could not move,\n // and the highlight never appeared).\n if (this.pendingActivation && c.open() && c.activeId() === null && !item.disabled) {\n c.activateFirst();\n this.pendingActivation = false;\n }\n }\n\n /** Called by {@link RiviniaMenuItem} directives. */\n unregisterItem(id: string): void {\n this.controller().unregister(id);\n }\n\n constructor() {\n effect(() => {\n if (!this.ambiguous()) return;\n throw new Error(\n 'rv-menu: `[items]` cannot be combined with projected `rvMenuItem` ' +\n 'children — pick one way to populate the panel.',\n );\n });\n effect((onCleanup) => {\n if (typeof window === 'undefined') return;\n if (!this.controller().open()) return;\n const onPointer = (event: PointerEvent) => {\n const host = this.self.nativeElement;\n // Close WITHOUT restoring focus: the pointer is heading somewhere the\n // browser will focus naturally (or nowhere focusable). Stealing it\n // back to the trigger would break that (APG + WCAG focus-visible).\n if (host && !host.contains(event.target as Node)) this.doClose(false);\n };\n document.addEventListener('pointerdown', onPointer);\n // Coordinates go stale on outside scroll/resize — close instead of\n // drifting (native-select-like; scrolling inside the panel is exempt).\n // Like Tab-close, this is APG-consistent non-modal behavior.\n const onScroll = (event: Event) => {\n const panel = this.self.nativeElement.querySelector('.rv-menu-panel');\n if (panel && event.target instanceof Node && panel.contains(event.target)) return;\n this.doClose(false);\n };\n window.addEventListener('scroll', onScroll, true);\n window.addEventListener('resize', onScroll);\n onCleanup(() => {\n document.removeEventListener('pointerdown', onPointer);\n window.removeEventListener('scroll', onScroll, true);\n window.removeEventListener('resize', onScroll);\n });\n });\n // APG menu-button: the trigger exposes haspopup + expanded. The trigger\n // element is consumer-owned, so the attributes sync onto its focusable\n // child (or the wrapper itself as a fallback). aria-controls references\n // the stable panel id (valid while hidden per ARIA).\n effect(() => {\n if (typeof document === 'undefined') return;\n const open = this.controller().open();\n const trigger = this.triggerEl();\n trigger?.setAttribute('aria-haspopup', 'menu');\n trigger?.setAttribute('aria-expanded', String(open));\n trigger?.setAttribute('aria-controls', this.panelId);\n });\n afterRenderEffect(() => {\n if (this.controller().open() && !this.positioned()) this.showAndPosition();\n this.consumePendingFocus();\n });\n }\n\n /**\n * Top-layer presentation: show the manual popover, measure trigger +\n * panel, and pin fixed coordinates (flipping when needed). Runs inside\n * afterRender — before paint, so no flash. Falls back to the absolute CSS\n * placement when popover is unsupported (jsdom included).\n */\n private showAndPosition(): void {\n const root = this.self.nativeElement.querySelector('.rv-menu-panel') as HTMLElement | null;\n if (!root) return;\n if (typeof window === 'undefined' || !this.topLayerSupported) {\n this.positioned.set(true);\n return;\n }\n try {\n const popover = root as HTMLElement & { showPopover?: () => void };\n if (typeof popover.showPopover !== 'function') {\n this.positioned.set(true);\n return;\n }\n popover.showPopover();\n const trigger = this.triggerEl()?.getBoundingClientRect();\n if (!trigger) {\n this.positioned.set(true);\n return;\n }\n const at = computeMenuPosition(\n { left: trigger.left, top: trigger.top, width: trigger.width, height: trigger.height },\n { width: root.offsetWidth, height: root.offsetHeight },\n this.placement(),\n { width: window.innerWidth, height: window.innerHeight },\n this.isRtl(),\n );\n root.style.left = `${at.left}px`;\n root.style.top = `${at.top}px`;\n this.flipped.set(at.flipped);\n this.positioned.set(true);\n } catch {\n this.positioned.set(true);\n }\n }\n\n private isRtl(): boolean {\n try {\n const doc = typeof document === 'undefined' ? null : document.documentElement;\n if (doc?.dir === 'rtl') return true;\n if (doc?.dir === 'ltr') return false;\n return getComputedStyle(this.self.nativeElement).direction === 'rtl';\n } catch {\n return false;\n }\n }\n\n /**\n * Mark an opening pass: deferred focus + re-measurement.\n *\n * `positioned` is reset unconditionally (not only in top-layer mode) so the\n * afterRenderEffect always gets a second pass: once with the panel measured,\n * then again after `positioned` flips back to true — that second pass is what\n * guarantees focus lands even if the first one raced the panel's insertion.\n */\n private markOpening(): void {\n this.focusPending = true;\n // Set on every open so rows that register after the panel rendered can\n // claim the initial activation exactly once.\n this.pendingActivation = true;\n this.flipped.set(false);\n this.positioned.set(false);\n }\n\n protected onTriggerClick(): void {\n const c = this.controller();\n c.toggle();\n if (c.open()) {\n this.opened.emit();\n this.markOpening();\n } else {\n // Focus never left the trigger on toggle-close — restore is a no-op.\n this.closed.emit();\n }\n }\n\n protected onTriggerKey(event: KeyboardEvent): void {\n const c = this.controller();\n switch (event.key) {\n case 'Enter':\n case ' ':\n case 'ArrowDown':\n event.preventDefault();\n c.openMenu('first');\n this.opened.emit();\n this.markOpening();\n break;\n case 'ArrowUp':\n event.preventDefault();\n c.openMenu('last');\n this.opened.emit();\n this.markOpening();\n break;\n case 'Escape':\n if (c.open()) {\n event.preventDefault();\n this.doClose(true);\n }\n break;\n default:\n break;\n }\n }\n\n protected onPanelKey(event: KeyboardEvent): void {\n const c = this.controller();\n switch (event.key) {\n case 'Escape':\n event.preventDefault();\n this.doClose(true);\n break;\n case 'ArrowDown':\n event.preventDefault();\n c.activateNext();\n this.focusActive();\n break;\n case 'ArrowUp':\n event.preventDefault();\n c.activatePrevious();\n this.focusActive();\n break;\n case 'Home':\n event.preventDefault();\n c.activateFirst();\n this.focusActive();\n break;\n case 'End':\n event.preventDefault();\n c.activateLast();\n this.focusActive();\n break;\n case 'Enter':\n case ' ':\n event.preventDefault();\n this.pickFocused();\n break;\n case 'Tab': {\n // Non-modal Tab must not trap focus. The panel is still in the DOM\n // when this event is handled (Angular removes it on the next CD), so\n // the browser's own Tab step would re-enter the panel and then lose\n // focus to <body> when it unmounted (measured on both Chromium and\n // Firefox). So we close, and step focus explicitly from the trigger.\n event.preventDefault();\n const backwards = event.shiftKey;\n this.doClose(false);\n this.focusAdjacentToTrigger(backwards);\n break;\n }\n default:\n this.onTypeahead(event);\n break;\n }\n }\n\n /**\n * APG printable-character search: typing moves focus to the next enabled\n * item whose visible text starts with the buffer. Repeating one character\n * cycles through its matches; the buffer resets after 500ms of quiet.\n * Labels are UI-owned (read from the DOM), so this lives in the component,\n * not the headless controller.\n */\n private resetTypeahead(): void {\n this.typeBuffer = '';\n if (this.typeTimer !== null) {\n clearTimeout(this.typeTimer);\n this.typeTimer = null;\n }\n }\n\n private onTypeahead(event: KeyboardEvent): void {\n if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) return;\n const ch = event.key.toLocaleLowerCase();\n // Repeating one character cycles its matches instead of growing the buffer.\n const cycling = this.typeBuffer !== '' && [...this.typeBuffer].every((c) => c === ch);\n if (!cycling) this.typeBuffer += ch;\n if (this.typeTimer !== null) clearTimeout(this.typeTimer);\n this.typeTimer = setTimeout(() => {\n this.typeBuffer = '';\n this.typeTimer = null;\n }, 500);\n const id = this.matchTypeahead(this.typeBuffer);\n if (id !== null && this.controller().activate(id)) this.focusActive();\n }\n\n private matchTypeahead(buffer: string): string | null {\n const root: HTMLElement | null = this.self.nativeElement.querySelector('.rv-menu-panel');\n if (!root) return null;\n const enabled = new Set(\n this.controller()\n .items()\n .filter((i) => !i.disabled)\n .map((i) => i.id),\n );\n const rows = [...root.querySelectorAll('[data-rv-id]')]\n .map((el) => ({\n id: el.getAttribute('data-rv-id') ?? '',\n text: (el.textContent ?? '').trim().toLocaleLowerCase(),\n }))\n .filter((r) => enabled.has(r.id) && r.text.startsWith(buffer));\n if (rows.length === 0) return null;\n if (new Set(buffer).size === 1) {\n // Repeated character: cycle from after the current item.\n const ids = rows.map((r) => r.id);\n const at = ids.indexOf(this.controller().activeId() ?? '');\n return ids[(at + 1) % ids.length];\n }\n return rows[0].id;\n }\n\n private pickFocused(): void {\n const c = this.controller();\n const id = c.pickActive();\n if (id === null) return;\n // Re-validate before emitting: the active item may have been disabled\n // after it received focus (dynamic `[disabled]`), and Enter must never\n // activate a disabled row (measured: all-disabled menu emitted `picked=one`).\n const item = c.items().find((i) => i.id === id);\n if (!item || item.disabled) return;\n this.doClose(false);\n this.picked.emit({ id });\n }\n\n /**\n * Mouse pick of a concrete item. Called by {@link RiviniaMenuItem}.\n * Disabled/unknown ids are ignored via `activate` (returns false).\n */\n pickItem(id: string): void {\n if (!this.controller().activate(id)) return;\n this.doClose(false);\n this.picked.emit({ id });\n }\n\n /** Mouse hover tracks keyboard focus. Called by {@link RiviniaMenuItem}. */\n hoverItem(id: string): void {\n this.controller().activate(id);\n }\n\n private doClose(restoreFocus: boolean): void {\n const was = this.controller().open();\n // APG: the typeahead buffer is per-open. Keeping it across a close made\n // the next open start mid-word (measured: `due` + `d` matched nothing).\n this.resetTypeahead();\n this.controller().closeMenu();\n if (was) this.closed.emit();\n if (restoreFocus) this.focusTrigger();\n }\n\n /**\n * Initial focus after open. The panel only exists in the DOM after the\n * opening change-detection pass, so focusing synchronously races rendering.\n * afterRenderEffect re-runs when `open` flips (reactive read) and executes\n * after the commit — panel present, in apps and in tests. Never runs on the\n * server (SSR-safe by construction). Skips when focus already sits inside\n * the panel, so later renders never steal it back.\n */\n private consumePendingFocus(): void {\n // `positioned` is read so the effect re-runs after measurement even when\n // `open` never changes again — the second pass is the retry.\n const ready = this.positioned();\n if (!ready || !this.controller().open() || !this.focusPending) return;\n const root: HTMLElement | null = this.self.nativeElement.querySelector('.rv-menu-panel');\n // Panel not committed yet: keep the flag so the next pass retries. Clearing\n // it here is what used to strand focus on <body> (measured: Playwright\n // \"received: inactive\" on every arrow assertion).\n if (!root) return;\n const active = this.activeDocumentElement();\n if (active && root.contains(active)) {\n this.focusPending = false;\n return;\n }\n // No enabled item to focus (empty/all-disabled menu): done, nothing to retry.\n this.focusPending = false;\n this.focusActive();\n }\n\n private focusActive(): void {\n const id = this.controller().activeId();\n const root: HTMLElement | null = this.self.nativeElement.querySelector('.rv-menu-panel');\n if (!root) return;\n const target = id\n ? root.querySelector<HTMLElement>(`[data-rv-id=\"${CSS.escape(id)}\"]`)\n : root.querySelector<HTMLElement>('[rvmenuitem]');\n try {\n target?.focus({ preventScroll: true });\n } catch {\n // Focus is best-effort.\n }\n }\n\n private activeDocumentElement(): Element | null {\n try {\n return typeof document === 'undefined' ? null : document.activeElement;\n } catch {\n return null;\n }\n }\n\n /** Focusable trigger child (APG target) or the wrapper as fallback. */\n private triggerEl(): HTMLElement | null {\n const wrap = this.self.nativeElement.querySelector('.rv-menu-trigger') as HTMLElement | null;\n if (!wrap) return null;\n return (wrap.querySelector('button, a, [tabindex]') as HTMLElement | null) ?? wrap;\n }\n\n /**\n * Move focus to the element before/after the trigger in document tab order.\n *\n * Explicit rather than delegating to the browser: the panel is still\n * mounted when Tab is handled, so the browser's own step re-enters the panel\n * and focus is then lost to <body> when it unmounts (measured on Chromium\n * and Firefox). Leaves focus on the document when nothing is left in that\n * direction.\n */\n private focusAdjacentToTrigger(backwards: boolean): void {\n try {\n const trigger = this.triggerEl();\n const doc = trigger?.ownerDocument;\n if (!trigger || !doc) return;\n const selector =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n const stops = [...doc.querySelectorAll<HTMLElement>(selector)].filter((el) => {\n if (el.hasAttribute('disabled') || el.getAttribute('aria-hidden') === 'true') return false;\n // Rows inside any open panel are part of that menu, not this page flow.\n if (el.closest('.rv-menu-panel')) return false;\n return el.getClientRects().length > 0;\n });\n stops.sort((a, b) =>\n a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1,\n );\n const at = stops.indexOf(trigger);\n if (at === -1) return;\n stops[backwards ? at - 1 : at + 1]?.focus({ preventScroll: true });\n } catch {\n // Focus stepping is best-effort; never break the close path.\n }\n }\n\n private focusTrigger(): void {\n try {\n this.triggerEl()?.focus({ preventScroll: true });\n } catch {\n // Best-effort.\n }\n }\n}\n"],"mappings":";;;AA8BA,MAAa,WAAW;AAExB,MAAM,OAAO;AASb,SAAgB,oBACd,SACA,OACA,WACA,UACA,KACA,MAAA,GAAc;CAEd,MAAM,CAAC,UAAU,cAAc,UAAU,MAAM,GAAG;CAClD,MAAM,QAAQ,aAAa;CAE3B,MAAM,YAAa,eAAe,UAAW;CAE7C,IAAI,OAAO,YAAY,QAAQ,OAAO,QAAQ,OAAO,QAAQ,QAAQ,MAAM;CAC3E,IAAI,MAAM,QAAQ,QAAQ,MAAM,QAAQ,SAAS,MAAM,QAAQ,MAAM,MAAM,MAAM;CACjF,IAAI,UAAU;CAEd,MAAM,YAAY,QAAQ,MAAM,QAAQ,SAAS,MAAM,MAAM,UAAU,SAAS,SAAS;CACzF,MAAM,YAAY,QAAQ,MAAM,MAAM,MAAM,UAAU;CACtD,IAAI,SAAS,MAAM,MAAM,SAAS,SAAS,SAAS,QAAQ,WAAW;EACrE,MAAM,QAAQ,MAAM,MAAM,MAAM;EAChC,UAAU;CACZ,OAAO,IAAI,CAAC,SAAS,MAAM,QAAQ,WAAW;EAC5C,MAAM,QAAQ,MAAM,QAAQ,SAAS;EACrC,UAAU;CACZ;CAEA,MAAM,WAAW,QAAQ,OAAO,QAAQ,QAAQ,MAAM,SAAS;CAC/D,MAAM,YAAY,QAAQ,OAAO,MAAM,SAAS,SAAS,QAAQ;CACjE,IAAI,aAAa,OAAO,MAAM,QAAQ,SAAS,QAAQ,QAAQ,UAC7D,OAAO,QAAQ,OAAO,QAAQ,QAAQ,MAAM;MACvC,IAAI,CAAC,aAAa,OAAO,QAAQ,WACtC,OAAO,QAAQ;CAGjB,OAAO,KAAK,IAAI,KAAK,IAAI,MAAM,IAAI,GAAG,KAAK,IAAI,MAAM,SAAS,QAAQ,MAAM,QAAQ,IAAI,CAAC;CACzF,MAAM,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,MAAM,SAAS,SAAS,MAAM,SAAS,IAAI,CAAC;CACzF,OAAO;EAAE,MAAM,KAAK,MAAM,IAAI;EAAG,KAAK,KAAK,MAAM,GAAG;EAAG;CAAO;AAChE;ACtDA,IAAI,cAAc;AAuBlB,IAAa,kBAAb,MAAa,gBAAe;CACjB,KAAK,MAAM,SAAkB,GAAA,YAAA,CAAA,EAAA,WAAA,KAAA,CAAA,+BAAA,CAAA,CAAA;CAC7B,WAAW,MAAM,OAAK;EAAA,GAAA,YAAA,EAAA,WAAA,WAAA,+BAAA,CAAA;EAC7B,YAAY,MAAwB,MAAM,MAAM,MAAM;CAAI,CAAA;CAOnD,UAAU,MAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAE7B,OAAO,OAAO,YAAyB,EAAE,UAAU,KAAI,CAAE;CACzD,OAAO,OAAO,sBAAsB;EAAE,UAAU;EAAM,UAAU;CAAI,CAAE;CACtE,SAAS,eAAe,KAAK,MAAM,WAAU,CAAE,CAAC,SAAQ,MAAO,KAAK,GAAE,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAEhF,MAAG;EACX,OAAO,KAAK,OAAM,IAAK,MAAM;CAC/B;CAEU,eAAY;EACpB,OAAO,KAAK,QAAO,MAAO,OAAO,aAAa;CAChD;CAEU,cAAW;EACnB,MAAM,UAAU,KAAK,QAAO;EAC5B,OAAO,YAAY,OAAO,OAAO,OAAO,OAAO;CACjD;CAQQ,aAAU;EAChB,IAAI,KAAK,SAAQ,GAAI,OAAO;EAC5B,OAAO,KAAK,MAAM,eAAe,aAAa;CAChD;CAGU,OAAI;EACZ,KAAK,MAAM,SAAS,KAAK,GAAE,CAAE;CAC/B;CAGU,QAAK;EACb,KAAK,MAAM,UAAU,KAAK,GAAE,CAAE;CAChC;CAGQ,eAA8B;CAEtC,cAAA;EASE,aAAY;GACV,MAAM,KAAK,KAAK,GAAE;GAClB,MAAM,OAAO,KAAK;GAClB,IAAI,KAAK,iBAAiB,MAAM;GAChC,IAAI,KAAK,iBAAiB,IAAI,MAAM,eAAe,KAAK,YAAY;GAGpE,MAAM,aAAa;IAAE;IAAI,UAAU,KAAK,WAAU;GAAE,CAAE;GACtD,KAAK,eAAe;EACtB,CAAC;CACH;CAEA,WAAQ;EACN,KAAK,eAAe,KAAK,GAAE;EAC3B,KAAK,MAAM,aAAa;GAAE,IAAI,KAAK,GAAE;GAAI,UAAU,KAAK,WAAU;EAAE,CAAE;CACxE;CAEA,cAAW;EACT,IAAI,KAAK,iBAAiB,MAAM;EAChC,KAAK,MAAM,eAAe,KAAK,YAAY;EAC3C,KAAK,eAAe;CACtB;CAnFW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAe,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAf,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAe,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,IAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,UAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,SAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,MAAA;GAAA,WAAA;IAAA,SAAA;IAAA,gBAAA;GAAA;GAAA,YAAA;IAAA,aAAA;IAAA,mBAAA;IAAA,iBAAA;IAAA,sBAAA;IAAA,oBAAA;IAAA,qBAAA;GAAA;EAAA;EAAA,UAAA;CAAA,CAAA;;AAAf,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAe,YAAA,CAAA;EAd3B,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,YAAY;GACZ,MAAM;IACJ,eAAe;IACf,qBAAqB;IACrB,mBAAmB;IACnB,wBAAwB;IACxB,sBAAsB;IACtB,uBAAuB;IACvB,WAAW;IACX,kBAAkB;GACnB;EACF,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6SD,IAAa,uBAAb,MAAa,qBAAoB;CAKtB,OAAO,MAAoC,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAEhD,YAAY,MAA8B,gBAAe,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAQzD,YAAY,MAAqB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAMtC,QAAQ,MAA6C,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAE1D,aAAa,MAAqB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAEvC,YAAY,MAAqB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAGtC,SAAS,OAAM;CACf,SAAS,OAAM;CACf,SAAS,OAAM;CAEP,QAAQ,IAAI,YAAW;CACvB,OAAO,OAAO,UAAuB;CAErC,iBAAiB,gBAAgB,iBAAe;EAAA,GAAA,YAAA,EAAA,WAAA,iBAAA,+BAAA,CAAA;EAAI,aAAa;CAAI,CAAA;CAKnE,UAAU,iBAAiB,EAAE;CAExC,eAAe;CAEf,oBAAoB;CAKT,oBACjB,OAAO,gBAAgB,eAAe,iBAAiB,YAAY;CAElD,aAAa,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAE1B,UAAU,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAElC,aAAa;CACb,YAAkD;CAG1D,aAAU;EACR,OAAO,KAAK,KAAI,KAAM,KAAK;CAC7B;CAGU,YAAS;EACjB,OAAO,KAAK,UAAS,IAAK,CAAA,IAAM,KAAK,MAAK,KAAM,CAAA;CAClD;CAQU,WAAW,IAAU;EAC7B,MAAM,IAAI,KAAK,WAAU;EAGzB,OAAO,OAAO,EAAE,eAAe,aAAa,EAAE,WAAW,EAAE,IAAI;CACjE;CAOU,YAAS;EACjB,OAAO,KAAK,MAAK,MAAO,QAAQ,KAAK,eAAc,CAAE,CAAC,SAAS;CACjE;CAGA,aAAa,MAAuC;EAClD,MAAM,IAAI,KAAK,WAAU;EACzB,EAAE,SAAS,IAAI;EAMf,IAAI,KAAK,qBAAqB,EAAE,KAAI,KAAM,EAAE,SAAQ,MAAO,QAAQ,CAAC,KAAK,UAAU;GACjF,EAAE,cAAa;GACf,KAAK,oBAAoB;EAC3B;CACF;CAGA,eAAe,IAAU;EACvB,KAAK,WAAU,CAAE,CAAC,WAAW,EAAE;CACjC;CAEA,cAAA;EACE,aAAY;GACV,IAAI,CAAC,KAAK,UAAS,GAAI;GACvB,MAAM,IAAI,MACR,kHACkD;EAEtD,CAAC;EACD,QAAQ,cAAa;GACnB,IAAI,OAAO,WAAW,aAAa;GACnC,IAAI,CAAC,KAAK,WAAU,CAAE,CAAC,KAAI,GAAI;GAC/B,MAAM,aAAa,UAAuB;IACxC,MAAM,OAAO,KAAK,KAAK;IAIvB,IAAI,QAAQ,CAAC,KAAK,SAAS,MAAM,MAAc,GAAG,KAAK,QAAQ,KAAK;GACtE;GACA,SAAS,iBAAiB,eAAe,SAAS;GAIlD,MAAM,YAAY,UAAgB;IAChC,MAAM,QAAQ,KAAK,KAAK,cAAc,cAAc,gBAAgB;IACpE,IAAI,SAAS,MAAM,kBAAkB,QAAQ,MAAM,SAAS,MAAM,MAAM,GAAG;IAC3E,KAAK,QAAQ,KAAK;GACpB;GACA,OAAO,iBAAiB,UAAU,UAAU,IAAI;GAChD,OAAO,iBAAiB,UAAU,QAAQ;GAC1C,gBAAe;IACb,SAAS,oBAAoB,eAAe,SAAS;IACrD,OAAO,oBAAoB,UAAU,UAAU,IAAI;IACnD,OAAO,oBAAoB,UAAU,QAAQ;GAC/C,CAAC;EACH,CAAC;EAKD,aAAY;GACV,IAAI,OAAO,aAAa,aAAa;GACrC,MAAM,OAAO,KAAK,WAAU,CAAE,CAAC,KAAI;GACnC,MAAM,UAAU,KAAK,UAAS;GAC9B,SAAS,aAAa,iBAAiB,MAAM;GAC7C,SAAS,aAAa,iBAAiB,OAAO,IAAI,CAAC;GACnD,SAAS,aAAa,iBAAiB,KAAK,OAAO;EACrD,CAAC;EACD,wBAAuB;GACrB,IAAI,KAAK,WAAU,CAAE,CAAC,KAAI,KAAM,CAAC,KAAK,WAAU,GAAI,KAAK,gBAAe;GACxE,KAAK,oBAAmB;EAC1B,CAAC;CACH;CAQQ,kBAAe;EACrB,MAAM,OAAO,KAAK,KAAK,cAAc,cAAc,gBAAgB;EACnE,IAAI,CAAC,MAAM;EACX,IAAI,OAAO,WAAW,eAAe,CAAC,KAAK,mBAAmB;GAC5D,KAAK,WAAW,IAAI,IAAI;GACxB;EACF;EACA,IAAI;GACF,MAAM,UAAU;GAChB,IAAI,OAAO,QAAQ,gBAAgB,YAAY;IAC7C,KAAK,WAAW,IAAI,IAAI;IACxB;GACF;GACA,QAAQ,YAAW;GACnB,MAAM,UAAU,KAAK,UAAS,CAAE,EAAE,sBAAqB;GACvD,IAAI,CAAC,SAAS;IACZ,KAAK,WAAW,IAAI,IAAI;IACxB;GACF;GACA,MAAM,KAAK,oBACT;IAAE,MAAM,QAAQ;IAAM,KAAK,QAAQ;IAAK,OAAO,QAAQ;IAAO,QAAQ,QAAQ;GAAM,GACpF;IAAE,OAAO,KAAK;IAAa,QAAQ,KAAK;GAAY,GACpD,KAAK,UAAS,GACd;IAAE,OAAO,OAAO;IAAY,QAAQ,OAAO;GAAW,GACtD,KAAK,MAAK,CAAE;GAEd,KAAK,MAAM,OAAO,GAAG,GAAG,KAAI;GAC5B,KAAK,MAAM,MAAM,GAAG,GAAG,IAAG;GAC1B,KAAK,QAAQ,IAAI,GAAG,OAAO;GAC3B,KAAK,WAAW,IAAI,IAAI;EAC1B,QAAQ;GACN,KAAK,WAAW,IAAI,IAAI;EAC1B;CACF;CAEQ,QAAK;EACX,IAAI;GACF,MAAM,MAAM,OAAO,aAAa,cAAc,OAAO,SAAS;GAC9D,IAAI,KAAK,QAAQ,OAAO,OAAO;GAC/B,IAAI,KAAK,QAAQ,OAAO,OAAO;GAC/B,OAAO,iBAAiB,KAAK,KAAK,aAAa,CAAC,CAAC,cAAc;EACjE,QAAQ;GACN,OAAO;EACT;CACF;CAUQ,cAAW;EACjB,KAAK,eAAe;EAGpB,KAAK,oBAAoB;EACzB,KAAK,QAAQ,IAAI,KAAK;EACtB,KAAK,WAAW,IAAI,KAAK;CAC3B;CAEU,iBAAc;EACtB,MAAM,IAAI,KAAK,WAAU;EACzB,EAAE,OAAM;EACR,IAAI,EAAE,KAAI,GAAI;GACZ,KAAK,OAAO,KAAI;GAChB,KAAK,YAAW;EAClB,OAEE,KAAK,OAAO,KAAI;CAEpB;CAEU,aAAa,OAAoB;EACzC,MAAM,IAAI,KAAK,WAAU;EACzB,QAAQ,MAAM,KAAd;GACE,KAAK;GACL,KAAK;GACL,KAAK;IACH,MAAM,eAAc;IACpB,EAAE,SAAS,OAAO;IAClB,KAAK,OAAO,KAAI;IAChB,KAAK,YAAW;IAChB;GACF,KAAK;IACH,MAAM,eAAc;IACpB,EAAE,SAAS,MAAM;IACjB,KAAK,OAAO,KAAI;IAChB,KAAK,YAAW;IAChB;GACF,KAAK,UACH,IAAI,EAAE,KAAI,GAAI;IACZ,MAAM,eAAc;IACpB,KAAK,QAAQ,IAAI;GACnB;EAIJ;CACF;CAEU,WAAW,OAAoB;EACvC,MAAM,IAAI,KAAK,WAAU;EACzB,QAAQ,MAAM,KAAd;GACE,KAAK;IACH,MAAM,eAAc;IACpB,KAAK,QAAQ,IAAI;IACjB;GACF,KAAK;IACH,MAAM,eAAc;IACpB,EAAE,aAAY;IACd,KAAK,YAAW;IAChB;GACF,KAAK;IACH,MAAM,eAAc;IACpB,EAAE,iBAAgB;IAClB,KAAK,YAAW;IAChB;GACF,KAAK;IACH,MAAM,eAAc;IACpB,EAAE,cAAa;IACf,KAAK,YAAW;IAChB;GACF,KAAK;IACH,MAAM,eAAc;IACpB,EAAE,aAAY;IACd,KAAK,YAAW;IAChB;GACF,KAAK;GACL,KAAK;IACH,MAAM,eAAc;IACpB,KAAK,YAAW;IAChB;GACF,KAAK,OAAO;IAMV,MAAM,eAAc;IACpB,MAAM,YAAY,MAAM;IACxB,KAAK,QAAQ,KAAK;IAClB,KAAK,uBAAuB,SAAS;IACrC;GACF;GACA,SACE,KAAK,YAAY,KAAK;EAE1B;CACF;CASQ,iBAAc;EACpB,KAAK,aAAa;EAClB,IAAI,KAAK,cAAc,MAAM;GAC3B,aAAa,KAAK,SAAS;GAC3B,KAAK,YAAY;EACnB;CACF;CAEQ,YAAY,OAAoB;EACtC,IAAI,MAAM,IAAI,WAAW,KAAK,MAAM,WAAW,MAAM,WAAW,MAAM,QAAQ;EAC9E,MAAM,KAAK,MAAM,IAAI,kBAAiB;EAGtC,IAAI,EADY,KAAK,eAAe,MAAM,CAAC,GAAG,KAAK,UAAU,CAAC,CAAC,OAAO,MAAM,MAAM,EAAE,IACtE,KAAK,cAAc;EACjC,IAAI,KAAK,cAAc,MAAM,aAAa,KAAK,SAAS;EACxD,KAAK,YAAY,iBAAgB;GAC/B,KAAK,aAAa;GAClB,KAAK,YAAY;EACnB,GAAG,GAAG;EACN,MAAM,KAAK,KAAK,eAAe,KAAK,UAAU;EAC9C,IAAI,OAAO,QAAQ,KAAK,WAAU,CAAE,CAAC,SAAS,EAAE,GAAG,KAAK,YAAW;CACrE;CAEQ,eAAe,QAAc;EACnC,MAAM,OAA2B,KAAK,KAAK,cAAc,cAAc,gBAAgB;EACvF,IAAI,CAAC,MAAM,OAAO;EAClB,MAAM,UAAU,IAAI,IAClB,KAAK,WAAU,CAAE,CACd,MAAK,CAAE,CACP,QAAQ,MAAM,CAAC,EAAE,QAAQ,CAAC,CAC1B,KAAK,MAAM,EAAE,EAAE,CAAC;EAErB,MAAM,OAAO,CAAC,GAAG,KAAK,iBAAiB,cAAc,CAAC,CAAC,CACpD,KAAK,QAAQ;GACZ,IAAI,GAAG,aAAa,YAAY,KAAK;GACrC,OAAO,GAAG,eAAe,GAAA,CAAI,KAAI,CAAE,CAAC,kBAAiB;EACtD,EAAC,CAAC,CACF,QAAQ,MAAM,QAAQ,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,WAAW,MAAM,CAAC;EAC/D,IAAI,KAAK,WAAW,GAAG,OAAO;EAC9B,IAAI,IAAI,IAAI,MAAM,CAAC,CAAC,SAAS,GAAG;GAE9B,MAAM,MAAM,KAAK,KAAK,MAAM,EAAE,EAAE;GAEhC,OAAO,KADI,IAAI,QAAQ,KAAK,WAAU,CAAE,CAAC,SAAQ,KAAM,EAC3C,IAAK,KAAK,IAAI;EAC5B;EACA,OAAO,KAAK,EAAE,CAAC;CACjB;CAEQ,cAAW;EACjB,MAAM,IAAI,KAAK,WAAU;EACzB,MAAM,KAAK,EAAE,WAAU;EACvB,IAAI,OAAO,MAAM;EAIjB,MAAM,OAAO,EAAE,MAAK,CAAE,CAAC,MAAM,MAAM,EAAE,OAAO,EAAE;EAC9C,IAAI,CAAC,QAAQ,KAAK,UAAU;EAC5B,KAAK,QAAQ,KAAK;EAClB,KAAK,OAAO,KAAK,EAAE,GAAE,CAAE;CACzB;CAMA,SAAS,IAAU;EACjB,IAAI,CAAC,KAAK,WAAU,CAAE,CAAC,SAAS,EAAE,GAAG;EACrC,KAAK,QAAQ,KAAK;EAClB,KAAK,OAAO,KAAK,EAAE,GAAE,CAAE;CACzB;CAGA,UAAU,IAAU;EAClB,KAAK,WAAU,CAAE,CAAC,SAAS,EAAE;CAC/B;CAEQ,QAAQ,cAAqB;EACnC,MAAM,MAAM,KAAK,WAAU,CAAE,CAAC,KAAI;EAGlC,KAAK,eAAc;EACnB,KAAK,WAAU,CAAE,CAAC,UAAS;EAC3B,IAAI,KAAK,KAAK,OAAO,KAAI;EACzB,IAAI,cAAc,KAAK,aAAY;CACrC;CAUQ,sBAAmB;EAIzB,IAAI,CADU,KAAK,WACd,KAAS,CAAC,KAAK,WAAU,CAAE,CAAC,KAAI,KAAM,CAAC,KAAK,cAAc;EAC/D,MAAM,OAA2B,KAAK,KAAK,cAAc,cAAc,gBAAgB;EAIvF,IAAI,CAAC,MAAM;EACX,MAAM,SAAS,KAAK,sBAAqB;EACzC,IAAI,UAAU,KAAK,SAAS,MAAM,GAAG;GACnC,KAAK,eAAe;GACpB;EACF;EAEA,KAAK,eAAe;EACpB,KAAK,YAAW;CAClB;CAEQ,cAAW;EACjB,MAAM,KAAK,KAAK,WAAU,CAAE,CAAC,SAAQ;EACrC,MAAM,OAA2B,KAAK,KAAK,cAAc,cAAc,gBAAgB;EACvF,IAAI,CAAC,MAAM;EACX,MAAM,SAAS,KACX,KAAK,cAA2B,gBAAgB,IAAI,OAAO,EAAE,EAAC,GAAI,IAClE,KAAK,cAA2B,cAAc;EAClD,IAAI;GACF,QAAQ,MAAM,EAAE,eAAe,KAAI,CAAE;EACvC,QAAQ,CAER;CACF;CAEQ,wBAAqB;EAC3B,IAAI;GACF,OAAO,OAAO,aAAa,cAAc,OAAO,SAAS;EAC3D,QAAQ;GACN,OAAO;EACT;CACF;CAGQ,YAAS;EACf,MAAM,OAAO,KAAK,KAAK,cAAc,cAAc,kBAAkB;EACrE,IAAI,CAAC,MAAM,OAAO;EAClB,OAAQ,KAAK,cAAc,uBAAuB,KAA4B;CAChF;CAWQ,uBAAuB,WAAkB;EAC/C,IAAI;GACF,MAAM,UAAU,KAAK,UAAS;GAC9B,MAAM,MAAM,SAAS;GACrB,IAAI,CAAC,WAAW,CAAC,KAAK;GAGtB,MAAM,QAAQ,CAAC,GAAG,IAAI,iBAA8B,6IAAQ,CAAC,CAAC,CAAC,QAAQ,OAAM;IAC3E,IAAI,GAAG,aAAa,UAAU,KAAK,GAAG,aAAa,aAAa,MAAM,QAAQ,OAAO;IAErF,IAAI,GAAG,QAAQ,gBAAgB,GAAG,OAAO;IACzC,OAAO,GAAG,eAAc,CAAE,CAAC,SAAS;GACtC,CAAC;GACD,MAAM,MAAM,GAAG,MACb,EAAE,wBAAwB,CAAC,IAAI,KAAK,8BAA8B,KAAK,CAAC;GAE1E,MAAM,KAAK,MAAM,QAAQ,OAAO;GAChC,IAAI,OAAO,IAAI;GACf,MAAM,YAAY,KAAK,IAAI,KAAK,EAAE,EAAE,MAAM,EAAE,eAAe,KAAI,CAAE;EACnE,QAAQ,CAER;CACF;CAEQ,eAAY;EAClB,IAAI;GACF,KAAK,UAAS,CAAE,EAAE,MAAM,EAAE,eAAe,KAAI,CAAE;EACjD,QAAQ,CAER;CACF;CAhgBW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAoB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAApB,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAoB,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,YAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,SAAA;GAAA,QAAA;GAAA,QAAA;GAAA,QAAA;EAAA;EAAA,SAAA,CAAA;GAAA,cAAA;GAAA,WAmCmB;GAAe,aAAA;GAAA,UAAA;EAAA,CAAA;EAAA,UAAA;EAAA,UAzNvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmCT,UAAA;EAAA,QAAA,CAAA,0wFAAA;EAAA,cAAA,CAAA;GAAA,MAAA;GAAA,MAzJU;GAAe,UAAA;GAAA,QAAA;IAAA;IAAA;IAAA;GAAA;EAAA,CAAA;EAAA,eAAA,GAAA,kBAAA;CAAA,CAAA;;AA4Sf,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAoB,YAAA,CAAA;EAnMhC,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAS,YACP;GAAI,eAMD,kBAAkB;GAAI,SAI5B,CAAC,eAAe;GAAC,UAChB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCT,QAAA,CAAA,0wFAAA;EAAA,CAAA;CAsLiD,CAAA;CAAA,sBAAA,CAAA;CAAA,gBAAA;EAAA,MAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,WAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,WAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,OAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,YAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,WAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,QAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,QAAA;EAAA,CAAA;EAAA,QAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,QAAA;EAAA,CAAA;EAAA,QAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,QAAA;EAAA,CAAA;EAAA,gBAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,GAAA,iBAAA,eAAe,GAAA;IAAI,aAAa;IAAM,UAAA;GAAA,CAAA;EAAA,CAAA;CAAA;AAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"rivinia-menu-ui.mjs","names":[],"sources":["../../../projects/menu/ui/src/lib/menu-position.ts","../../../projects/menu/ui/src/lib/menu-panel.ts"],"sourcesContent":["import type { RiviniaMenuPlacement } from '@rivinia/menu';\n\n/** Trigger rect in viewport coordinates. */\nexport interface MenuTriggerRect {\n readonly left: number;\n readonly top: number;\n readonly width: number;\n readonly height: number;\n}\n\n/** Measured panel size. */\nexport interface MenuPanelSize {\n readonly width: number;\n readonly height: number;\n}\n\n/** Viewport size. */\nexport interface MenuViewport {\n readonly width: number;\n readonly height: number;\n}\n\n/** Fixed-position coordinates plus whether the vertical side flipped. */\nexport interface MenuCoordinates {\n readonly left: number;\n readonly top: number;\n readonly flipped: boolean;\n}\n\n/** Gap between trigger and panel. */\nexport const MENU_GAP = 6;\n/** Minimum distance kept from viewport edges. */\nconst EDGE = 8;\n\n/**\n * Pure top-layer position for a menu panel: anchors to the trigger per\n * `placement` (logical, RTL-aware), flips to the opposite vertical side when\n * the preferred side overflows but the other fits, then clamps inside the\n * viewport. Zero DOM access — unit-testable with fake rects; the component\n * only measures and applies.\n */\nexport function computeMenuPosition(\n trigger: MenuTriggerRect,\n panel: MenuPanelSize,\n placement: RiviniaMenuPlacement,\n viewport: MenuViewport,\n rtl: boolean,\n gap = MENU_GAP,\n): MenuCoordinates {\n const [vertical, horizontal] = placement.split('-');\n const below = vertical !== 'top';\n // Logical start: left edge in LTR, right edge in RTL.\n const alignLeft = (horizontal !== 'end') !== rtl;\n\n let left = alignLeft ? trigger.left : trigger.left + trigger.width - panel.width;\n let top = below ? trigger.top + trigger.height + gap : trigger.top - gap - panel.height;\n let flipped = false;\n\n const fitsBelow = trigger.top + trigger.height + gap + panel.height <= viewport.height - EDGE;\n const fitsAbove = trigger.top - gap - panel.height >= EDGE;\n if (below && top + panel.height > viewport.height - EDGE && fitsAbove) {\n top = trigger.top - gap - panel.height;\n flipped = true;\n } else if (!below && top < EDGE && fitsBelow) {\n top = trigger.top + trigger.height + gap;\n flipped = true;\n }\n\n const fitsLeft = trigger.left + trigger.width - panel.width >= EDGE;\n const fitsRight = trigger.left + panel.width <= viewport.width - EDGE;\n if (alignLeft && left + panel.width > viewport.width - EDGE && fitsLeft) {\n left = trigger.left + trigger.width - panel.width;\n } else if (!alignLeft && left < EDGE && fitsRight) {\n left = trigger.left;\n }\n\n left = Math.min(Math.max(left, EDGE), Math.max(EDGE, viewport.width - panel.width - EDGE));\n top = Math.min(Math.max(top, EDGE), Math.max(EDGE, viewport.height - panel.height - EDGE));\n return { left: Math.round(left), top: Math.round(top), flipped };\n}\n","import {\n Component,\n Directive,\n ElementRef,\n ViewEncapsulation,\n afterRenderEffect,\n computed,\n contentChildren,\n effect,\n inject,\n input,\n output,\n signal,\n} from '@angular/core';\nimport type { RiviniaMenuController, RiviniaMenuItemData } from '@rivinia/menu';\nimport { RiviniaMenu } from '@rivinia/menu';\nimport type { RiviniaMenuUiPlacement } from './ui-menu-types';\nimport { computeMenuPosition } from './menu-position';\n\n/** Payload of {@link RiviniaMenuComponent.picked}. */\nexport interface MenuPick {\n readonly id: string;\n}\n\n/** Per-instance panel id counter (client-side; SSR renders closed). */\nlet nextPanelId = 0;\n\n/**\n * Marker + behavior for one navigable row. Registers itself with the\n * enclosing menu on init (unregisters on destroy), exposes `data-rv-id`\n * for focus targeting, and keeps `tabindex`/`aria-disabled` in sync.\n * Non-`rvMenuItem` content (headers, dividers, badges) is skipped by\n * keyboard navigation automatically.\n */\n@Directive({\n selector: '[rvMenuItem]',\n standalone: true,\n host: {\n '[attr.role]': 'computedRole()',\n '[attr.data-rv-id]': 'id()',\n '[attr.data-rv-label]': 'label()',\n '[attr.tabindex]': 'tab()',\n '[attr.aria-disabled]': 'isDisabled()',\n '[attr.data-active]': 'active()',\n '[attr.aria-checked]': 'checkedAttr()',\n '(click)': 'pick()',\n '(pointerenter)': 'hover()',\n },\n})\nexport class RiviniaMenuItem {\n readonly id = input.required<string>();\n readonly disabled = input(false, {\n transform: (v: boolean | string) => v === '' || v === true,\n });\n /**\n * Tri-state selection (e.g. multi-select options). `null` (default) renders\n * a plain `menuitem`; a boolean renders `menuitemcheckbox` + `aria-checked`\n * so screen readers announce the state (the visual check alone is silent).\n */\n readonly checked = input<boolean | null>(null);\n /**\n * Optional explicit typeahead label. When set, typeahead matches on this\n * instead of the row's `textContent` (which can include icons, badges or\n * hints). Defaults to `null` → falls back to `textContent`. Mirrors\n * `@rivinia/select`'s documented rule that an item's accessible name should\n * match its visible text.\n */\n readonly label = input<string | null>(null);\n\n private readonly host = inject(ElementRef<HTMLElement>, { optional: true });\n private readonly menu = inject(RiviniaMenuComponent, { optional: true, skipSelf: true });\n private readonly active = computed(() => this.menu?.controller().activeId() === this.id());\n\n protected tab(): string {\n return this.active() ? '0' : '-1';\n }\n\n protected computedRole(): string {\n return this.checked() === null ? 'menuitem' : 'menuitemcheckbox';\n }\n\n protected checkedAttr(): string | null {\n const checked = this.checked();\n return checked === null ? null : String(checked);\n }\n\n /**\n * `disabled` collides with the native `<button disabled>` property, so a\n * static attribute on a native button element can land on the DOM instead\n * of this input (measured: `<button rvMenuItem disabled>` reported\n * `aria-disabled=\"false\"`). Accept both so the two spellings agree.\n */\n private isDisabled(): boolean {\n if (this.disabled()) return true;\n return this.host?.nativeElement?.disabled === true;\n }\n\n /** Mouse pick: disabled items are ignored (controller refuses them). */\n protected pick(): void {\n this.menu?.pickItem(this.id());\n }\n\n /** Mouse hover follows keyboard focus so highlight tracks the pointer. */\n protected hover(): void {\n this.menu?.hoverItem(this.id());\n }\n\n /** Id currently present in the registry, so renames can be de-registered. */\n private registeredId: string | null = null;\n\n constructor() {\n // `[disabled]` and `id` are ordinary bindings that change at runtime.\n // Registering once in ngOnInit left the registry stale — measured:\n // toggling `[disabled]` on a focused row still let Enter activate it.\n //\n // The initial registration stays in ngOnInit (synchronous) because an\n // effect-based first pass lands after the opening change-detection tick,\n // which raced the menu's own open and produced intermittently empty\n // panels. The effect only handles SUBSEQUENT changes.\n effect(() => {\n const id = this.id();\n const menu = this.menu;\n if (this.registeredId === null) return;\n if (this.registeredId !== id) menu?.unregisterItem(this.registeredId);\n // Re-register unconditionally: covers a `disabled` flip as well as an\n // id rename (register is idempotent by id).\n menu?.registerItem({ id, disabled: this.isDisabled() });\n this.registeredId = id;\n });\n }\n\n ngOnInit(): void {\n this.registeredId = this.id();\n this.menu?.registerItem({ id: this.id(), disabled: this.isDisabled() });\n }\n\n ngOnDestroy(): void {\n if (this.registeredId === null) return;\n this.menu?.unregisterItem(this.registeredId);\n this.registeredId = null;\n }\n}\n\n/**\n * Ready-made dropdown menu. Binds a {@link RiviniaMenu} controller (or owns\n * one when omitted) and projects trigger + panel:\n *\n * ```html\n * <rv-menu [menu]=\"menu\" [placement]=\"'bottom-start'\" (picked)=\"go($event)\">\n * <button rv-menu-trigger><img src=\"avatar.png\" alt=\"Account\" /></button>\n * <div rv-menu-panel>\n * <div class=\"head\">Sophie · sophie@ui.live</div>\n * <button rvMenuItem id=\"profile\">Profile</button>\n * <button rvMenuItem id=\"logout\">Sign out</button>\n * </div>\n * </rv-menu>\n * ```\n *\n * Keyboard: Enter/Space/ArrowDown opens; Escape closes and refocuses the\n * trigger; arrows/Home/End move through enabled items; Tab closes and moves\n * on (non-modal by design). Outside pointer-down closes. SSR renders closed.\n */\n@Component({\n selector: 'rv-menu',\n standalone: true,\n // Deliberately unencapsulated: projected item buttons belong to the\n // consumer's template, and emulated descendant selectors never match\n // projected content (measured in Chrome: `display` stayed `inline-block`).\n // Every rule below is scoped under `.rv-menu-panel` / `.rv-menu-trigger`,\n // so nothing leaks into application styles.\n encapsulation: ViewEncapsulation.None,\n // The declarative `[items]` mode renders its own rows through the SAME\n // directive consumers use, so registration, keyboard and click handling have\n // exactly one implementation.\n imports: [RiviniaMenuItem],\n template: `\n <div class=\"rv-menu-trigger\" (click)=\"onTriggerClick()\" (keydown)=\"onTriggerKey($event)\">\n <ng-content select=\"[rv-menu-trigger], [rvMenuTrigger]\"></ng-content>\n </div>\n @if (controller().open()) {\n <div\n #panel\n class=\"rv-menu-panel\"\n [class]=\"panelClass()\"\n role=\"menu\"\n [attr.id]=\"panelId\"\n [attr.aria-label]=\"menuLabel()\"\n [attr.popover]=\"topLayerSupported ? 'manual' : null\"\n [attr.data-placement]=\"placement()\"\n [attr.data-flipped]=\"flipped()\"\n [attr.data-positioned]=\"positioned()\"\n [attr.data-top-layer]=\"topLayerSupported\"\n (keydown)=\"onPanelKey($event)\"\n >\n @if (dataItems().length > 0) {\n @for (item of dataItems(); track item.id) {\n <button\n rvMenuItem\n [id]=\"item.id\"\n [disabled]=\"item.disabled ?? false\"\n [checked]=\"checkedFor(item.id)\"\n [class]=\"itemClass()\"\n >\n {{ item.label }}\n </button>\n }\n }\n <ng-content select=\"[rv-menu-panel], [rvMenuPanel]\"></ng-content>\n </div>\n }\n `,\n styles: [\n `\n .rv-menu-trigger {\n display: inline-flex;\n align-items: center;\n }\n .rv-menu-panel {\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n .rv-menu-panel hr {\n border: 0;\n border-top: 1px solid var(--rv-menu-border, #e0e0e0);\n margin: 6px 4px;\n }\n /* NOTE: the :host pseudo-class does not exist without encapsulation,\n so the host element is styled directly (still rv-prefixed).\n Relative positioning anchors the no-popover fallback panel. */\n rv-menu {\n position: relative;\n display: inline-block;\n }\n /* Top-layer mode (popover-capable browsers): the panel escapes every\n clipping ancestor and stacking context. Coordinates come from the\n component (fixed + measured + flipped); the absolute placement\n classes below stay as the no-popover fallback. */\n .rv-menu-panel[data-top-layer='true'] {\n position: fixed;\n inset: auto;\n margin: 0;\n }\n .rv-menu-panel[data-positioned='false'] {\n animation: none;\n }\n /* Flipped panels grow out of the opposite edge (matches coordinates). */\n .rv-menu-panel[data-top-layer='true'][data-placement^='bottom'][data-flipped='true'] {\n transform-origin: bottom center;\n }\n .rv-menu-panel[data-top-layer='true'][data-placement^='top'][data-flipped='true'] {\n transform-origin: top center;\n }\n .rv-menu-panel {\n position: absolute;\n z-index: var(--rv-menu-z, 500);\n min-width: var(--rv-menu-width, 16rem);\n max-width: calc(100dvw - 2rem);\n /* Long lists scroll inside the panel instead of leaving the viewport. */\n max-height: min(20rem, calc(100dvh - 2rem));\n overflow-y: auto;\n background: var(--rv-menu-bg, #ffffff);\n color: var(--rv-menu-fg, #111111);\n font-family: var(--rv-menu-font, system-ui, -apple-system, 'Segoe UI', sans-serif);\n border: 1px solid var(--rv-menu-border, #e0e0e0);\n border-radius: var(--rv-menu-radius, 12px);\n box-shadow: var(--rv-menu-shadow, 0 16px 48px rgba(0, 0, 0, 0.22));\n padding: var(--rv-menu-pad, 6px);\n }\n .rv-menu-panel[data-placement='bottom-start'] {\n inset-block-start: calc(100% + 6px);\n inset-inline-start: 0;\n }\n .rv-menu-panel[data-placement='bottom-end'] {\n inset-block-start: calc(100% + 6px);\n inset-inline-end: 0;\n }\n .rv-menu-panel[data-placement='top-start'] {\n inset-block-end: calc(100% + 6px);\n inset-inline-start: 0;\n }\n .rv-menu-panel[data-placement='top-end'] {\n inset-block-end: calc(100% + 6px);\n inset-inline-end: 0;\n }\n .rv-menu-panel [rvmenuitem] {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n min-height: 40px;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 14px;\n line-height: 1.4;\n text-align: start;\n padding: 8px 10px;\n cursor: pointer;\n }\n .rv-menu-panel [rvmenuitem][data-active='true'] {\n background: color-mix(in srgb, currentColor 10%, transparent);\n }\n .rv-menu-panel [rvmenuitem]:hover {\n background: color-mix(in srgb, currentColor 8%, transparent);\n }\n .rv-menu-panel [rvmenuitem]:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: -2px;\n }\n .rv-menu-panel [rvmenuitem][aria-disabled='true'] {\n opacity: 0.45;\n cursor: default;\n }\n /* Optional icons: consumer projects any svg/icon content; the UI only\n owns sizing, alignment and tone so rows stay consistent. */\n .rv-menu-panel [rvmenuitem] svg,\n .rv-menu-panel [rvmenuitem] [data-icon] {\n width: 16px;\n height: 16px;\n flex: none;\n color: var(--rv-menu-muted);\n }\n .rv-menu-panel [rvmenuitem][data-active='true'] svg,\n .rv-menu-panel [rvmenuitem][data-active='true'] [data-icon] {\n color: inherit;\n }\n /* Opening motion (researched: Radix/shadcn/HeadlessUI pattern):\n fade + scale from the trigger edge + a small slide toward rest.\n Origin follows the vertical side so the panel grows out of the\n trigger; duration/easing stay overridable via vars. */\n .rv-menu-panel {\n transform-origin: top center;\n animation: rv-menu-in var(--rv-menu-duration, 160ms)\n var(--rv-menu-easing, cubic-bezier(0.16, 1, 0.3, 1));\n }\n .rv-menu-panel[data-placement='top-start'],\n .rv-menu-panel[data-placement='top-end'] {\n --rv-menu-from-y: 6px;\n transform-origin: bottom center;\n }\n @media (prefers-reduced-motion: reduce) {\n .rv-menu-panel {\n animation: none;\n }\n }\n @keyframes rv-menu-in {\n from {\n opacity: 0;\n transform: translateY(var(--rv-menu-from-y, -6px)) scale(0.96);\n }\n }\n `,\n ],\n})\nexport class RiviniaMenuComponent {\n /**\n * External controller (shared/test double). Omitted → the component owns\n * a private one (simplest embedding).\n */\n readonly menu = input<RiviniaMenuController | null>(null);\n /** Panel placement relative to the trigger. @default 'bottom-start' */\n readonly placement = input<RiviniaMenuUiPlacement>('bottom-start');\n /**\n * Accessible name for the menu panel (`aria-label`). Menus should be named\n * so screen-reader users know what they chose from; omitted → no label.\n *\n * Named `menuLabel` (not `label`) so it cannot be confused with the\n * per-item `label` in {@link RiviniaMenuItemData}.\n */\n readonly menuLabel = input<string | null>(null);\n /**\n * Declarative items. When provided, the panel renders these rows itself —\n * no `rvMenuItem` markup and no controller needed. Mutually exclusive with\n * projected `rvMenuItem` children (see `ambiguous`).\n */\n readonly items = input<readonly RiviniaMenuItemData[] | null>(null);\n /** Extra class(es) on the panel, alongside `.rv-menu-panel`. */\n readonly panelClass = input<string | null>(null);\n /** Extra class(es) on data-driven rows only (projected rows use your markup). */\n readonly itemClass = input<string | null>(null);\n\n /** Fired when the user picks an enabled item (id). Closes first. */\n readonly picked = output<MenuPick>();\n readonly opened = output<void>();\n readonly closed = output<void>();\n\n private readonly owned = new RiviniaMenu();\n private readonly self = inject(ElementRef<HTMLElement>);\n /** Item directives the consumer projected (not the ones we render). */\n private readonly projectedItems = contentChildren(RiviniaMenuItem, { descendants: true });\n /**\n * Stable panel id for `aria-controls`. Instance-unique via module counter;\n * SSR-safe because the panel (and its id) only renders client-side on open.\n */\n protected readonly panelId = `rv-menu-panel-${++nextPanelId}`;\n /** Deferred initial focus: set on open paths, consumed after render. */\n private focusPending = false;\n /** True between an open and the first row registered for it. */\n private pendingActivation = false;\n /**\n * Manual popover support (top-layer). Feature-detected: modern browsers\n * escape clipping ancestors; older ones keep the absolute fallback.\n */\n protected readonly topLayerSupported =\n typeof HTMLElement !== 'undefined' && 'showPopover' in HTMLElement.prototype;\n /** Coordinates applied (gates the enter animation until measured). */\n protected readonly positioned = signal(false);\n /** Whether the panel flipped to the opposite vertical side. */\n protected readonly flipped = signal(false);\n /** Printable-character search buffer; reset by timer after 500ms quiet. */\n private typeBuffer = '';\n private typeTimer: ReturnType<typeof setTimeout> | null = null;\n\n /** Effective controller: external input or the owned fallback. */\n controller(): RiviniaMenuController {\n return this.menu() ?? this.owned;\n }\n\n /** Data-driven rows to render, or empty when the consumer projects markup. */\n protected dataItems(): readonly RiviniaMenuItemData[] {\n return this.ambiguous() ? [] : (this.items() ?? []);\n }\n\n /**\n * Checkbox semantics only when a consumer supplied an external controller\n * with `multi`-style values; a single-select menu keeps plain `menuitem`\n * roles. Kept a method (not a computed) so it stays cheap and reactive via\n * template reads.\n */\n protected checkedFor(id: string): boolean | null {\n const c = this.controller() as RiviniaMenuController & {\n isSelected?: (id: string) => boolean;\n };\n return typeof c.isSelected === 'function' ? c.isSelected(id) : null;\n }\n\n /**\n * `[items]` and projected `rvMenuItem` children are two ways to populate the\n * same panel. Supplying both is ambiguous: we throw instead of silently\n * picking a winner, because a silently-ignored list is a data-loss bug.\n */\n protected ambiguous(): boolean {\n return this.items() !== null && this.projectedItems().length > 0;\n }\n\n /** Called by {@link RiviniaMenuItem} directives. */\n registerItem(item: { id: string; disabled: boolean }): void {\n const c = this.controller();\n c.register(item);\n // Rows can register AFTER the panel opened — the declarative `[items]`\n // path only renders them once `open()` is true, so at open time the\n // registry was still empty and `openMenu('first')` had nothing to\n // activate (measured: `activeId` stayed null, ArrowDown could not move,\n // and the highlight never appeared).\n if (this.pendingActivation && c.open() && c.activeId() === null && !item.disabled) {\n c.activateFirst();\n this.pendingActivation = false;\n }\n }\n\n /** Called by {@link RiviniaMenuItem} directives. */\n unregisterItem(id: string): void {\n this.controller().unregister(id);\n }\n\n constructor() {\n effect(() => {\n if (!this.ambiguous()) return;\n throw new Error(\n 'rv-menu: `[items]` cannot be combined with projected `rvMenuItem` ' +\n 'children — pick one way to populate the panel.',\n );\n });\n effect((onCleanup) => {\n if (typeof window === 'undefined') return;\n if (!this.controller().open()) return;\n const onPointer = (event: PointerEvent) => {\n const host = this.self.nativeElement;\n // Close WITHOUT restoring focus: the pointer is heading somewhere the\n // browser will focus naturally (or nowhere focusable). Stealing it\n // back to the trigger would break that (APG + WCAG focus-visible).\n if (host && !host.contains(event.target as Node)) this.doClose(false);\n };\n document.addEventListener('pointerdown', onPointer);\n // Coordinates go stale on outside scroll/resize — close instead of\n // drifting (native-select-like; scrolling inside the panel is exempt).\n // Like Tab-close, this is APG-consistent non-modal behavior.\n const onScroll = (event: Event) => {\n const panel = this.self.nativeElement.querySelector('.rv-menu-panel');\n if (panel && event.target instanceof Node && panel.contains(event.target)) return;\n this.doClose(false);\n };\n window.addEventListener('scroll', onScroll, true);\n window.addEventListener('resize', onScroll);\n onCleanup(() => {\n document.removeEventListener('pointerdown', onPointer);\n window.removeEventListener('scroll', onScroll, true);\n window.removeEventListener('resize', onScroll);\n });\n });\n // APG menu-button: the trigger exposes haspopup + expanded. The trigger\n // element is consumer-owned, so the attributes sync onto its focusable\n // child (or the wrapper itself as a fallback). aria-controls references\n // the stable panel id (valid while hidden per ARIA).\n effect(() => {\n if (typeof document === 'undefined') return;\n const open = this.controller().open();\n const trigger = this.triggerEl();\n trigger?.setAttribute('aria-haspopup', 'menu');\n trigger?.setAttribute('aria-expanded', String(open));\n trigger?.setAttribute('aria-controls', this.panelId);\n });\n afterRenderEffect(() => {\n if (this.controller().open() && !this.positioned()) this.showAndPosition();\n this.consumePendingFocus();\n });\n }\n\n /**\n * Top-layer presentation: show the manual popover, measure trigger +\n * panel, and pin fixed coordinates (flipping when needed). Runs inside\n * afterRender — before paint, so no flash. Falls back to the absolute CSS\n * placement when popover is unsupported (jsdom included).\n */\n private showAndPosition(): void {\n const root = this.self.nativeElement.querySelector('.rv-menu-panel') as HTMLElement | null;\n if (!root) return;\n if (typeof window === 'undefined' || !this.topLayerSupported) {\n this.positioned.set(true);\n return;\n }\n try {\n const popover = root as HTMLElement & { showPopover?: () => void };\n if (typeof popover.showPopover !== 'function') {\n this.positioned.set(true);\n return;\n }\n popover.showPopover();\n const trigger = this.triggerEl()?.getBoundingClientRect();\n if (!trigger) {\n this.positioned.set(true);\n return;\n }\n const at = computeMenuPosition(\n { left: trigger.left, top: trigger.top, width: trigger.width, height: trigger.height },\n { width: root.offsetWidth, height: root.offsetHeight },\n this.placement(),\n { width: window.innerWidth, height: window.innerHeight },\n this.isRtl(),\n );\n root.style.left = `${at.left}px`;\n root.style.top = `${at.top}px`;\n this.flipped.set(at.flipped);\n this.positioned.set(true);\n } catch {\n this.positioned.set(true);\n }\n }\n\n private isRtl(): boolean {\n try {\n const doc = typeof document === 'undefined' ? null : document.documentElement;\n if (doc?.dir === 'rtl') return true;\n if (doc?.dir === 'ltr') return false;\n return getComputedStyle(this.self.nativeElement).direction === 'rtl';\n } catch {\n return false;\n }\n }\n\n /**\n * Mark an opening pass: deferred focus + re-measurement.\n *\n * `positioned` is reset unconditionally (not only in top-layer mode) so the\n * afterRenderEffect always gets a second pass: once with the panel measured,\n * then again after `positioned` flips back to true — that second pass is what\n * guarantees focus lands even if the first one raced the panel's insertion.\n */\n private markOpening(): void {\n this.focusPending = true;\n // Set on every open so rows that register after the panel rendered can\n // claim the initial activation exactly once.\n this.pendingActivation = true;\n this.flipped.set(false);\n this.positioned.set(false);\n }\n\n protected onTriggerClick(): void {\n const c = this.controller();\n c.toggle();\n if (c.open()) {\n this.opened.emit();\n this.markOpening();\n } else {\n // Focus never left the trigger on toggle-close — restore is a no-op.\n this.closed.emit();\n }\n }\n\n protected onTriggerKey(event: KeyboardEvent): void {\n const c = this.controller();\n switch (event.key) {\n case 'Enter':\n case ' ':\n case 'ArrowDown':\n event.preventDefault();\n c.openMenu('first');\n this.opened.emit();\n this.markOpening();\n break;\n case 'ArrowUp':\n event.preventDefault();\n c.openMenu('last');\n this.opened.emit();\n this.markOpening();\n break;\n case 'Escape':\n if (c.open()) {\n event.preventDefault();\n this.doClose(true);\n }\n break;\n default:\n break;\n }\n }\n\n protected onPanelKey(event: KeyboardEvent): void {\n const c = this.controller();\n switch (event.key) {\n case 'Escape':\n event.preventDefault();\n this.doClose(true);\n break;\n case 'ArrowDown':\n event.preventDefault();\n c.activateNext();\n this.focusActive();\n break;\n case 'ArrowUp':\n event.preventDefault();\n c.activatePrevious();\n this.focusActive();\n break;\n case 'Home':\n event.preventDefault();\n c.activateFirst();\n this.focusActive();\n break;\n case 'End':\n event.preventDefault();\n c.activateLast();\n this.focusActive();\n break;\n case 'Enter':\n case ' ':\n event.preventDefault();\n this.pickFocused();\n break;\n case 'Tab': {\n // Non-modal Tab must not trap focus. The panel is still in the DOM\n // when this event is handled (Angular removes it on the next CD), so\n // the browser's own Tab step would re-enter the panel and then lose\n // focus to <body> when it unmounted (measured on both Chromium and\n // Firefox). So we close, and step focus explicitly from the trigger.\n event.preventDefault();\n const backwards = event.shiftKey;\n this.doClose(false);\n this.focusAdjacentToTrigger(backwards);\n break;\n }\n default:\n this.onTypeahead(event);\n break;\n }\n }\n\n /**\n * APG printable-character search: typing moves focus to the next enabled\n * item whose visible text starts with the buffer. Repeating one character\n * cycles through its matches; the buffer resets after 500ms of quiet.\n * Labels are UI-owned (read from the DOM), so this lives in the component,\n * not the headless controller.\n */\n private resetTypeahead(): void {\n this.typeBuffer = '';\n if (this.typeTimer !== null) {\n clearTimeout(this.typeTimer);\n this.typeTimer = null;\n }\n }\n\n private onTypeahead(event: KeyboardEvent): void {\n if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) return;\n const ch = event.key.toLocaleLowerCase();\n // Repeating one character cycles its matches instead of growing the buffer.\n const cycling = this.typeBuffer !== '' && [...this.typeBuffer].every((c) => c === ch);\n if (!cycling) this.typeBuffer += ch;\n if (this.typeTimer !== null) clearTimeout(this.typeTimer);\n this.typeTimer = setTimeout(() => {\n this.typeBuffer = '';\n this.typeTimer = null;\n }, 500);\n const id = this.matchTypeahead(this.typeBuffer);\n if (id !== null && this.controller().activate(id)) this.focusActive();\n }\n\n private matchTypeahead(buffer: string): string | null {\n const root: HTMLElement | null = this.self.nativeElement.querySelector('.rv-menu-panel');\n if (!root) return null;\n const enabled = new Set(\n this.controller()\n .items()\n .filter((i) => !i.disabled)\n .map((i) => i.id),\n );\n const rows = [...root.querySelectorAll('[data-rv-id]')]\n .map((el) => ({\n id: el.getAttribute('data-rv-id') ?? '',\n text: (el.getAttribute('data-rv-label') ?? el.textContent ?? '').trim().toLocaleLowerCase(),\n }))\n .filter((r) => enabled.has(r.id) && r.text.startsWith(buffer));\n if (rows.length === 0) return null;\n if (new Set(buffer).size === 1) {\n // Repeated character: cycle from after the current item.\n const ids = rows.map((r) => r.id);\n const at = ids.indexOf(this.controller().activeId() ?? '');\n return ids[(at + 1) % ids.length];\n }\n return rows[0].id;\n }\n\n private pickFocused(): void {\n const c = this.controller();\n const id = c.pickActive();\n if (id === null) return;\n // Re-validate before emitting: the active item may have been disabled\n // after it received focus (dynamic `[disabled]`), and Enter must never\n // activate a disabled row (measured: all-disabled menu emitted `picked=one`).\n const item = c.items().find((i) => i.id === id);\n if (!item || item.disabled) return;\n this.doClose(false);\n this.picked.emit({ id });\n }\n\n /**\n * Mouse pick of a concrete item. Called by {@link RiviniaMenuItem}.\n * Disabled/unknown ids are ignored via `activate` (returns false).\n */\n pickItem(id: string): void {\n if (!this.controller().activate(id)) return;\n this.doClose(false);\n this.picked.emit({ id });\n }\n\n /** Mouse hover tracks keyboard focus. Called by {@link RiviniaMenuItem}. */\n hoverItem(id: string): void {\n this.controller().activate(id);\n }\n\n private doClose(restoreFocus: boolean): void {\n const was = this.controller().open();\n // APG: the typeahead buffer is per-open. Keeping it across a close made\n // the next open start mid-word (measured: `due` + `d` matched nothing).\n this.resetTypeahead();\n this.controller().closeMenu();\n if (was) this.closed.emit();\n if (restoreFocus) this.focusTrigger();\n }\n\n /**\n * Initial focus after open. The panel only exists in the DOM after the\n * opening change-detection pass, so focusing synchronously races rendering.\n * afterRenderEffect re-runs when `open` flips (reactive read) and executes\n * after the commit — panel present, in apps and in tests. Never runs on the\n * server (SSR-safe by construction). Skips when focus already sits inside\n * the panel, so later renders never steal it back.\n */\n private consumePendingFocus(): void {\n // `positioned` is read so the effect re-runs after measurement even when\n // `open` never changes again — the second pass is the retry.\n const ready = this.positioned();\n if (!ready || !this.controller().open() || !this.focusPending) return;\n const root: HTMLElement | null = this.self.nativeElement.querySelector('.rv-menu-panel');\n // Panel not committed yet: keep the flag so the next pass retries. Clearing\n // it here is what used to strand focus on <body> (measured: Playwright\n // \"received: inactive\" on every arrow assertion).\n if (!root) return;\n const active = this.activeDocumentElement();\n if (active && root.contains(active)) {\n this.focusPending = false;\n return;\n }\n // No enabled item to focus (empty/all-disabled menu): done, nothing to retry.\n this.focusPending = false;\n this.focusActive();\n }\n\n private focusActive(): void {\n const id = this.controller().activeId();\n const root: HTMLElement | null = this.self.nativeElement.querySelector('.rv-menu-panel');\n if (!root) return;\n const target = id\n ? root.querySelector<HTMLElement>(`[data-rv-id=\"${CSS.escape(id)}\"]`)\n : root.querySelector<HTMLElement>('[rvmenuitem]');\n try {\n target?.focus({ preventScroll: true });\n } catch {\n // Focus is best-effort.\n }\n }\n\n private activeDocumentElement(): Element | null {\n try {\n return typeof document === 'undefined' ? null : document.activeElement;\n } catch {\n return null;\n }\n }\n\n /** Focusable trigger child (APG target) or the wrapper as fallback. */\n private triggerEl(): HTMLElement | null {\n const wrap = this.self.nativeElement.querySelector('.rv-menu-trigger') as HTMLElement | null;\n if (!wrap) return null;\n return (wrap.querySelector('button, a, [tabindex]') as HTMLElement | null) ?? wrap;\n }\n\n /**\n * Move focus to the element before/after the trigger in document tab order.\n *\n * Explicit rather than delegating to the browser: the panel is still\n * mounted when Tab is handled, so the browser's own step re-enters the panel\n * and focus is then lost to <body> when it unmounts (measured on Chromium\n * and Firefox). Leaves focus on the document when nothing is left in that\n * direction.\n */\n private focusAdjacentToTrigger(backwards: boolean): void {\n try {\n const trigger = this.triggerEl();\n const doc = trigger?.ownerDocument;\n if (!trigger || !doc) return;\n const selector =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n const stops = [...doc.querySelectorAll<HTMLElement>(selector)].filter((el) => {\n if (el.hasAttribute('disabled') || el.getAttribute('aria-hidden') === 'true') return false;\n // Rows inside any open panel are part of that menu, not this page flow.\n if (el.closest('.rv-menu-panel')) return false;\n return el.getClientRects().length > 0;\n });\n stops.sort((a, b) =>\n a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1,\n );\n const at = stops.indexOf(trigger);\n if (at === -1) return;\n stops[backwards ? at - 1 : at + 1]?.focus({ preventScroll: true });\n } catch {\n // Focus stepping is best-effort; never break the close path.\n }\n }\n\n private focusTrigger(): void {\n try {\n this.triggerEl()?.focus({ preventScroll: true });\n } catch {\n // Best-effort.\n }\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -22,6 +22,14 @@ export declare class RiviniaMenuItem {
|
|
|
22
22
|
* so screen readers announce the state (the visual check alone is silent).
|
|
23
23
|
*/
|
|
24
24
|
readonly checked: import("@angular/core").InputSignal<boolean | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Optional explicit typeahead label. When set, typeahead matches on this
|
|
27
|
+
* instead of the row's `textContent` (which can include icons, badges or
|
|
28
|
+
* hints). Defaults to `null` → falls back to `textContent`. Mirrors
|
|
29
|
+
* `@rivinia/select`'s documented rule that an item's accessible name should
|
|
30
|
+
* match its visible text.
|
|
31
|
+
*/
|
|
32
|
+
readonly label: import("@angular/core").InputSignal<string | null>;
|
|
25
33
|
private readonly host;
|
|
26
34
|
private readonly menu;
|
|
27
35
|
private readonly active;
|
|
@@ -61,6 +69,11 @@ export declare class RiviniaMenuItem {
|
|
|
61
69
|
"required": false;
|
|
62
70
|
"isSignal": true;
|
|
63
71
|
};
|
|
72
|
+
"label": {
|
|
73
|
+
"alias": "label";
|
|
74
|
+
"required": false;
|
|
75
|
+
"isSignal": true;
|
|
76
|
+
};
|
|
64
77
|
}, {}, never, never, true, never>;
|
|
65
78
|
}
|
|
66
79
|
/**
|