forty-cdk 0.2.0 → 0.3.0

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.
Files changed (220) hide show
  1. package/accordion/README.md +122 -0
  2. package/aspect-ratio/README.md +76 -0
  3. package/avatar/README.md +100 -0
  4. package/breadcrumbs/README.md +49 -0
  5. package/breakpoints/README.md +81 -0
  6. package/button/README.md +49 -0
  7. package/calendar/README.md +458 -0
  8. package/carousel/README.md +358 -0
  9. package/checkbox/README.md +146 -0
  10. package/combobox/README.md +535 -0
  11. package/context-menu/README.md +139 -0
  12. package/date-field/README.md +184 -0
  13. package/date-picker/README.md +338 -0
  14. package/dialog/README.md +388 -0
  15. package/disclosure/README.md +114 -0
  16. package/drag-drop/README.md +359 -0
  17. package/drawer/README.md +560 -0
  18. package/dropdown-menu/README.md +176 -0
  19. package/fesm2022/forty-cdk-accordion.mjs +348 -0
  20. package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
  21. package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
  22. package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
  23. package/fesm2022/forty-cdk-avatar.mjs +308 -0
  24. package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
  25. package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
  26. package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
  27. package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
  28. package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
  29. package/fesm2022/forty-cdk-button.mjs +134 -0
  30. package/fesm2022/forty-cdk-button.mjs.map +1 -0
  31. package/fesm2022/forty-cdk-calendar.mjs +2034 -0
  32. package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
  33. package/fesm2022/forty-cdk-carousel.mjs +968 -0
  34. package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
  35. package/fesm2022/forty-cdk-checkbox.mjs +226 -0
  36. package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
  37. package/fesm2022/forty-cdk-combobox.mjs +2596 -0
  38. package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
  39. package/fesm2022/forty-cdk-context-menu.mjs +413 -0
  40. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
  41. package/fesm2022/forty-cdk-core.mjs +9022 -0
  42. package/fesm2022/forty-cdk-core.mjs.map +1 -0
  43. package/fesm2022/forty-cdk-date-field.mjs +744 -0
  44. package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
  45. package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
  46. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
  47. package/fesm2022/forty-cdk-dialog.mjs +707 -0
  48. package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
  49. package/fesm2022/forty-cdk-disclosure.mjs +190 -0
  50. package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
  51. package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
  52. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
  53. package/fesm2022/forty-cdk-drawer.mjs +1641 -0
  54. package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
  55. package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
  56. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
  57. package/fesm2022/forty-cdk-field.mjs +425 -0
  58. package/fesm2022/forty-cdk-field.mjs.map +1 -0
  59. package/fesm2022/forty-cdk-fieldset.mjs +164 -0
  60. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
  61. package/fesm2022/forty-cdk-file-upload.mjs +221 -0
  62. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
  63. package/fesm2022/forty-cdk-hover-card.mjs +496 -0
  64. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
  65. package/fesm2022/forty-cdk-input.mjs +274 -0
  66. package/fesm2022/forty-cdk-input.mjs.map +1 -0
  67. package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
  68. package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
  69. package/fesm2022/forty-cdk-listbox.mjs +1279 -0
  70. package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
  71. package/fesm2022/forty-cdk-menu.mjs +1439 -0
  72. package/fesm2022/forty-cdk-menu.mjs.map +1 -0
  73. package/fesm2022/forty-cdk-menubar.mjs +787 -0
  74. package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
  75. package/fesm2022/forty-cdk-meter.mjs +211 -0
  76. package/fesm2022/forty-cdk-meter.mjs.map +1 -0
  77. package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
  78. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
  79. package/fesm2022/forty-cdk-number-input.mjs +559 -0
  80. package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
  81. package/fesm2022/forty-cdk-otp-input.mjs +527 -0
  82. package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
  83. package/fesm2022/forty-cdk-pagination.mjs +323 -0
  84. package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
  85. package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
  86. package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
  87. package/fesm2022/forty-cdk-popover.mjs +698 -0
  88. package/fesm2022/forty-cdk-popover.mjs.map +1 -0
  89. package/fesm2022/forty-cdk-progress.mjs +226 -0
  90. package/fesm2022/forty-cdk-progress.mjs.map +1 -0
  91. package/fesm2022/forty-cdk-radio-group.mjs +378 -0
  92. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
  93. package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
  94. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
  95. package/fesm2022/forty-cdk-search.mjs +205 -0
  96. package/fesm2022/forty-cdk-search.mjs.map +1 -0
  97. package/fesm2022/forty-cdk-select.mjs +1661 -0
  98. package/fesm2022/forty-cdk-select.mjs.map +1 -0
  99. package/fesm2022/forty-cdk-separator.mjs +82 -0
  100. package/fesm2022/forty-cdk-separator.mjs.map +1 -0
  101. package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
  102. package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
  103. package/fesm2022/forty-cdk-slider.mjs +803 -0
  104. package/fesm2022/forty-cdk-slider.mjs.map +1 -0
  105. package/fesm2022/forty-cdk-stepper.mjs +886 -0
  106. package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
  107. package/fesm2022/forty-cdk-switch.mjs +137 -0
  108. package/fesm2022/forty-cdk-switch.mjs.map +1 -0
  109. package/fesm2022/forty-cdk-table.mjs +1518 -0
  110. package/fesm2022/forty-cdk-table.mjs.map +1 -0
  111. package/fesm2022/forty-cdk-tabs.mjs +400 -0
  112. package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
  113. package/fesm2022/forty-cdk-time-field.mjs +593 -0
  114. package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
  115. package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
  116. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
  117. package/fesm2022/forty-cdk-toast.mjs +1153 -0
  118. package/fesm2022/forty-cdk-toast.mjs.map +1 -0
  119. package/fesm2022/forty-cdk-toggle.mjs +516 -0
  120. package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
  121. package/fesm2022/forty-cdk-toolbar.mjs +374 -0
  122. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
  123. package/fesm2022/forty-cdk-tooltip.mjs +672 -0
  124. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
  125. package/fesm2022/forty-cdk-tree.mjs +2007 -0
  126. package/fesm2022/forty-cdk-tree.mjs.map +1 -0
  127. package/fesm2022/forty-cdk-virtualization.mjs +1 -1
  128. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  129. package/fesm2022/forty-cdk.mjs +0 -43310
  130. package/fesm2022/forty-cdk.mjs.map +1 -1
  131. package/field/README.md +97 -0
  132. package/fieldset/README.md +86 -0
  133. package/file-upload/README.md +73 -0
  134. package/hover-card/README.md +171 -0
  135. package/input/README.md +156 -0
  136. package/listbox/README.md +424 -0
  137. package/menu/README.md +181 -0
  138. package/menubar/README.md +140 -0
  139. package/meter/README.md +128 -0
  140. package/navigation-menu/README.md +253 -0
  141. package/number-input/README.md +171 -0
  142. package/otp-input/README.md +198 -0
  143. package/package.json +213 -1
  144. package/pagination/README.md +61 -0
  145. package/pane-resizer/README.md +136 -0
  146. package/popover/README.md +262 -0
  147. package/progress/README.md +115 -0
  148. package/radio-group/README.md +129 -0
  149. package/scroll-area/README.md +184 -0
  150. package/search/README.md +42 -0
  151. package/select/README.md +488 -0
  152. package/separator/README.md +84 -0
  153. package/signal-forms/README.md +72 -0
  154. package/slider/README.md +152 -0
  155. package/stepper/README.md +292 -0
  156. package/switch/README.md +116 -0
  157. package/table/README.md +769 -0
  158. package/tabs/README.md +130 -0
  159. package/time-field/README.md +157 -0
  160. package/time-picker/README.md +172 -0
  161. package/toast/README.md +398 -0
  162. package/toggle/README.md +224 -0
  163. package/toolbar/README.md +109 -0
  164. package/tooltip/README.md +274 -0
  165. package/tree/README.md +708 -0
  166. package/types/forty-cdk-accordion.d.ts +242 -0
  167. package/types/forty-cdk-aspect-ratio.d.ts +59 -0
  168. package/types/forty-cdk-avatar.d.ts +133 -0
  169. package/types/forty-cdk-breadcrumbs.d.ts +92 -0
  170. package/types/forty-cdk-breakpoints.d.ts +141 -0
  171. package/types/forty-cdk-button.d.ts +80 -0
  172. package/types/forty-cdk-calendar.d.ts +914 -0
  173. package/types/forty-cdk-carousel.d.ts +530 -0
  174. package/types/forty-cdk-checkbox.d.ts +141 -0
  175. package/types/forty-cdk-combobox.d.ts +1259 -0
  176. package/types/forty-cdk-context-menu.d.ts +313 -0
  177. package/types/forty-cdk-core.d.ts +5774 -0
  178. package/types/forty-cdk-date-field.d.ts +307 -0
  179. package/types/forty-cdk-date-picker.d.ts +622 -0
  180. package/types/forty-cdk-dialog.d.ts +546 -0
  181. package/types/forty-cdk-disclosure.d.ts +127 -0
  182. package/types/forty-cdk-drag-drop.d.ts +456 -0
  183. package/types/forty-cdk-drawer.d.ts +871 -0
  184. package/types/forty-cdk-dropdown-menu.d.ts +242 -0
  185. package/types/forty-cdk-field.d.ts +236 -0
  186. package/types/forty-cdk-fieldset.d.ts +119 -0
  187. package/types/forty-cdk-file-upload.d.ts +124 -0
  188. package/types/forty-cdk-hover-card.d.ts +320 -0
  189. package/types/forty-cdk-input.d.ts +169 -0
  190. package/types/forty-cdk-internationalized-date.d.ts +1 -1
  191. package/types/forty-cdk-listbox.d.ts +513 -0
  192. package/types/forty-cdk-menu.d.ts +629 -0
  193. package/types/forty-cdk-menubar.d.ts +451 -0
  194. package/types/forty-cdk-meter.d.ts +122 -0
  195. package/types/forty-cdk-navigation-menu.d.ts +514 -0
  196. package/types/forty-cdk-number-input.d.ts +319 -0
  197. package/types/forty-cdk-otp-input.d.ts +248 -0
  198. package/types/forty-cdk-pagination.d.ts +214 -0
  199. package/types/forty-cdk-pane-resizer.d.ts +145 -0
  200. package/types/forty-cdk-popover.d.ts +509 -0
  201. package/types/forty-cdk-progress.d.ts +143 -0
  202. package/types/forty-cdk-radio-group.d.ts +222 -0
  203. package/types/forty-cdk-scroll-area.d.ts +258 -0
  204. package/types/forty-cdk-search.d.ts +142 -0
  205. package/types/forty-cdk-select.d.ts +899 -0
  206. package/types/forty-cdk-separator.d.ts +59 -0
  207. package/types/forty-cdk-signal-forms.d.ts +58 -0
  208. package/types/forty-cdk-slider.d.ts +379 -0
  209. package/types/forty-cdk-stepper.d.ts +650 -0
  210. package/types/forty-cdk-switch.d.ts +87 -0
  211. package/types/forty-cdk-table.d.ts +723 -0
  212. package/types/forty-cdk-tabs.d.ts +235 -0
  213. package/types/forty-cdk-time-field.d.ts +307 -0
  214. package/types/forty-cdk-time-picker.d.ts +578 -0
  215. package/types/forty-cdk-toast.d.ts +598 -0
  216. package/types/forty-cdk-toggle.d.ts +310 -0
  217. package/types/forty-cdk-toolbar.d.ts +217 -0
  218. package/types/forty-cdk-tooltip.d.ts +436 -0
  219. package/types/forty-cdk-tree.d.ts +688 -0
  220. package/types/forty-cdk.d.ts +1 -19743
@@ -0,0 +1,787 @@
1
+ import * as i0 from '@angular/core';
2
+ import { signal, computed, InjectionToken, inject, model, input, booleanAttribute, numberAttribute, DestroyRef, Directive, ElementRef } from '@angular/core';
3
+ import { createMenuItemList, InitialFocusState, CloseReasonState, MENU_POSITIONING_DEFAULTS, createDefaults, injectTextDirection, Collection, firstEnabledHost, RovingTabindex, injectTypeahead, createDebouncedAction, reconcileRovingActive, nextEnabledHandle, FOR_MENU_CONTEXT, IdGenerator, hostId, registerHandle, resolveListNavigation } from 'forty-cdk/core';
4
+
5
+ /**
6
+ * Single concrete `ForMenuContext` the `[forMenubar]` root provides to its
7
+ * descendant `[forMenuContent]` and items. It is the one place the bar's
8
+ * "which trigger's menu is open" multiplexing lives: open / anchor / side /
9
+ * ids / placement are all derived from the host's `activeTrigger`, so the same
10
+ * context shape transparently covers whichever trigger's menu is mounted.
11
+ *
12
+ * Item navigation reuses the shared `MenuItemList` (the same item-collection /
13
+ * typeahead / navigate / focus mechanics that back `MenuOverlay`), and the
14
+ * initial-focus / highlight-consume protocol and the close-reason record reuse
15
+ * the shared `InitialFocusState` / `CloseReasonState` micro-helpers (also
16
+ * composed by `MenuOverlay`), so this only has to cover the parts the
17
+ * single-owner overlay can't — the `activeTrigger`-derived multiplexing.
18
+ *
19
+ * A bar-level menu has no per-trigger dismiss / auto-focus outputs and no
20
+ * per-context trigger registration (triggers register with the bar directly),
21
+ * so the corresponding `ForMenuContext` members are deliberately inert here:
22
+ * the outside-interaction emit forwarders have nowhere to surface (the implicit
23
+ * close runs through {@link requestClose}), the auto-focus emitters never veto
24
+ * (bar menus follow APG-prescribed focus movement), and trigger
25
+ * register/unregister are owned by the bar. They stay as documented members of
26
+ * this one implementation rather than scattered no-op literals.
27
+ */
28
+ class MenubarMenuContext {
29
+ #host;
30
+ #itemList = createMenuItemList(() => this.#host.loop());
31
+ #contentEl = signal(null, /* @ts-ignore */
32
+ ...(ngDevMode ? [{ debugName: "#contentEl" }] : /* istanbul ignore next */ []));
33
+ #initialFocusState = new InitialFocusState();
34
+ #closeReasonState = new CloseReasonState();
35
+ open = computed(() => this.#host.value() !== '', /* @ts-ignore */
36
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
37
+ disabled;
38
+ dismissible;
39
+ returnFocus = signal(true).asReadonly();
40
+ dir;
41
+ side = computed(() => this.#host.activeTrigger()?.side(), /* @ts-ignore */
42
+ ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
43
+ align = computed(() => this.#host.activeTrigger()?.align(), /* @ts-ignore */
44
+ ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
45
+ sideOffset = computed(() => this.#host.activeTrigger()?.sideOffset() ?? 4, /* @ts-ignore */
46
+ ...(ngDevMode ? [{ debugName: "sideOffset" }] : /* istanbul ignore next */ []));
47
+ alignOffset = computed(() => this.#host.activeTrigger()?.alignOffset() ?? MENU_POSITIONING_DEFAULTS.alignOffset, /* @ts-ignore */
48
+ ...(ngDevMode ? [{ debugName: "alignOffset" }] : /* istanbul ignore next */ []));
49
+ avoidCollisions = computed(() => this.#host.activeTrigger()?.avoidCollisions() ?? MENU_POSITIONING_DEFAULTS.avoidCollisions, /* @ts-ignore */
50
+ ...(ngDevMode ? [{ debugName: "avoidCollisions" }] : /* istanbul ignore next */ []));
51
+ collisionPadding = computed(() => this.#host.activeTrigger()?.collisionPadding() ?? 8, /* @ts-ignore */
52
+ ...(ngDevMode ? [{ debugName: "collisionPadding" }] : /* istanbul ignore next */ []));
53
+ arrowPadding = computed(() => this.#host.activeTrigger()?.arrowPadding() ?? MENU_POSITIONING_DEFAULTS.arrowPadding, /* @ts-ignore */
54
+ ...(ngDevMode ? [{ debugName: "arrowPadding" }] : /* istanbul ignore next */ []));
55
+ sticky = computed(() => this.#host.activeTrigger()?.sticky() ?? MENU_POSITIONING_DEFAULTS.sticky, /* @ts-ignore */
56
+ ...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
57
+ hideWhenDetached = computed(() => this.#host.activeTrigger()?.hideWhenDetached() ?? MENU_POSITIONING_DEFAULTS.hideWhenDetached, /* @ts-ignore */
58
+ ...(ngDevMode ? [{ debugName: "hideWhenDetached" }] : /* istanbul ignore next */ []));
59
+ clipUntilPositioned = computed(() => this.#host.activeTrigger()?.clipUntilPositioned() ??
60
+ MENU_POSITIONING_DEFAULTS.clipUntilPositioned, /* @ts-ignore */
61
+ ...(ngDevMode ? [{ debugName: "clipUntilPositioned" }] : /* istanbul ignore next */ []));
62
+ loop;
63
+ initialFocus = this.#initialFocusState.target;
64
+ lastCloseReason = this.#closeReasonState.reason;
65
+ triggerId = computed(() => this.#host.activeTrigger()?.triggerId() ?? '', /* @ts-ignore */
66
+ ...(ngDevMode ? [{ debugName: "triggerId" }] : /* istanbul ignore next */ []));
67
+ contentId = computed(() => this.#host.activeTrigger()?.contentId() ?? '', /* @ts-ignore */
68
+ ...(ngDevMode ? [{ debugName: "contentId" }] : /* istanbul ignore next */ []));
69
+ ariaLabel = computed(() => this.#host.activeTrigger()?.ariaLabel() ?? null, /* @ts-ignore */
70
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
71
+ anchor = computed(() => this.#host.activeTrigger()?.host ?? null, /* @ts-ignore */
72
+ ...(ngDevMode ? [{ debugName: "anchor" }] : /* istanbul ignore next */ []));
73
+ trigger;
74
+ content = this.#contentEl.asReadonly();
75
+ parentMenu = null;
76
+ menubar;
77
+ /**
78
+ * Exempt every menubar trigger element so clicking another trigger doesn't
79
+ * fire `pointerDownOutside` (the trigger's own click handler routes the
80
+ * close + open).
81
+ */
82
+ dismissableExemptions = computed(() => this.#host.triggers().map((t) => t.host), /* @ts-ignore */
83
+ ...(ngDevMode ? [{ debugName: "dismissableExemptions" }] : /* istanbul ignore next */ []));
84
+ constructor(host) {
85
+ this.#host = host;
86
+ this.disabled = host.disabled;
87
+ this.dismissible = host.dismissible;
88
+ this.dir = host.dir;
89
+ this.loop = host.loop;
90
+ this.trigger = host.lastTriggerHost;
91
+ this.menubar = host;
92
+ }
93
+ setInitialFocus(target) {
94
+ this.#initialFocusState.setTarget(target);
95
+ }
96
+ /**
97
+ * Read by the bar's `openTrigger` so the next open seeds the resolved
98
+ * initial-focus target and clears the prior close reason. A `'pointer'`
99
+ * `modality` keeps the upcoming programmatic initial focus from reflecting
100
+ * `data-highlighted` on the focused item.
101
+ */
102
+ prepareOpen(initialFocus, modality = 'keyboard') {
103
+ this.#initialFocusState.prepareOpen(initialFocus, modality === 'keyboard');
104
+ this.#closeReasonState.reset();
105
+ }
106
+ /** Read by the bar's pointer-driven close so the content skips its return-focus. */
107
+ setLastCloseReason(reason) {
108
+ this.#closeReasonState.set(reason);
109
+ }
110
+ registerTrigger() {
111
+ // Triggers register with the menubar itself, not with this menu context.
112
+ }
113
+ unregisterTrigger() { }
114
+ registerContent(el) {
115
+ this.#contentEl.set(el);
116
+ this.#host.attachContentPointer(el);
117
+ }
118
+ unregisterContent(el) {
119
+ if (this.#contentEl() === el) {
120
+ this.#contentEl.set(null);
121
+ }
122
+ this.#host.detachContentPointer();
123
+ }
124
+ registerItem(handle) {
125
+ this.#itemList.registerItem(handle);
126
+ }
127
+ unregisterItem(handle) {
128
+ this.#itemList.unregisterItem(handle);
129
+ }
130
+ navigate(currentItem, action) {
131
+ this.#itemList.navigate(currentItem, action);
132
+ }
133
+ handleTypeahead(event) {
134
+ this.#itemList.handleTypeahead(event);
135
+ }
136
+ clearItemHighlights() {
137
+ this.#itemList.clearHighlights();
138
+ }
139
+ focusFirstEnabledItem() {
140
+ return this.#itemList.focusFirstEnabledItem(this.#initialFocusState.consumeHighlight());
141
+ }
142
+ focusLastEnabledItem() {
143
+ return this.#itemList.focusLastEnabledItem(this.#initialFocusState.consumeHighlight());
144
+ }
145
+ toggle() {
146
+ // Without a specific trigger value, toggle from the bar context can only
147
+ // close. Triggers themselves drive the open path via the bar's openTrigger.
148
+ if (this.#host.value() !== '') {
149
+ this.#host.closeOpen();
150
+ }
151
+ }
152
+ openMenu() {
153
+ // Open requires a trigger value — see the bar's openTrigger.
154
+ }
155
+ closeMenu(reason) {
156
+ this.#closeReasonState.set(reason);
157
+ this.#host.closeOpen();
158
+ }
159
+ emitEscapeKeyDown(event) {
160
+ if (!event.defaultPrevented && this.#host.dismissible()) {
161
+ event.stopPropagation();
162
+ this.#closeReasonState.set('escape');
163
+ this.#host.closeOpen();
164
+ }
165
+ }
166
+ // The menubar has no per-trigger `(pointerDownOutside)` / `(focusOutside)` /
167
+ // `(interactOutside)` outputs, so these emit forwarders have nowhere to
168
+ // surface; the implicit close runs through `requestClose` below.
169
+ emitPointerDownOutside() { }
170
+ emitFocusOutside() { }
171
+ emitInteractOutside() { }
172
+ requestClose(reason) {
173
+ if (this.#host.value() === '') {
174
+ return;
175
+ }
176
+ this.#closeReasonState.set(reason);
177
+ this.#host.closeOpen();
178
+ }
179
+ // Bar-level menus follow APG-prescribed focus movement and expose no
180
+ // `(autoFocusOnOpen)` / `(autoFocusOnClose)` outputs, so the vetoes never
181
+ // fire — the content's focus moves always run.
182
+ emitAutoFocusOnOpen() {
183
+ return false;
184
+ }
185
+ emitAutoFocusOnClose() {
186
+ return false;
187
+ }
188
+ }
189
+
190
+ const FOR_MENUBAR_CONTEXT = new InjectionToken('FOR_MENUBAR_CONTEXT');
191
+ function injectMenubarContext(piece) {
192
+ const ctx = inject(FOR_MENUBAR_CONTEXT, { optional: true });
193
+ if (!ctx) {
194
+ throw new Error(`[forty-cdk/menubar] ${piece} must be used inside a [forMenubar] element. ` +
195
+ "If it is declared inside an ng-template, DI resolves at the template's declaration site — " +
196
+ 'not where it is stamped (e.g. via ngTemplateOutlet) — so declare the template inside the ' +
197
+ '[forMenubar] root.');
198
+ }
199
+ return ctx;
200
+ }
201
+
202
+ /**
203
+ * Library fallback for menubar defaults, read at the root injector when no
204
+ * consumer has called `provideForMenubarDefaults`. Exported for the shared
205
+ * defaults contract spec; not re-exported from the primitive's public entry.
206
+ */
207
+ const FOR_MENUBAR_FALLBACK_DEFAULTS = {
208
+ closeDelay: 150,
209
+ };
210
+ const { token, provideDefaults } = createDefaults('FOR_MENUBAR_DEFAULTS', FOR_MENUBAR_FALLBACK_DEFAULTS);
211
+ /** Token holding the resolved menubar defaults for the current scope. */
212
+ const FOR_MENUBAR_DEFAULTS = token;
213
+ /**
214
+ * Configures forty-cdk menubar defaults for this injector scope. Partial
215
+ * overrides inherit unspecified keys from the parent scope (or library
216
+ * defaults at the root).
217
+ */
218
+ function provideForMenubarDefaults(defaults = {}) {
219
+ return provideDefaults(defaults);
220
+ }
221
+
222
+ /**
223
+ * Headless implementation of the
224
+ * [WAI-ARIA Menubar pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/).
225
+ *
226
+ * A horizontal (or vertical) bar of triggers, each opening a dropdown menu.
227
+ * The bar owns "which child menu is open" — opening another implicitly
228
+ * closes the previous. Cross-menu ArrowLeft / ArrowRight navigation,
229
+ * hover-after-first-open (switch siblings instantly, dismiss on hover-leave
230
+ * after `closeDelay`), and roving tabindex among triggers are wired
231
+ * automatically.
232
+ *
233
+ * Surface composition:
234
+ *
235
+ * ```html
236
+ * <div forMenubar [(value)]="open">
237
+ * <button forMenubarTrigger value="file">File</button>
238
+ * @if (open() === 'file') {
239
+ * <div forMenuContent>
240
+ * <button forMenuItem (activate)="newDoc()">New</button>
241
+ * </div>
242
+ * }
243
+ *
244
+ * <button forMenubarTrigger value="edit">Edit</button>
245
+ * @if (open() === 'edit') {
246
+ * <div forMenuContent>…</div>
247
+ * }
248
+ * </div>
249
+ * ```
250
+ *
251
+ * `[forMenuContent]` is the same directive used by `[forDropdownMenu]` /
252
+ * `[forContextMenu]`. The bar provides a multiplexed `ForMenuContext`
253
+ * whose anchor / placement / ids are pulled from whichever trigger is
254
+ * currently active, so the content directive needs no menubar-specific
255
+ * code path.
256
+ */
257
+ class ForMenubar {
258
+ #defaults = inject(FOR_MENUBAR_DEFAULTS);
259
+ /**
260
+ * Two-way bindable. The value of the open trigger, or `''` when none.
261
+ * The `model()` change emitter (`(valueChange)`) fires only on internal
262
+ * transitions (trigger interaction, item activation, Escape, outside
263
+ * dismissal, cross-menu nav), never on consumer writes via `[(value)]`.
264
+ */
265
+ value = model('', /* @ts-ignore */
266
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
267
+ orientation = input('horizontal', /* @ts-ignore */
268
+ ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
269
+ /**
270
+ * Writing direction. When unset (default `null`), the inherited ambient
271
+ * direction is resolved from the nearest ancestor carrying a `dir` attribute
272
+ * (or `<html dir>`), defaulting to `'ltr'`. An explicit `[dir]` always wins.
273
+ * The resolved value is reflected to the host `dir` attribute, swaps
274
+ * cross-menu ArrowLeft / ArrowRight, and is inherited by descendant menus.
275
+ */
276
+ _dirInput = input(null, { ...(ngDevMode ? { debugName: "_dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
277
+ dir = injectTextDirection(this._dirInput);
278
+ loop = input(true, { ...(ngDevMode ? { debugName: "loop" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
279
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
280
+ /**
281
+ * Whether the open menu closes on Escape, an outside interaction, or the
282
+ * pointer leaving the bar. When `false`, the menu stays open until the
283
+ * consumer flips `value` (or a trigger / item interaction switches it).
284
+ * Matches the dismiss contract of `[forDropdownMenu]` / `[forContextMenu]`.
285
+ * Default `true`.
286
+ */
287
+ dismissible = input(true, { ...(ngDevMode ? { debugName: "dismissible" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
288
+ /** Optional accessible name for the menubar. */
289
+ ariaLabel = input(null, /* @ts-ignore */
290
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
291
+ /**
292
+ * ms before the open menu closes after the pointer leaves the bar (and any
293
+ * open menu). Default `150`. The default is read from
294
+ * `provideForMenubarDefaults` for the surrounding scope.
295
+ */
296
+ closeDelay = input(this.#defaults.closeDelay, { ...(ngDevMode ? { debugName: "closeDelay" } : /* istanbul ignore next */ {}), transform: numberAttribute });
297
+ #triggerCollection = new Collection();
298
+ triggers = this.#triggerCollection.items;
299
+ activeTrigger = computed(() => {
300
+ const v = this.value();
301
+ if (v === '') {
302
+ return null;
303
+ }
304
+ return this.#triggerCollection.items().find((t) => t.value() === v) ?? null;
305
+ }, /* @ts-ignore */
306
+ ...(ngDevMode ? [{ debugName: "activeTrigger" }] : /* istanbul ignore next */ []));
307
+ /**
308
+ * The most-recently-active trigger value. Persists past close so the
309
+ * `[forMenuContent]` destroy hook can still target the trigger (by then
310
+ * `value()` is already `''`). Updated synchronously by `openTrigger`
311
+ * and snapshotted in `closeOpen` before clearing `value` — that covers
312
+ * every internal close path (Escape, item activation, outside dismiss).
313
+ */
314
+ #lastValue = signal('', /* @ts-ignore */
315
+ ...(ngDevMode ? [{ debugName: "#lastValue" }] : /* istanbul ignore next */ []));
316
+ /**
317
+ * The most-recently-active trigger host. Persists past close so the
318
+ * multiplexed `[forMenuContent]` destroy hook can still target the trigger
319
+ * (by then `value()` is already `''`). Consumed by {@link MenubarMenuContext}
320
+ * as its return-focus `trigger`.
321
+ */
322
+ lastTriggerHost = computed(() => {
323
+ const v = this.value() || this.#lastValue();
324
+ if (v === '') {
325
+ return null;
326
+ }
327
+ return this.#triggerCollection.items().find((t) => t.value() === v)?.host ?? null;
328
+ }, /* @ts-ignore */
329
+ ...(ngDevMode ? [{ debugName: "lastTriggerHost" }] : /* istanbul ignore next */ []));
330
+ #firstEnabledTriggerHost = computed(() => firstEnabledHost(this.#triggerCollection.items()), /* @ts-ignore */
331
+ ...(ngDevMode ? [{ debugName: "#firstEnabledTriggerHost" }] : /* istanbul ignore next */ []));
332
+ /**
333
+ * Roving-tabindex tracker shared by every menubar trigger, mirroring
334
+ * `ForToolbar` / `ForTabs`. Triggers promote themselves to active on
335
+ * `(focus)` (via {@link setFocusedTrigger}) and read `active()` through
336
+ * {@link tabindexFor}, so re-entry restores the last focused trigger. Before
337
+ * any focus — and on every close — `active()` is `null`/stale and the tab
338
+ * stop falls back to the first enabled trigger. The active pointer is
339
+ * self-healing (a detached / disabled trigger no longer owns the stop).
340
+ */
341
+ roving = new RovingTabindex();
342
+ #triggerTypeahead = injectTypeahead();
343
+ #closeAction = createDebouncedAction(() => this.#closeByPointer());
344
+ #detachContentPointerFn = null;
345
+ constructor() {
346
+ reconcileRovingActive(this.roving, this.#triggerCollection.items);
347
+ inject(DestroyRef).onDestroy(() => {
348
+ this.#closeAction.cancel();
349
+ this.detachContentPointer();
350
+ });
351
+ }
352
+ // -- Multiplexed ForMenuContext for the currently-active menu -------------
353
+ /**
354
+ * Single `ForMenuContext` provided to descendant `[forMenuContent]` and
355
+ * items, implemented by the dedicated {@link MenubarMenuContext} class. Its
356
+ * open / anchor / side / ids fields are derived from `activeTrigger`, so the
357
+ * same context shape transparently covers whichever trigger's menu is
358
+ * mounted. The bar's menu-item navigation reuses the shared `MenuItemList`
359
+ * (the same mechanics that back `MenuOverlay`), so the multiplexing only
360
+ * covers the parts the single-owner overlay can't.
361
+ */
362
+ menuCtx = new MenubarMenuContext(this);
363
+ // -- ForMenubarContext ----------------------------------------------------
364
+ registerTrigger(handle) {
365
+ this.#triggerCollection.register(handle);
366
+ }
367
+ unregisterTrigger(handle) {
368
+ this.#triggerCollection.unregister(handle);
369
+ this.roving.unregister(handle.host);
370
+ }
371
+ triggerFor(value) {
372
+ if (value === '') {
373
+ return null;
374
+ }
375
+ return this.#triggerCollection.items().find((t) => t.value() === value) ?? null;
376
+ }
377
+ tabindexFor(el) {
378
+ const v = this.value();
379
+ if (v !== '') {
380
+ // While a menu is open, only its trigger is tabbable.
381
+ return this.activeTrigger()?.host === el ? 0 : -1;
382
+ }
383
+ // Otherwise the roving tracker (promoted by trigger `(focus)`) holds the
384
+ // tab stop, falling back to the first enabled trigger when nothing has
385
+ // been focused yet or the active trigger went stale.
386
+ if (this.roving.hasActive()) {
387
+ return this.roving.tabindexFor(el);
388
+ }
389
+ return this.#firstEnabledTriggerHost() === el ? 0 : -1;
390
+ }
391
+ setFocusedTrigger(el) {
392
+ this.roving.setActive(el);
393
+ }
394
+ navigateTriggers(currentTrigger, action) {
395
+ if (this.disabled()) {
396
+ return;
397
+ }
398
+ const target = nextEnabledHandle(this.#triggerCollection.items(), currentTrigger, action, {
399
+ loop: this.loop(),
400
+ });
401
+ target?.host.focus();
402
+ }
403
+ openTrigger(value, initialFocus, modality = 'keyboard') {
404
+ if (this.disabled()) {
405
+ return;
406
+ }
407
+ const handle = this.triggerFor(value);
408
+ if (!handle || handle.disabled()) {
409
+ return;
410
+ }
411
+ this.menuCtx.prepareOpen(initialFocus, modality);
412
+ this.#lastValue.set(value);
413
+ if (this.value() !== value) {
414
+ this.value.set(value);
415
+ }
416
+ }
417
+ closeOpen() {
418
+ const current = this.value();
419
+ if (current === '') {
420
+ return;
421
+ }
422
+ // Snapshot so [forMenuContent]'s destroy hook can return focus to the
423
+ // just-closed trigger via menuCtx.trigger (which falls back to #lastValue
424
+ // once value() is '').
425
+ this.#lastValue.set(current);
426
+ this.value.set('');
427
+ }
428
+ switchToSibling(direction) {
429
+ if (this.disabled()) {
430
+ return;
431
+ }
432
+ const items = this.#triggerCollection.items();
433
+ if (items.length === 0) {
434
+ return;
435
+ }
436
+ const currentValue = this.value();
437
+ const currentIndex = items.findIndex((t) => t.value() === currentValue);
438
+ const target = nextEnabledHandle(items, currentIndex < 0 ? 0 : currentIndex, direction, {
439
+ loop: this.loop(),
440
+ });
441
+ if (target === null) {
442
+ return;
443
+ }
444
+ this.openTrigger(target.value(), 'first');
445
+ }
446
+ pointerEnterTrigger(value) {
447
+ if (this.disabled()) {
448
+ return;
449
+ }
450
+ // Entering any trigger aborts a pending hover-leave close — the pointer
451
+ // is still travelling across the bar, so keep the open menu alive.
452
+ this.#closeAction.cancel();
453
+ // Hover-after-open opens siblings instantly; while no menu is
454
+ // open, hover does nothing (first open requires keyboard / click).
455
+ if (this.value() === '' || this.value() === value) {
456
+ return;
457
+ }
458
+ this.openTrigger(value, 'first', 'pointer');
459
+ }
460
+ /**
461
+ * Abort a scheduled hover-leave close, if any. Bound to the bar's
462
+ * `pointerenter` / `pointermove` so re-entering the bar (or its open menu)
463
+ * keeps the menu open. Also called by the multiplexed content's
464
+ * `pointerenter` so travelling from a trigger into its portaled menu does
465
+ * not trip the close timer.
466
+ */
467
+ cancelPendingClose() {
468
+ this.#closeAction.cancel();
469
+ }
470
+ /**
471
+ * The pointer left the bar. Schedule a close after `closeDelay`; entering
472
+ * the bar, a trigger, or the open menu again cancels it. No-op while no
473
+ * menu is open or for non-mouse pointers (touch / pen drive open/close via
474
+ * tap, not hover).
475
+ */
476
+ onBarPointerLeave(event) {
477
+ if (event.pointerType !== '' && event.pointerType !== 'mouse') {
478
+ return;
479
+ }
480
+ this.#scheduleCloseByPointer();
481
+ }
482
+ #scheduleCloseByPointer() {
483
+ this.#closeAction.cancel();
484
+ // A non-dismissible menu stays pinned open: hover-leave is held to the
485
+ // same contract as Escape / outside interaction.
486
+ if (this.value() === '' || !this.dismissible()) {
487
+ return;
488
+ }
489
+ this.#closeAction.schedule(this.closeDelay());
490
+ }
491
+ #closeByPointer() {
492
+ if (this.value() === '') {
493
+ return;
494
+ }
495
+ this.menuCtx.setLastCloseReason('pointerDownOutside');
496
+ this.closeOpen();
497
+ }
498
+ /**
499
+ * Attach the bar's hover-keepalive listeners to the mounted multiplexed
500
+ * content element so travelling from a trigger into its portaled menu keeps
501
+ * the open chain alive. Called by {@link MenubarMenuContext} on content
502
+ * registration.
503
+ */
504
+ attachContentPointer(el) {
505
+ this.detachContentPointer();
506
+ const onEnter = (event) => {
507
+ if (event.pointerType !== '' && event.pointerType !== 'mouse') {
508
+ return;
509
+ }
510
+ this.#closeAction.cancel();
511
+ };
512
+ const onLeave = (event) => {
513
+ if (event.pointerType !== '' && event.pointerType !== 'mouse') {
514
+ return;
515
+ }
516
+ this.#scheduleCloseByPointer();
517
+ };
518
+ el.addEventListener('pointerenter', onEnter);
519
+ el.addEventListener('pointerleave', onLeave);
520
+ this.#detachContentPointerFn = () => {
521
+ el.removeEventListener('pointerenter', onEnter);
522
+ el.removeEventListener('pointerleave', onLeave);
523
+ };
524
+ }
525
+ /** Detach the bar's hover-keepalive listeners. Called by {@link MenubarMenuContext}. */
526
+ detachContentPointer() {
527
+ this.#detachContentPointerFn?.();
528
+ this.#detachContentPointerFn = null;
529
+ }
530
+ handleTriggerTypeahead(event) {
531
+ if (!this.#triggerTypeahead.handle(event)) {
532
+ return;
533
+ }
534
+ const buffer = this.#triggerTypeahead.buffer().toLowerCase();
535
+ if (!buffer) {
536
+ return;
537
+ }
538
+ const items = this.#triggerCollection.items();
539
+ const match = items.find((t) => {
540
+ if (t.disabled()) {
541
+ return false;
542
+ }
543
+ const text = t.host.textContent ?? '';
544
+ return text.trim().toLowerCase().startsWith(buffer);
545
+ });
546
+ match?.host.focus();
547
+ }
548
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenubar, deps: [], target: i0.ɵɵFactoryTarget.Directive });
549
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForMenubar, isStandalone: true, selector: "[forMenubar]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, _dirInput: { classPropertyName: "_dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, loop: { classPropertyName: "loop", publicName: "loop", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, closeDelay: { classPropertyName: "closeDelay", publicName: "closeDelay", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { attributes: { "role": "menubar" }, listeners: { "pointerenter": "cancelPendingClose()", "pointermove": "cancelPendingClose()", "pointerleave": "onBarPointerLeave($event)" }, properties: { "attr.aria-orientation": "orientation()", "attr.aria-label": "ariaLabel() || null", "attr.aria-disabled": "disabled() ? \"true\" : null", "attr.data-state": "value() === \"\" ? \"closed\" : \"open\"", "attr.data-orientation": "orientation()", "attr.data-disabled": "disabled() ? \"\" : null", "attr.dir": "dir()" } }, providers: [
550
+ { provide: FOR_MENUBAR_CONTEXT, useExisting: ForMenubar },
551
+ {
552
+ provide: FOR_MENU_CONTEXT,
553
+ useFactory: (m) => m.menuCtx,
554
+ deps: [ForMenubar],
555
+ },
556
+ ], exportAs: ["forMenubar"], ngImport: i0 });
557
+ }
558
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenubar, decorators: [{
559
+ type: Directive,
560
+ args: [{
561
+ selector: '[forMenubar]',
562
+ exportAs: 'forMenubar',
563
+ host: {
564
+ role: 'menubar',
565
+ '[attr.aria-orientation]': 'orientation()',
566
+ '[attr.aria-label]': 'ariaLabel() || null',
567
+ '[attr.aria-disabled]': 'disabled() ? "true" : null',
568
+ '[attr.data-state]': 'value() === "" ? "closed" : "open"',
569
+ '[attr.data-orientation]': 'orientation()',
570
+ '[attr.data-disabled]': 'disabled() ? "" : null',
571
+ '[attr.dir]': 'dir()',
572
+ '(pointerenter)': 'cancelPendingClose()',
573
+ '(pointermove)': 'cancelPendingClose()',
574
+ '(pointerleave)': 'onBarPointerLeave($event)',
575
+ },
576
+ providers: [
577
+ { provide: FOR_MENUBAR_CONTEXT, useExisting: ForMenubar },
578
+ {
579
+ provide: FOR_MENU_CONTEXT,
580
+ useFactory: (m) => m.menuCtx,
581
+ deps: [ForMenubar],
582
+ },
583
+ ],
584
+ }]
585
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], _dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], loop: [{ type: i0.Input, args: [{ isSignal: true, alias: "loop", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], closeDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeDelay", required: false }] }] } });
586
+
587
+ /**
588
+ * One trigger inside `[forMenubar]`. Apply on a `<button>` so Space / Enter
589
+ * dispatch native click events that route through `(click)`.
590
+ *
591
+ * Wires `role="menuitem"`, `aria-haspopup="menu"`, `aria-expanded`, and
592
+ * `aria-controls`. Participates in the menubar's roving tabindex (only the
593
+ * active trigger is tabbable; the rest are `tabindex="-1"`).
594
+ *
595
+ * Each trigger registers its per-menu floating-ui inputs (side, align,
596
+ * sideOffset, …) with the menubar so the multiplexed `[forMenuContent]`
597
+ * reads the right values when this trigger's menu is the one currently
598
+ * open.
599
+ *
600
+ * Keyboard:
601
+ * - **Click / Enter / Space** — toggle this trigger's menu (focus first item on open).
602
+ * - **ArrowDown** — open and focus first item.
603
+ * - **ArrowUp** — open and focus last item.
604
+ * - **ArrowLeft / ArrowRight** — move focus across sibling triggers (RTL inverts).
605
+ * - **Home / End** — jump to first / last enabled trigger.
606
+ * - **Typeahead** — printable keys focus the first sibling trigger whose label
607
+ * starts with the buffered string.
608
+ *
609
+ * While some other trigger's menu is open, hovering this trigger opens it
610
+ * immediately (no delay) — "first open is intentional, subsequent
611
+ * are hover". Keyboard focus alone never opens a menu.
612
+ *
613
+ * Pointer-driven opens (click — detected by the `pointerdown` preceding it —
614
+ * and hover-after-open) move focus to the menu's first item without
615
+ * highlighting it; keyboard activation (Enter / Space / ArrowDown / ArrowUp)
616
+ * highlights the focused item.
617
+ */
618
+ class ForMenubarTrigger {
619
+ menubar = injectMenubarContext('ForMenubarTrigger');
620
+ #host = inject(ElementRef);
621
+ #idGen = inject(IdGenerator);
622
+ #pointerActivation = false;
623
+ /** Identifies this trigger in the menubar's `value` model. */
624
+ value = input.required(/* @ts-ignore */
625
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
626
+ /** Per-trigger disabled, in addition to the menubar's `disabled`. */
627
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
628
+ // -- Floating-ui inputs (forwarded to the multiplexed [forMenuContent]) --
629
+ /** Anchor side. Default `'bottom'`. */
630
+ side = input('bottom', /* @ts-ignore */
631
+ ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
632
+ /** Alignment along `side`. Default `'start'`. */
633
+ align = input('start', /* @ts-ignore */
634
+ ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
635
+ /** Gap (px) along the main axis. Default `4`. */
636
+ sideOffset = input(4, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
637
+ /** Gap (px) along the cross axis. Default `0`. */
638
+ alignOffset = input(0, { ...(ngDevMode ? { debugName: "alignOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
639
+ avoidCollisions = input(true, { ...(ngDevMode ? { debugName: "avoidCollisions" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
640
+ collisionPadding = input(8, { ...(ngDevMode ? { debugName: "collisionPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
641
+ arrowPadding = input(0, { ...(ngDevMode ? { debugName: "arrowPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
642
+ sticky = input('partial', /* @ts-ignore */
643
+ ...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
644
+ hideWhenDetached = input(false, { ...(ngDevMode ? { debugName: "hideWhenDetached" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
645
+ /**
646
+ * When `true` (default), the content is clipped until floating-ui resolves
647
+ * its first position, preventing a flash at the viewport corner. Set to
648
+ * `false` so a dramatic `animate.enter` plays from its first frame (the
649
+ * surface may flash briefly at the unresolved position while positioning
650
+ * computes).
651
+ */
652
+ clipUntilPositioned = input(true, { ...(ngDevMode ? { debugName: "clipUntilPositioned" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
653
+ /** Manual `aria-label` on `[forMenuContent]` when the trigger isn't a meaningful name. */
654
+ ariaLabel = input(null, /* @ts-ignore */
655
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
656
+ triggerId = hostId('for-menubar-trigger');
657
+ contentId = signal(this.#idGen.next('for-menubar-content'), /* @ts-ignore */
658
+ ...(ngDevMode ? [{ debugName: "contentId" }] : /* istanbul ignore next */ []));
659
+ effectiveDisabled = computed(() => this.disabled() || this.menubar.disabled(), /* @ts-ignore */
660
+ ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
661
+ isOpen = computed(() => this.menubar.value() === this.value(), /* @ts-ignore */
662
+ ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
663
+ tabindex = computed(() => {
664
+ if (this.effectiveDisabled()) {
665
+ return -1;
666
+ }
667
+ return this.menubar.tabindexFor(this.#host.nativeElement);
668
+ }, /* @ts-ignore */
669
+ ...(ngDevMode ? [{ debugName: "tabindex" }] : /* istanbul ignore next */ []));
670
+ constructor() {
671
+ const handle = {
672
+ host: this.#host.nativeElement,
673
+ value: this.value,
674
+ disabled: this.effectiveDisabled,
675
+ triggerId: this.triggerId,
676
+ contentId: this.contentId,
677
+ side: this.side,
678
+ align: this.align,
679
+ sideOffset: this.sideOffset,
680
+ alignOffset: this.alignOffset,
681
+ avoidCollisions: this.avoidCollisions,
682
+ collisionPadding: this.collisionPadding,
683
+ arrowPadding: this.arrowPadding,
684
+ sticky: this.sticky,
685
+ hideWhenDetached: this.hideWhenDetached,
686
+ clipUntilPositioned: this.clipUntilPositioned,
687
+ ariaLabel: this.ariaLabel,
688
+ };
689
+ registerHandle(handle, (h) => this.menubar.registerTrigger(h), (h) => this.menubar.unregisterTrigger(h));
690
+ }
691
+ onPointerDown() {
692
+ this.#pointerActivation = true;
693
+ }
694
+ onClick() {
695
+ const modality = this.#pointerActivation ? 'pointer' : 'keyboard';
696
+ this.#pointerActivation = false;
697
+ if (this.effectiveDisabled()) {
698
+ return;
699
+ }
700
+ if (this.isOpen()) {
701
+ this.menubar.closeOpen();
702
+ }
703
+ else {
704
+ this.menubar.openTrigger(this.value(), 'first', modality);
705
+ }
706
+ }
707
+ onKeyDown(event) {
708
+ this.#pointerActivation = false;
709
+ if (this.effectiveDisabled()) {
710
+ return;
711
+ }
712
+ // Open / focus-on-open first.
713
+ if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
714
+ event.preventDefault();
715
+ this.menubar.openTrigger(this.value(), 'first');
716
+ return;
717
+ }
718
+ if (event.key === 'ArrowUp') {
719
+ event.preventDefault();
720
+ this.menubar.openTrigger(this.value(), 'last');
721
+ return;
722
+ }
723
+ // Trigger-row navigation (cross-axis to the open key, per APG).
724
+ const action = resolveListNavigation(event, {
725
+ orientation: this.menubar.orientation(),
726
+ dir: this.menubar.dir(),
727
+ });
728
+ if (action) {
729
+ event.preventDefault();
730
+ this.menubar.navigateTriggers(this.#host.nativeElement, action);
731
+ return;
732
+ }
733
+ // Typeahead among sibling triggers.
734
+ this.menubar.handleTriggerTypeahead(event);
735
+ }
736
+ onFocus() {
737
+ if (this.effectiveDisabled()) {
738
+ return;
739
+ }
740
+ // Roving tab stop follows focus — but focus alone never opens a menu.
741
+ // Keyboard traversal across triggers (ArrowLeft / ArrowRight, typeahead)
742
+ // only moves focus; opening is reserved for hover (pointerenter),
743
+ // click, and the open keys. Cross-menu nav while a menu is open is driven
744
+ // by the items, not by trigger focus.
745
+ this.menubar.setFocusedTrigger(this.#host.nativeElement);
746
+ }
747
+ onPointerEnter() {
748
+ if (this.effectiveDisabled()) {
749
+ return;
750
+ }
751
+ this.menubar.pointerEnterTrigger(this.value());
752
+ }
753
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenubarTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
754
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForMenubarTrigger, isStandalone: true, selector: "[forMenubarTrigger]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, sideOffset: { classPropertyName: "sideOffset", publicName: "sideOffset", isSignal: true, isRequired: false, transformFunction: null }, alignOffset: { classPropertyName: "alignOffset", publicName: "alignOffset", isSignal: true, isRequired: false, transformFunction: null }, avoidCollisions: { classPropertyName: "avoidCollisions", publicName: "avoidCollisions", isSignal: true, isRequired: false, transformFunction: null }, collisionPadding: { classPropertyName: "collisionPadding", publicName: "collisionPadding", isSignal: true, isRequired: false, transformFunction: null }, arrowPadding: { classPropertyName: "arrowPadding", publicName: "arrowPadding", isSignal: true, isRequired: false, transformFunction: null }, sticky: { classPropertyName: "sticky", publicName: "sticky", isSignal: true, isRequired: false, transformFunction: null }, hideWhenDetached: { classPropertyName: "hideWhenDetached", publicName: "hideWhenDetached", isSignal: true, isRequired: false, transformFunction: null }, clipUntilPositioned: { classPropertyName: "clipUntilPositioned", publicName: "clipUntilPositioned", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menuitem", "type": "button" }, listeners: { "pointerdown": "onPointerDown()", "click": "onClick()", "keydown": "onKeyDown($event)", "focus": "onFocus()", "pointerenter": "onPointerEnter()" }, properties: { "id": "triggerId()", "attr.tabindex": "tabindex()", "attr.aria-haspopup": "\"menu\"", "attr.aria-expanded": "isOpen() ? \"true\" : \"false\"", "attr.aria-controls": "isOpen() ? contentId() : null", "attr.aria-disabled": "effectiveDisabled() ? \"true\" : null", "attr.data-state": "isOpen() ? \"open\" : \"closed\"", "attr.data-disabled": "effectiveDisabled() ? \"\" : null", "attr.data-orientation": "menubar.orientation()" } }, exportAs: ["forMenubarTrigger"], ngImport: i0 });
755
+ }
756
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenubarTrigger, decorators: [{
757
+ type: Directive,
758
+ args: [{
759
+ selector: '[forMenubarTrigger]',
760
+ exportAs: 'forMenubarTrigger',
761
+ host: {
762
+ role: 'menuitem',
763
+ type: 'button',
764
+ '[id]': 'triggerId()',
765
+ '[attr.tabindex]': 'tabindex()',
766
+ '[attr.aria-haspopup]': '"menu"',
767
+ '[attr.aria-expanded]': 'isOpen() ? "true" : "false"',
768
+ '[attr.aria-controls]': 'isOpen() ? contentId() : null',
769
+ '[attr.aria-disabled]': 'effectiveDisabled() ? "true" : null',
770
+ '[attr.data-state]': 'isOpen() ? "open" : "closed"',
771
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
772
+ '[attr.data-orientation]': 'menubar.orientation()',
773
+ '(pointerdown)': 'onPointerDown()',
774
+ '(click)': 'onClick()',
775
+ '(keydown)': 'onKeyDown($event)',
776
+ '(focus)': 'onFocus()',
777
+ '(pointerenter)': 'onPointerEnter()',
778
+ },
779
+ }]
780
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], sideOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "sideOffset", required: false }] }], alignOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignOffset", required: false }] }], avoidCollisions: [{ type: i0.Input, args: [{ isSignal: true, alias: "avoidCollisions", required: false }] }], collisionPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "collisionPadding", required: false }] }], arrowPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "arrowPadding", required: false }] }], sticky: [{ type: i0.Input, args: [{ isSignal: true, alias: "sticky", required: false }] }], hideWhenDetached: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideWhenDetached", required: false }] }], clipUntilPositioned: [{ type: i0.Input, args: [{ isSignal: true, alias: "clipUntilPositioned", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
781
+
782
+ /**
783
+ * Generated bundle index. Do not edit.
784
+ */
785
+
786
+ export { FOR_MENUBAR_CONTEXT, FOR_MENUBAR_DEFAULTS, ForMenubar, ForMenubarTrigger, provideForMenubarDefaults };
787
+ //# sourceMappingURL=forty-cdk-menubar.mjs.map