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,1439 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, ElementRef, Directive, input, booleanAttribute, computed, signal, output, InjectionToken, model, PLATFORM_ID, numberAttribute, DestroyRef } from '@angular/core';
3
+ import { injectMenuContext, registerHandle, injectOverlayShell, emitVetoableEvent, resolveListNavigation, createVetoableEvent, registerA11yName, createDefaults, MenuOverlayHost, MENU_POSITIONING_DEFAULTS, createMenuOverlay, createDebouncedAction, FOR_MENU_CONTEXT, resolveGraceSide, buildSubmenuGracePolygon, attachPointerGrace } from 'forty-cdk/core';
4
+ export { FOR_MENU_CONTEXT } from 'forty-cdk/core';
5
+ import { isPlatformBrowser } from '@angular/common';
6
+
7
+ /**
8
+ * The menu surface. Carries `role="menu"`, is portaled to `document.body`,
9
+ * and is positioned by `@floating-ui/dom` against the registered anchor —
10
+ * the trigger button for `[forDropdownMenu]`, or a virtual pointer element
11
+ * for `[forContextMenu]`.
12
+ *
13
+ * The directive does not manage DOM presence — wrap with
14
+ * `@if (open())` so `animate.enter` / `animate.leave` fire on the natural
15
+ * mount cycle. While mounted, a `DismissableLayer` activates (Escape,
16
+ * pointer-down outside, focus outside) and initial focus is sent to the
17
+ * first or last menu item per the trigger's hint.
18
+ *
19
+ * The trigger element is exempt from the layer's outside-pointer checks,
20
+ * so clicking the trigger again routes through its own toggle handler
21
+ * without spuriously closing.
22
+ *
23
+ * The lifecycle (positioner + dismissable layer + initial focus + return
24
+ * focus) is owned by the shared `injectOverlayShell` helper.
25
+ *
26
+ * Navigation is vertical-only (Up / Down between items, per the APG Menu
27
+ * pattern), so the surface reflects `aria-orientation="vertical"` explicitly.
28
+ * Horizontal menus are out of scope; a horizontal *bar* of menus is modelled
29
+ * by `[forMenubar]` instead.
30
+ */
31
+ class ForMenuContent {
32
+ ctx = injectMenuContext('ForMenuContent');
33
+ #host = inject(ElementRef);
34
+ constructor() {
35
+ registerHandle(this.#host.nativeElement, (el) => this.ctx.registerContent(el), (el) => this.ctx.unregisterContent(el));
36
+ injectOverlayShell({
37
+ positioner: {
38
+ kind: 'floating',
39
+ reference: this.ctx.anchor,
40
+ open: this.ctx.open,
41
+ side: this.ctx.side,
42
+ align: this.ctx.align,
43
+ sideOffset: this.ctx.sideOffset,
44
+ alignOffset: this.ctx.alignOffset,
45
+ avoidCollisions: this.ctx.avoidCollisions,
46
+ collisionPadding: this.ctx.collisionPadding,
47
+ arrowPadding: this.ctx.arrowPadding,
48
+ sticky: this.ctx.sticky,
49
+ hideWhenDetached: this.ctx.hideWhenDetached,
50
+ clipUntilPositioned: this.ctx.clipUntilPositioned,
51
+ },
52
+ dismiss: {
53
+ dismissible: this.ctx.dismissible,
54
+ requestClose: (reason) => this.ctx.requestClose(reason),
55
+ emitEscapeKeyDown: (event) => this.ctx.emitEscapeKeyDown(event),
56
+ emitPointerDownOutside: (veto) => this.ctx.emitPointerDownOutside(veto),
57
+ emitFocusOutside: (veto) => this.ctx.emitFocusOutside(veto),
58
+ emitInteractOutside: (veto) => this.ctx.emitInteractOutside(veto),
59
+ // DropdownMenu's trigger is exempt (its own click handler toggles —
60
+ // without exemption pointer-down-outside would race and double-close).
61
+ // ContextMenu exempts nothing so left-clicks on the region close the
62
+ // menu like any other outside click.
63
+ exemptElements: () => this.ctx.dismissableExemptions(),
64
+ },
65
+ // Primitive-owned move: focusFirstEnabledItem / focusLastEnabledItem
66
+ // each return `true` on success. The shell falls back to focusing the
67
+ // host element on miss, which mirrors the previous hand-rolled code.
68
+ initialFocus: {
69
+ move: () => this.ctx.initialFocus() === 'last'
70
+ ? this.ctx.focusLastEnabledItem()
71
+ : this.ctx.focusFirstEnabledItem(),
72
+ veto: () => this.ctx.emitAutoFocusOnOpen(),
73
+ },
74
+ returnFocus: {
75
+ enabled: this.ctx.returnFocus,
76
+ target: () => this.ctx.trigger(),
77
+ // `(autoFocusOnClose)` lets the consumer veto the return-focus.
78
+ veto: () => this.ctx.emitAutoFocusOnClose(),
79
+ // Skip on `'tab'` closes — Tab already moved focus to the trigger
80
+ // and let the browser advance from there; re-focusing would steal
81
+ // it back.
82
+ skip: () => this.ctx.lastCloseReason() === 'tab',
83
+ },
84
+ });
85
+ }
86
+ /**
87
+ * Hover-follows-pointer: the pointer left the menu surface, so drop the
88
+ * highlight from whichever item the pointer was over. Focus is left where it
89
+ * is (anchored on the item for keyboard navigation); only `data-highlighted`
90
+ * clears. Gated to mouse — touch / pen never hover.
91
+ */
92
+ onPointerLeave(event) {
93
+ if (event.pointerType !== '' && event.pointerType !== 'mouse') {
94
+ return;
95
+ }
96
+ this.ctx.clearItemHighlights();
97
+ }
98
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuContent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
99
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForMenuContent, isStandalone: true, selector: "[forMenuContent], [forMenuSubContent]", host: { attributes: { "role": "menu", "tabindex": "-1" }, listeners: { "pointerleave": "onPointerLeave($event)" }, properties: { "id": "ctx.contentId()", "attr.aria-labelledby": "ctx.ariaLabel() ? null : ctx.triggerId()", "attr.aria-label": "ctx.ariaLabel() || null", "attr.aria-orientation": "\"vertical\"", "attr.data-state": "ctx.open() ? \"open\" : \"closed\"" } }, exportAs: ["forMenuContent"], ngImport: i0 });
100
+ }
101
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuContent, decorators: [{
102
+ type: Directive,
103
+ args: [{
104
+ // The same directive serves submenu content too — the behavior is identical
105
+ // (the injected ctx is the [forMenuSub] in that case). The extra selector is
106
+ // an alias for template readability.
107
+ selector: '[forMenuContent], [forMenuSubContent]',
108
+ exportAs: 'forMenuContent',
109
+ host: {
110
+ role: 'menu',
111
+ '[id]': 'ctx.contentId()',
112
+ '[attr.aria-labelledby]': 'ctx.ariaLabel() ? null : ctx.triggerId()',
113
+ '[attr.aria-label]': 'ctx.ariaLabel() || null',
114
+ '[attr.aria-orientation]': '"vertical"',
115
+ '[attr.data-state]': 'ctx.open() ? "open" : "closed"',
116
+ tabindex: '-1',
117
+ '(pointerleave)': 'onPointerLeave($event)',
118
+ },
119
+ }]
120
+ }], ctorParameters: () => [] });
121
+
122
+ /**
123
+ * Handle ArrowLeft / ArrowRight on a menu item (regular, checkbox, radio,
124
+ * or submenu trigger when its open-key is the orthogonal arrow). Returns
125
+ * `true` when the event was consumed (caller should stop further handling).
126
+ *
127
+ * Routing precedence:
128
+ * - Inside a submenu (`ctx.parentMenu != null`): the close-submenu key
129
+ * (ArrowLeft in LTR, ArrowRight in RTL) closes the submenu. The other
130
+ * horizontal arrow propagates to the menubar (if any) only after the
131
+ * submenu chain has been collapsed — so this helper returns `false` for
132
+ * it and the caller can let it bubble.
133
+ * - At the top of a menubar (`ctx.parentMenu == null && ctx.menubar`):
134
+ * both arrows switch to the previous / next sibling menu.
135
+ * - Otherwise: not handled.
136
+ */
137
+ function handleMenuHorizontalArrow(event, ctx) {
138
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
139
+ return false;
140
+ }
141
+ const isRtl = ctx.dir() === 'rtl';
142
+ if (ctx.parentMenu) {
143
+ const closeKey = isRtl ? 'ArrowRight' : 'ArrowLeft';
144
+ if (event.key === closeKey) {
145
+ event.preventDefault();
146
+ ctx.closeMenu('escape');
147
+ return true;
148
+ }
149
+ return false;
150
+ }
151
+ const menubar = ctx.menubar;
152
+ if (!menubar) {
153
+ return false;
154
+ }
155
+ const isPrev = (event.key === 'ArrowLeft' && !isRtl) || (event.key === 'ArrowRight' && isRtl);
156
+ event.preventDefault();
157
+ menubar.switchToSibling(isPrev ? 'prev' : 'next');
158
+ return true;
159
+ }
160
+
161
+ /**
162
+ * Handle Tab from inside a menu (regular item, checkbox / radio item, or a
163
+ * submenu trigger). Per WAI-ARIA APG, Tab moves focus out of the menu rather
164
+ * than back to the trigger.
165
+ *
166
+ * Focus is moved synchronously to the outermost menu's trigger — the only
167
+ * element guaranteed to survive in the DOM once the whole open chain unmounts
168
+ * — so the browser's own Tab default advances from there to the next (or
169
+ * previous, with Shift) focusable. The event is NOT `preventDefault`-ed for
170
+ * that reason. `closeMenu('tab')` then collapses the chain; every
171
+ * `[forMenuContent]` skips its return-focus on `'tab'` (see
172
+ * `ForMenuContext.lastCloseReason`) so none of them steal focus back from
173
+ * wherever the browser advanced it.
174
+ *
175
+ * Pass the context that should drive the close: the item's own `ctx` for
176
+ * menu items, or the submenu trigger's `parentMenu` (the menu the trigger
177
+ * lives in).
178
+ */
179
+ function handleMenuTabOut(ctx) {
180
+ let root = ctx;
181
+ while (root.parentMenu) {
182
+ root = root.parentMenu;
183
+ }
184
+ root.trigger()?.focus();
185
+ ctx.closeMenu('tab');
186
+ }
187
+
188
+ /**
189
+ * A single action inside `[forMenuContent]`. Apply on a `<button>` so
190
+ * Space / Enter activation come from native button semantics.
191
+ *
192
+ * Activation emits `(activate)` and then closes the menu. To keep the menu
193
+ * open after activation (e.g. the action toggled something the user
194
+ * wants to refine), call `event.preventDefault()` on the emitted event.
195
+ *
196
+ * Disabled items remain in the tab/focus rotation (per APG) so screen
197
+ * readers can announce them — `aria-disabled="true"` rather than the
198
+ * native `disabled` attribute.
199
+ */
200
+ class ForMenuItem {
201
+ ctx = injectMenuContext('ForMenuItem');
202
+ #host = inject(ElementRef);
203
+ /** Per-item disabled, in addition to the menu's `disabled`. */
204
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
205
+ /**
206
+ * Override the string used for typeahead matching. Defaults to `''`,
207
+ * which falls back to the item's `textContent`. Set this when the item
208
+ * DOM contains icons, kbd hints, or other text that shouldn't bleed
209
+ * into the match — e.g. `<button forMenuItem textValue="New file">…</button>`
210
+ * for an item rendered as `New file ⌘N`.
211
+ */
212
+ textValue = input('', /* @ts-ignore */
213
+ ...(ngDevMode ? [{ debugName: "textValue" }] : /* istanbul ignore next */ []));
214
+ effectiveDisabled = computed(() => this.disabled() || this.ctx.disabled(), /* @ts-ignore */
215
+ ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
216
+ #highlighted = signal(false, /* @ts-ignore */
217
+ ...(ngDevMode ? [{ debugName: "#highlighted" }] : /* istanbul ignore next */ []));
218
+ /**
219
+ * True while this item is the active keyboard candidate or hovered by the
220
+ * pointer. Set on keyboard-driven focus and on `pointermove` (hover follows
221
+ * the pointer), cleared on `blur` and when the pointer leaves the surface.
222
+ * The programmatic initial focus of a pointer-driven open lands without a
223
+ * highlight until the pointer moves onto the item or keyboard navigation
224
+ * begins. Reflected as `data-highlighted`.
225
+ */
226
+ highlighted = this.#highlighted.asReadonly();
227
+ #suppressNextFocusHighlight = false;
228
+ /**
229
+ * Fires on click / Enter / Space activation. Call `preventDefault()`
230
+ * on the emitted veto to keep the menu open after activation.
231
+ */
232
+ activate = output();
233
+ constructor() {
234
+ const handle = {
235
+ host: this.#host.nativeElement,
236
+ disabled: this.effectiveDisabled,
237
+ textValue: this.textValue,
238
+ suppressHighlightOnNextFocus: () => {
239
+ this.#suppressNextFocusHighlight = true;
240
+ },
241
+ clearHighlight: () => {
242
+ this.#highlighted.set(false);
243
+ },
244
+ };
245
+ registerHandle(handle, (h) => this.ctx.registerItem(h), (h) => this.ctx.unregisterItem(h));
246
+ }
247
+ onClick() {
248
+ if (this.effectiveDisabled()) {
249
+ return;
250
+ }
251
+ if (!emitVetoableEvent(this.activate)) {
252
+ this.ctx.closeMenu('select');
253
+ }
254
+ }
255
+ onFocus() {
256
+ if (this.#suppressNextFocusHighlight) {
257
+ this.#suppressNextFocusHighlight = false;
258
+ return;
259
+ }
260
+ this.#highlighted.set(true);
261
+ }
262
+ onBlur() {
263
+ this.#suppressNextFocusHighlight = false;
264
+ this.#highlighted.set(false);
265
+ }
266
+ onPointerMove(event) {
267
+ if (event.pointerType !== '' && event.pointerType !== 'mouse') {
268
+ return;
269
+ }
270
+ if (this.effectiveDisabled()) {
271
+ return;
272
+ }
273
+ const host = this.#host.nativeElement;
274
+ if (host.ownerDocument.activeElement !== host) {
275
+ host.focus({ preventScroll: true });
276
+ }
277
+ this.#highlighted.set(true);
278
+ }
279
+ onKeyDown(event) {
280
+ if (this.effectiveDisabled()) {
281
+ return;
282
+ }
283
+ // ArrowLeft / ArrowRight: inside a submenu, close-submenu key collapses
284
+ // one level; in the top menu of a menubar, both arrows switch to the
285
+ // previous / next sibling menu.
286
+ if (handleMenuHorizontalArrow(event, this.ctx)) {
287
+ return;
288
+ }
289
+ const action = resolveListNavigation(event, { orientation: 'vertical' });
290
+ if (action) {
291
+ event.preventDefault();
292
+ this.ctx.navigate(this.#host.nativeElement, action);
293
+ return;
294
+ }
295
+ if (event.key === 'Tab') {
296
+ handleMenuTabOut(this.ctx);
297
+ return;
298
+ }
299
+ this.ctx.handleTypeahead(event);
300
+ }
301
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
302
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForMenuItem, isStandalone: true, selector: "[forMenuItem]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, textValue: { classPropertyName: "textValue", publicName: "textValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activate: "activate" }, host: { attributes: { "role": "menuitem", "type": "button", "tabindex": "-1" }, listeners: { "click": "onClick()", "keydown": "onKeyDown($event)", "focus": "onFocus()", "blur": "onBlur()", "pointermove": "onPointerMove($event)" }, properties: { "attr.aria-disabled": "effectiveDisabled() ? \"true\" : null", "attr.data-disabled": "effectiveDisabled() ? \"\" : null", "attr.data-highlighted": "highlighted() ? \"\" : null" } }, exportAs: ["forMenuItem"], ngImport: i0 });
303
+ }
304
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuItem, decorators: [{
305
+ type: Directive,
306
+ args: [{
307
+ selector: '[forMenuItem]',
308
+ exportAs: 'forMenuItem',
309
+ host: {
310
+ role: 'menuitem',
311
+ type: 'button',
312
+ tabindex: '-1',
313
+ '[attr.aria-disabled]': 'effectiveDisabled() ? "true" : null',
314
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
315
+ '[attr.data-highlighted]': 'highlighted() ? "" : null',
316
+ '(click)': 'onClick()',
317
+ '(keydown)': 'onKeyDown($event)',
318
+ '(focus)': 'onFocus()',
319
+ '(blur)': 'onBlur()',
320
+ '(pointermove)': 'onPointerMove($event)',
321
+ },
322
+ }]
323
+ }], ctorParameters: () => [], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], textValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "textValue", required: false }] }], activate: [{ type: i0.Output, args: ["activate"] }] } });
324
+
325
+ /**
326
+ * Injection key the `[forMenuItemIndicator]` uses to resolve a parent
327
+ * checkbox item, decoupled from the concrete `ForMenuCheckboxItem` class.
328
+ * `ForMenuCheckboxItem` provides itself under this token, so a design system
329
+ * wrapping the item by subclassing re-points it at the subclass with a single
330
+ * provider (`{ provide: FOR_MENU_CHECKBOX_ITEM, useExisting: MtxMenuCheckboxItem }`)
331
+ * and the indicator keeps resolving — see `docs/wrapping-form-primitives.md`.
332
+ */
333
+ const FOR_MENU_CHECKBOX_ITEM = new InjectionToken('FOR_MENU_CHECKBOX_ITEM');
334
+ /**
335
+ * Tri-state-free checkbox item. Click and Enter toggle `checked`, emit
336
+ * `(activate)`, and close the menu — `event.preventDefault()` on the emitted
337
+ * event keeps the menu open. Per APG, **Space** toggles `checked` and
338
+ * emits `(activate)` without closing the menu, so users can flip several
339
+ * options in one open without consumer glue.
340
+ */
341
+ class ForMenuCheckboxItem {
342
+ ctx = injectMenuContext('ForMenuCheckboxItem');
343
+ #host = inject(ElementRef);
344
+ /** Two-way bindable. */
345
+ checked = model(false, /* @ts-ignore */
346
+ ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
347
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
348
+ /**
349
+ * Override the string used for typeahead matching. Defaults to `''`,
350
+ * which falls back to the item's `textContent`. See `[forMenuItem]`
351
+ * for the rationale.
352
+ */
353
+ textValue = input('', /* @ts-ignore */
354
+ ...(ngDevMode ? [{ debugName: "textValue" }] : /* istanbul ignore next */ []));
355
+ effectiveDisabled = computed(() => this.disabled() || this.ctx.disabled(), /* @ts-ignore */
356
+ ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
357
+ #highlighted = signal(false, /* @ts-ignore */
358
+ ...(ngDevMode ? [{ debugName: "#highlighted" }] : /* istanbul ignore next */ []));
359
+ /**
360
+ * True while this item is the active keyboard candidate or hovered by the
361
+ * pointer. Set on keyboard-driven focus and on `pointermove` (hover follows
362
+ * the pointer), cleared on `blur` and when the pointer leaves the surface.
363
+ * The programmatic initial focus of a pointer-driven open lands without a
364
+ * highlight until the pointer moves onto the item or keyboard navigation
365
+ * begins. Reflected as `data-highlighted`.
366
+ */
367
+ highlighted = this.#highlighted.asReadonly();
368
+ #suppressNextFocusHighlight = false;
369
+ /**
370
+ * Fires on click / Enter / Space activation. Call `preventDefault()`
371
+ * on the emitted veto to keep the menu open after activation.
372
+ */
373
+ activate = output();
374
+ constructor() {
375
+ const handle = {
376
+ host: this.#host.nativeElement,
377
+ disabled: this.effectiveDisabled,
378
+ textValue: this.textValue,
379
+ suppressHighlightOnNextFocus: () => {
380
+ this.#suppressNextFocusHighlight = true;
381
+ },
382
+ clearHighlight: () => {
383
+ this.#highlighted.set(false);
384
+ },
385
+ };
386
+ registerHandle(handle, (h) => this.ctx.registerItem(h), (h) => this.ctx.unregisterItem(h));
387
+ }
388
+ onClick() {
389
+ if (this.effectiveDisabled()) {
390
+ return;
391
+ }
392
+ this.checked.update((v) => !v);
393
+ if (!emitVetoableEvent(this.activate)) {
394
+ this.ctx.closeMenu('select');
395
+ }
396
+ }
397
+ onFocus() {
398
+ if (this.#suppressNextFocusHighlight) {
399
+ this.#suppressNextFocusHighlight = false;
400
+ return;
401
+ }
402
+ this.#highlighted.set(true);
403
+ }
404
+ onBlur() {
405
+ this.#suppressNextFocusHighlight = false;
406
+ this.#highlighted.set(false);
407
+ }
408
+ onPointerMove(event) {
409
+ if (event.pointerType !== '' && event.pointerType !== 'mouse') {
410
+ return;
411
+ }
412
+ if (this.effectiveDisabled()) {
413
+ return;
414
+ }
415
+ const host = this.#host.nativeElement;
416
+ if (host.ownerDocument.activeElement !== host) {
417
+ host.focus({ preventScroll: true });
418
+ }
419
+ this.#highlighted.set(true);
420
+ }
421
+ onKeyDown(event) {
422
+ if (this.effectiveDisabled()) {
423
+ return;
424
+ }
425
+ if (handleMenuHorizontalArrow(event, this.ctx)) {
426
+ return;
427
+ }
428
+ // APG menubar guidance: Space toggles checked without closing the menu
429
+ // (Enter and click still close via native button activation). preventDefault
430
+ // here suppresses the browser-synthesized click so the menu stays open.
431
+ if (event.key === ' ') {
432
+ event.preventDefault();
433
+ this.checked.update((v) => !v);
434
+ this.activate.emit(createVetoableEvent());
435
+ return;
436
+ }
437
+ const action = resolveListNavigation(event, { orientation: 'vertical' });
438
+ if (action) {
439
+ event.preventDefault();
440
+ this.ctx.navigate(this.#host.nativeElement, action);
441
+ return;
442
+ }
443
+ if (event.key === 'Tab') {
444
+ handleMenuTabOut(this.ctx);
445
+ return;
446
+ }
447
+ this.ctx.handleTypeahead(event);
448
+ }
449
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuCheckboxItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
450
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForMenuCheckboxItem, isStandalone: true, selector: "[forMenuCheckboxItem]", inputs: { checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, textValue: { classPropertyName: "textValue", publicName: "textValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange", activate: "activate" }, host: { attributes: { "role": "menuitemcheckbox", "type": "button", "tabindex": "-1" }, listeners: { "click": "onClick()", "keydown": "onKeyDown($event)", "focus": "onFocus()", "blur": "onBlur()", "pointermove": "onPointerMove($event)" }, properties: { "attr.aria-checked": "checked() ? \"true\" : \"false\"", "attr.aria-disabled": "effectiveDisabled() ? \"true\" : null", "attr.data-state": "checked() ? \"checked\" : \"unchecked\"", "attr.data-disabled": "effectiveDisabled() ? \"\" : null", "attr.data-highlighted": "highlighted() ? \"\" : null" } }, providers: [{ provide: FOR_MENU_CHECKBOX_ITEM, useExisting: ForMenuCheckboxItem }], exportAs: ["forMenuCheckboxItem"], ngImport: i0 });
451
+ }
452
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuCheckboxItem, decorators: [{
453
+ type: Directive,
454
+ args: [{
455
+ selector: '[forMenuCheckboxItem]',
456
+ exportAs: 'forMenuCheckboxItem',
457
+ providers: [{ provide: FOR_MENU_CHECKBOX_ITEM, useExisting: ForMenuCheckboxItem }],
458
+ host: {
459
+ role: 'menuitemcheckbox',
460
+ type: 'button',
461
+ tabindex: '-1',
462
+ '[attr.aria-checked]': 'checked() ? "true" : "false"',
463
+ '[attr.aria-disabled]': 'effectiveDisabled() ? "true" : null',
464
+ '[attr.data-state]': 'checked() ? "checked" : "unchecked"',
465
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
466
+ '[attr.data-highlighted]': 'highlighted() ? "" : null',
467
+ '(click)': 'onClick()',
468
+ '(keydown)': 'onKeyDown($event)',
469
+ '(focus)': 'onFocus()',
470
+ '(blur)': 'onBlur()',
471
+ '(pointermove)': 'onPointerMove($event)',
472
+ },
473
+ }]
474
+ }], ctorParameters: () => [], propDecorators: { checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], textValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "textValue", required: false }] }], activate: [{ type: i0.Output, args: ["activate"] }] } });
475
+
476
+ const FOR_MENU_RADIO_GROUP_CONTEXT = new InjectionToken('FOR_MENU_RADIO_GROUP_CONTEXT');
477
+ function injectMenuRadioGroupContext(piece) {
478
+ const ctx = inject(FOR_MENU_RADIO_GROUP_CONTEXT, { optional: true });
479
+ if (!ctx) {
480
+ throw new Error(`[forty-cdk/menu] ${piece} must be used inside a [forMenuRadioGroup] element.`);
481
+ }
482
+ return ctx;
483
+ }
484
+
485
+ /**
486
+ * Container for a set of `[forMenuRadioItem]` elements. Acts as a
487
+ * `role="group"` plus a coordination point for the shared `value`.
488
+ */
489
+ class ForMenuRadioGroup {
490
+ /**
491
+ * Two-way bindable. The selected radio item's `value`. The `model()`
492
+ * change emitter (`(valueChange)`) fires only on internal selection.
493
+ */
494
+ value = model('', /* @ts-ignore */
495
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
496
+ isSelected(v) {
497
+ return this.value() === v;
498
+ }
499
+ select(v) {
500
+ this.value.set(v);
501
+ }
502
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuRadioGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
503
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForMenuRadioGroup, isStandalone: true, selector: "[forMenuRadioGroup]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { attributes: { "role": "group" } }, providers: [{ provide: FOR_MENU_RADIO_GROUP_CONTEXT, useExisting: ForMenuRadioGroup }], exportAs: ["forMenuRadioGroup"], ngImport: i0 });
504
+ }
505
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuRadioGroup, decorators: [{
506
+ type: Directive,
507
+ args: [{
508
+ selector: '[forMenuRadioGroup]',
509
+ exportAs: 'forMenuRadioGroup',
510
+ host: {
511
+ role: 'group',
512
+ },
513
+ providers: [{ provide: FOR_MENU_RADIO_GROUP_CONTEXT, useExisting: ForMenuRadioGroup }],
514
+ }]
515
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }] } });
516
+
517
+ /**
518
+ * Injection key the `[forMenuItemIndicator]` uses to resolve a parent radio
519
+ * item, decoupled from the concrete `ForMenuRadioItem` class.
520
+ * `ForMenuRadioItem` provides itself under this token, so a design system
521
+ * wrapping the item by subclassing re-points it at the subclass with a single
522
+ * provider (`{ provide: FOR_MENU_RADIO_ITEM, useExisting: MtxMenuRadioItem }`)
523
+ * and the indicator keeps resolving — see `docs/wrapping-form-primitives.md`.
524
+ */
525
+ const FOR_MENU_RADIO_ITEM = new InjectionToken('FOR_MENU_RADIO_ITEM');
526
+ /**
527
+ * One radio option inside `[forMenuRadioGroup]`. Click and Enter set the
528
+ * group's `value` to this item's `value`, emit `(activate)`, and close the
529
+ * menu — call `event.preventDefault()` on the emitted event to keep the
530
+ * menu open. Per APG, **Space** sets the value and emits `(activate)`
531
+ * without closing the menu.
532
+ */
533
+ class ForMenuRadioItem {
534
+ menu = injectMenuContext('ForMenuRadioItem');
535
+ group = injectMenuRadioGroupContext('ForMenuRadioItem');
536
+ #host = inject(ElementRef);
537
+ /** Identifier added to / read from the radio group's `value`. Required. */
538
+ value = input.required(/* @ts-ignore */
539
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
540
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
541
+ /**
542
+ * Override the string used for typeahead matching. Defaults to `''`,
543
+ * which falls back to the item's `textContent`. See `[forMenuItem]`
544
+ * for the rationale.
545
+ */
546
+ textValue = input('', /* @ts-ignore */
547
+ ...(ngDevMode ? [{ debugName: "textValue" }] : /* istanbul ignore next */ []));
548
+ checked = computed(() => this.group.isSelected(this.value()), /* @ts-ignore */
549
+ ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
550
+ effectiveDisabled = computed(() => this.disabled() || this.menu.disabled(), /* @ts-ignore */
551
+ ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
552
+ #highlighted = signal(false, /* @ts-ignore */
553
+ ...(ngDevMode ? [{ debugName: "#highlighted" }] : /* istanbul ignore next */ []));
554
+ /**
555
+ * True while this item is the active keyboard candidate or hovered by the
556
+ * pointer. Set on keyboard-driven focus and on `pointermove` (hover follows
557
+ * the pointer), cleared on `blur` and when the pointer leaves the surface.
558
+ * The programmatic initial focus of a pointer-driven open lands without a
559
+ * highlight until the pointer moves onto the item or keyboard navigation
560
+ * begins. Reflected as `data-highlighted`.
561
+ */
562
+ highlighted = this.#highlighted.asReadonly();
563
+ #suppressNextFocusHighlight = false;
564
+ /**
565
+ * Fires on click / Enter / Space activation. Call `preventDefault()`
566
+ * on the emitted veto to keep the menu open after activation.
567
+ */
568
+ activate = output();
569
+ constructor() {
570
+ const handle = {
571
+ host: this.#host.nativeElement,
572
+ disabled: this.effectiveDisabled,
573
+ textValue: this.textValue,
574
+ suppressHighlightOnNextFocus: () => {
575
+ this.#suppressNextFocusHighlight = true;
576
+ },
577
+ clearHighlight: () => {
578
+ this.#highlighted.set(false);
579
+ },
580
+ };
581
+ registerHandle(handle, (h) => this.menu.registerItem(h), (h) => this.menu.unregisterItem(h));
582
+ }
583
+ onClick() {
584
+ if (this.effectiveDisabled()) {
585
+ return;
586
+ }
587
+ this.group.select(this.value());
588
+ if (!emitVetoableEvent(this.activate)) {
589
+ this.menu.closeMenu('select');
590
+ }
591
+ }
592
+ onFocus() {
593
+ if (this.#suppressNextFocusHighlight) {
594
+ this.#suppressNextFocusHighlight = false;
595
+ return;
596
+ }
597
+ this.#highlighted.set(true);
598
+ }
599
+ onBlur() {
600
+ this.#suppressNextFocusHighlight = false;
601
+ this.#highlighted.set(false);
602
+ }
603
+ onPointerMove(event) {
604
+ if (event.pointerType !== '' && event.pointerType !== 'mouse') {
605
+ return;
606
+ }
607
+ if (this.effectiveDisabled()) {
608
+ return;
609
+ }
610
+ const host = this.#host.nativeElement;
611
+ if (host.ownerDocument.activeElement !== host) {
612
+ host.focus({ preventScroll: true });
613
+ }
614
+ this.#highlighted.set(true);
615
+ }
616
+ onKeyDown(event) {
617
+ if (this.effectiveDisabled()) {
618
+ return;
619
+ }
620
+ if (handleMenuHorizontalArrow(event, this.menu)) {
621
+ return;
622
+ }
623
+ // APG menubar guidance: Space sets the group value without closing the
624
+ // menu (Enter and click still close via native button activation).
625
+ // preventDefault here suppresses the browser-synthesized click.
626
+ if (event.key === ' ') {
627
+ event.preventDefault();
628
+ this.group.select(this.value());
629
+ this.activate.emit(createVetoableEvent());
630
+ return;
631
+ }
632
+ const action = resolveListNavigation(event, { orientation: 'vertical' });
633
+ if (action) {
634
+ event.preventDefault();
635
+ this.menu.navigate(this.#host.nativeElement, action);
636
+ return;
637
+ }
638
+ if (event.key === 'Tab') {
639
+ handleMenuTabOut(this.menu);
640
+ return;
641
+ }
642
+ this.menu.handleTypeahead(event);
643
+ }
644
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuRadioItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
645
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForMenuRadioItem, isStandalone: true, selector: "[forMenuRadioItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, textValue: { classPropertyName: "textValue", publicName: "textValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activate: "activate" }, host: { attributes: { "role": "menuitemradio", "type": "button", "tabindex": "-1" }, listeners: { "click": "onClick()", "keydown": "onKeyDown($event)", "focus": "onFocus()", "blur": "onBlur()", "pointermove": "onPointerMove($event)" }, properties: { "attr.aria-checked": "checked() ? \"true\" : \"false\"", "attr.aria-disabled": "effectiveDisabled() ? \"true\" : null", "attr.data-state": "checked() ? \"checked\" : \"unchecked\"", "attr.data-disabled": "effectiveDisabled() ? \"\" : null", "attr.data-highlighted": "highlighted() ? \"\" : null" } }, providers: [{ provide: FOR_MENU_RADIO_ITEM, useExisting: ForMenuRadioItem }], exportAs: ["forMenuRadioItem"], ngImport: i0 });
646
+ }
647
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuRadioItem, decorators: [{
648
+ type: Directive,
649
+ args: [{
650
+ selector: '[forMenuRadioItem]',
651
+ exportAs: 'forMenuRadioItem',
652
+ providers: [{ provide: FOR_MENU_RADIO_ITEM, useExisting: ForMenuRadioItem }],
653
+ host: {
654
+ role: 'menuitemradio',
655
+ type: 'button',
656
+ tabindex: '-1',
657
+ '[attr.aria-checked]': 'checked() ? "true" : "false"',
658
+ '[attr.aria-disabled]': 'effectiveDisabled() ? "true" : null',
659
+ '[attr.data-state]': 'checked() ? "checked" : "unchecked"',
660
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
661
+ '[attr.data-highlighted]': 'highlighted() ? "" : null',
662
+ '(click)': 'onClick()',
663
+ '(keydown)': 'onKeyDown($event)',
664
+ '(focus)': 'onFocus()',
665
+ '(blur)': 'onBlur()',
666
+ '(pointermove)': 'onPointerMove($event)',
667
+ },
668
+ }]
669
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], textValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "textValue", required: false }] }], activate: [{ type: i0.Output, args: ["activate"] }] } });
670
+
671
+ /**
672
+ * Visibility helper inside `[forMenuCheckboxItem]` or `[forMenuRadioItem]`.
673
+ * The directive flips a `[hidden]` host binding so the consumer can keep
674
+ * the checkmark / dot inline without extra `@if` glue:
675
+ *
676
+ * ```html
677
+ * <button forMenuCheckboxItem [(checked)]="bold">
678
+ * <span forMenuItemIndicator>✓</span>
679
+ * Bold
680
+ * </button>
681
+ *
682
+ * <div forMenuRadioGroup [(value)]="alignment">
683
+ * <button forMenuRadioItem value="left">
684
+ * <span forMenuItemIndicator>•</span>
685
+ * Left
686
+ * </button>
687
+ * </div>
688
+ * ```
689
+ *
690
+ * Reflects the parent item's `data-state` (`"checked" | "unchecked"`) so
691
+ * the consumer can also style it from CSS. Set `[forceMount]` to keep the
692
+ * indicator in the DOM regardless of state — useful when wrapping
693
+ * `animate.leave` for an exit animation, or when the consumer styles the
694
+ * indicator via `data-state` instead of presence.
695
+ *
696
+ * Visibility while unchecked is enforced with an inline `display: none`
697
+ * (which beats any author `display` rule a consumer applies via a class) in
698
+ * addition to the `hidden` attribute that removes it from the a11y tree.
699
+ */
700
+ class ForMenuItemIndicator {
701
+ #checkbox = inject(FOR_MENU_CHECKBOX_ITEM, { optional: true });
702
+ #radio = inject(FOR_MENU_RADIO_ITEM, { optional: true });
703
+ checked = computed(() => {
704
+ if (this.#checkbox) {
705
+ return this.#checkbox.checked();
706
+ }
707
+ if (this.#radio) {
708
+ return this.#radio.checked();
709
+ }
710
+ return false;
711
+ }, /* @ts-ignore */
712
+ ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
713
+ /** Keep the indicator mounted even when the parent item is unchecked. */
714
+ forceMount = input(false, { ...(ngDevMode ? { debugName: "forceMount" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
715
+ constructor() {
716
+ if (!this.#checkbox && !this.#radio) {
717
+ throw new Error('[forty-cdk/menu] ForMenuItemIndicator must be used inside a [forMenuCheckboxItem] or [forMenuRadioItem] element.');
718
+ }
719
+ }
720
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuItemIndicator, deps: [], target: i0.ɵɵFactoryTarget.Directive });
721
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForMenuItemIndicator, isStandalone: true, selector: "[forMenuItemIndicator]", inputs: { forceMount: { classPropertyName: "forceMount", publicName: "forceMount", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, properties: { "attr.data-state": "checked() ? \"checked\" : \"unchecked\"", "hidden": "!checked() && !forceMount()", "style.display": "!checked() && !forceMount() ? \"none\" : null" } }, exportAs: ["forMenuItemIndicator"], ngImport: i0 });
722
+ }
723
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuItemIndicator, decorators: [{
724
+ type: Directive,
725
+ args: [{
726
+ selector: '[forMenuItemIndicator]',
727
+ exportAs: 'forMenuItemIndicator',
728
+ host: {
729
+ 'aria-hidden': 'true',
730
+ '[attr.data-state]': 'checked() ? "checked" : "unchecked"',
731
+ '[hidden]': '!checked() && !forceMount()',
732
+ '[style.display]': '!checked() && !forceMount() ? "none" : null',
733
+ },
734
+ }]
735
+ }], ctorParameters: () => [], propDecorators: { forceMount: [{ type: i0.Input, args: [{ isSignal: true, alias: "forceMount", required: false }] }] } });
736
+
737
+ /**
738
+ * Visual + semantic separator between menu items. Decorative — never
739
+ * focused, never registers with the menu's item collection.
740
+ */
741
+ class ForMenuSeparator {
742
+ /**
743
+ * Axis the separator divides along, reflected to `aria-orientation`.
744
+ * `horizontal` (default) splits items stacked vertically — the common case
745
+ * in a vertical menu; `vertical` splits items laid out horizontally.
746
+ */
747
+ orientation = input('horizontal', /* @ts-ignore */
748
+ ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
749
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuSeparator, deps: [], target: i0.ɵɵFactoryTarget.Directive });
750
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForMenuSeparator, isStandalone: true, selector: "[forMenuSeparator]", inputs: { orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "separator" }, properties: { "attr.aria-orientation": "orientation()" } }, ngImport: i0 });
751
+ }
752
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuSeparator, decorators: [{
753
+ type: Directive,
754
+ args: [{
755
+ selector: '[forMenuSeparator]',
756
+ host: {
757
+ role: 'separator',
758
+ '[attr.aria-orientation]': 'orientation()',
759
+ },
760
+ }]
761
+ }], propDecorators: { orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }] } });
762
+
763
+ const FOR_MENU_GROUP_CONTEXT = new InjectionToken('FOR_MENU_GROUP_CONTEXT');
764
+ function injectMenuGroupContext(piece) {
765
+ const ctx = inject(FOR_MENU_GROUP_CONTEXT, { optional: true });
766
+ if (!ctx) {
767
+ throw new Error(`[forty-cdk/menu] ${piece} must be used inside a [forMenuGroup] element.`);
768
+ }
769
+ return ctx;
770
+ }
771
+
772
+ /**
773
+ * Logical grouping of menu items, exposed to assistive tech as
774
+ * `role="group"`. Use with `[forMenuGroupLabel]` for an accessible name
775
+ * (rendered as a section header in most designs).
776
+ */
777
+ class ForMenuGroup {
778
+ #labelIds = signal([], /* @ts-ignore */
779
+ ...(ngDevMode ? [{ debugName: "#labelIds" }] : /* istanbul ignore next */ []));
780
+ labelledBy = computed(() => {
781
+ const ids = this.#labelIds();
782
+ return ids.length === 0 ? null : ids.join(' ');
783
+ }, /* @ts-ignore */
784
+ ...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
785
+ registerLabel(id) {
786
+ this.#labelIds.update((arr) => (arr.includes(id) ? arr : [...arr, id]));
787
+ }
788
+ unregisterLabel(id) {
789
+ this.#labelIds.update((arr) => arr.filter((x) => x !== id));
790
+ }
791
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
792
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForMenuGroup, isStandalone: true, selector: "[forMenuGroup]", host: { attributes: { "role": "group" }, properties: { "attr.aria-labelledby": "labelledBy()" } }, providers: [{ provide: FOR_MENU_GROUP_CONTEXT, useExisting: ForMenuGroup }], exportAs: ["forMenuGroup"], ngImport: i0 });
793
+ }
794
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuGroup, decorators: [{
795
+ type: Directive,
796
+ args: [{
797
+ selector: '[forMenuGroup]',
798
+ exportAs: 'forMenuGroup',
799
+ host: {
800
+ role: 'group',
801
+ '[attr.aria-labelledby]': 'labelledBy()',
802
+ },
803
+ providers: [{ provide: FOR_MENU_GROUP_CONTEXT, useExisting: ForMenuGroup }],
804
+ }]
805
+ }] });
806
+
807
+ /**
808
+ * Accessible name for `[forMenuGroup]`. Generates an id on the host and
809
+ * registers it with the parent group so `aria-labelledby` resolves to
810
+ * this element.
811
+ */
812
+ class ForMenuGroupLabel {
813
+ id = registerA11yName(injectMenuGroupContext('ForMenuGroupLabel'), 'for-menu-group-label');
814
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuGroupLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive });
815
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForMenuGroupLabel, isStandalone: true, selector: "[forMenuGroupLabel]", host: { properties: { "id": "id()" } }, ngImport: i0 });
816
+ }
817
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuGroupLabel, decorators: [{
818
+ type: Directive,
819
+ args: [{
820
+ selector: '[forMenuGroupLabel]',
821
+ host: {
822
+ '[id]': 'id()',
823
+ },
824
+ }]
825
+ }] });
826
+
827
+ /**
828
+ * Library fallback for menu defaults, read at the root injector when no
829
+ * consumer has called `provideForMenuDefaults`. Exported for the shared
830
+ * defaults contract spec; not re-exported from the primitive's public entry.
831
+ */
832
+ const FOR_MENU_FALLBACK_DEFAULTS = {
833
+ subMenuOpenDelay: 100,
834
+ subMenuCloseDelay: 100,
835
+ subMenuPointerGraceDuration: 300,
836
+ sideOffset: 0,
837
+ collisionPadding: 8,
838
+ };
839
+ const { token, provideDefaults } = createDefaults('FOR_MENU_DEFAULTS', FOR_MENU_FALLBACK_DEFAULTS);
840
+ /** Token holding the resolved menu defaults for the current scope. */
841
+ const FOR_MENU_DEFAULTS = token;
842
+ /**
843
+ * Configures forty-cdk menu defaults for this injector scope. Partial
844
+ * overrides inherit unspecified keys from the parent scope (or library
845
+ * defaults at the root).
846
+ */
847
+ function provideForMenuDefaults(defaults = {}) {
848
+ return provideDefaults(defaults);
849
+ }
850
+
851
+ /**
852
+ * Root for a nested submenu inside a parent `[forDropdownMenu]` /
853
+ * `[forContextMenu]` (or another `[forMenuSub]`). Owns its own open
854
+ * state, ids, and item collection — items inside the submenu register
855
+ * here, not in the parent.
856
+ *
857
+ * The shared item-collection / typeahead / navigate / focus / id /
858
+ * outside-veto logic is owned by the `_internal/menu-overlay` helper (the
859
+ * same one `[forDropdownMenu]` / `[forContextMenu]` compose). The submenu
860
+ * contributes only its genuine differences on top: pointer-driven (hover)
861
+ * open/close scheduling, the `#suppressFocusMoves` autofocus vetoes that
862
+ * keep hover from stealing / returning focus, and the reason-dependent
863
+ * upward `closeMenu` propagation — the latter two wired through the
864
+ * overlay's `onOpen` / `onClose` lifecycle hooks.
865
+ *
866
+ * The parent menu's content is added to this submenu's dismissable
867
+ * exemptions so a click on a parent menu item doesn't fire the
868
+ * submenu's outside-handler (the parent item's own click flow closes
869
+ * everything via propagated `closeMenu`).
870
+ *
871
+ * Closing this submenu propagates `closeMenu` upward for every reason
872
+ * except `'escape'` and `'programmatic'` — Escape inside a submenu
873
+ * closes only that level, while activating an item or clicking outside
874
+ * everything tears down the entire chain.
875
+ *
876
+ * ```html
877
+ * <div forDropdownMenu [(open)]="open">
878
+ * <button forDropdownMenuTrigger>Options</button>
879
+ * @if (open()) {
880
+ * <div forMenuContent>
881
+ * <button forMenuItem>Cut</button>
882
+ * <div forMenuSub [(open)]="moreOpen">
883
+ * <button forMenuSubTrigger>More</button>
884
+ * @if (moreOpen()) {
885
+ * <div forMenuSubContent>
886
+ * <button forMenuItem>Advanced</button>
887
+ * </div>
888
+ * }
889
+ * </div>
890
+ * </div>
891
+ * }
892
+ * </div>
893
+ * ```
894
+ */
895
+ class ForMenuSub extends MenuOverlayHost {
896
+ #defaults = inject(FOR_MENU_DEFAULTS);
897
+ #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
898
+ /** The enclosing menu — required (orphan throws). */
899
+ parentMenu;
900
+ open = model(false, /* @ts-ignore */
901
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
902
+ /**
903
+ * Writing direction. When unset, inherits from the enclosing menu — set
904
+ * `[dir]` once on the top-level `[forDropdownMenu]` / `[forContextMenu]`
905
+ * and every nested submenu picks it up. Override per-submenu only when
906
+ * a specific submenu needs to render against the opposite direction.
907
+ *
908
+ * The input is aliased to `dir`; consumers bind `[dir]="..."` and read
909
+ * the effective value via the public `dir` computed below.
910
+ */
911
+ _dirInput = input(undefined, { ...(ngDevMode ? { debugName: "_dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
912
+ dir = computed(() => this._dirInput() ?? this.parentMenu.dir(), /* @ts-ignore */
913
+ ...(ngDevMode ? [{ debugName: "dir" }] : /* istanbul ignore next */ []));
914
+ /**
915
+ * Side the submenu opens on. When unset, defaults to `'right'` in LTR
916
+ * and `'left'` in RTL (per `dir`). Set explicitly to pin a side
917
+ * regardless of writing direction.
918
+ *
919
+ * The input is aliased to `side`; consumers bind `[side]="..."` and read
920
+ * the effective value via the public `side` computed below.
921
+ */
922
+ _sideInput = input(undefined, { ...(ngDevMode ? { debugName: "_sideInput" } : /* istanbul ignore next */ {}), alias: 'side' });
923
+ side = computed(() => this._sideInput() ?? (this.dir() === 'rtl' ? 'left' : 'right'), /* @ts-ignore */
924
+ ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
925
+ /**
926
+ * Gap (px) along the main axis. Defaults to `0` from `provideForMenuDefaults`
927
+ * — a submenu sits flush against its parent item. Now read from the defaults
928
+ * provider (like `[forDropdownMenu]` / `[forContextMenu]`) rather than
929
+ * hardcoded, so it can't drift.
930
+ */
931
+ sideOffset = input(this.#defaults.sideOffset, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
932
+ /**
933
+ * Alignment along the chosen `side`. Defaults to `'start'`.
934
+ *
935
+ * Shares the single `MENU_POSITIONING_DEFAULTS` source with the two
936
+ * top-level roots; `menu-positioning-inputs.spec.ts` guards the three roots
937
+ * against drift.
938
+ */
939
+ align = input(MENU_POSITIONING_DEFAULTS.align, /* @ts-ignore */
940
+ ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
941
+ /** Gap (px) along the cross axis. Default `0`. */
942
+ alignOffset = input(MENU_POSITIONING_DEFAULTS.alignOffset, { ...(ngDevMode ? { debugName: "alignOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
943
+ /** When `true` (default), `flip` and `shift` keep the submenu inside the viewport. */
944
+ avoidCollisions = input(MENU_POSITIONING_DEFAULTS.avoidCollisions, { ...(ngDevMode ? { debugName: "avoidCollisions" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
945
+ /**
946
+ * Padding (px) applied uniformly to flip / shift / size. Defaults to `8`
947
+ * from `provideForMenuDefaults` for the surrounding scope.
948
+ */
949
+ collisionPadding = input(this.#defaults.collisionPadding, { ...(ngDevMode ? { debugName: "collisionPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
950
+ /** Padding (px) for the `arrow` middleware. Default `0`. */
951
+ arrowPadding = input(MENU_POSITIONING_DEFAULTS.arrowPadding, { ...(ngDevMode ? { debugName: "arrowPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
952
+ /** Stickiness behaviour for `shift`. Default `'partial'`. */
953
+ sticky = input(MENU_POSITIONING_DEFAULTS.sticky, /* @ts-ignore */
954
+ ...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
955
+ /** When `true`, sets `data-detached=""` while the parent item is scrolled off-screen. */
956
+ hideWhenDetached = input(MENU_POSITIONING_DEFAULTS.hideWhenDetached, { ...(ngDevMode ? { debugName: "hideWhenDetached" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
957
+ /**
958
+ * When `true` (default), the menu is clipped until floating-ui resolves
959
+ * its first position, preventing a flash at the viewport corner. Set to
960
+ * `false` so a dramatic `animate.enter` plays from its first frame.
961
+ */
962
+ clipUntilPositioned = input(MENU_POSITIONING_DEFAULTS.clipUntilPositioned, { ...(ngDevMode ? { debugName: "clipUntilPositioned" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
963
+ /**
964
+ * When `true` (default), arrow-key navigation wraps from the last enabled
965
+ * item back to the first (and vice versa). When `false`, navigation stops
966
+ * at the ends.
967
+ */
968
+ loop = input(true, { ...(ngDevMode ? { debugName: "loop" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
969
+ /**
970
+ * When true, the sub-trigger interaction is ignored and any open submenu
971
+ * stays open until the consumer flips `open`. Layered on top of the parent
972
+ * menu's own `disabled`.
973
+ */
974
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
975
+ /** When true (default), Escape, pointer-down outside, and focus outside close the submenu. */
976
+ dismissible = input(true, { ...(ngDevMode ? { debugName: "dismissible" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
977
+ /** When true (default), focus returns to the sub-trigger on close. */
978
+ returnFocus = input(true, { ...(ngDevMode ? { debugName: "returnFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
979
+ /** Manual `aria-label` on `[forMenuSubContent]`. Use when the sub-trigger isn't a meaningful name. */
980
+ ariaLabel = input(null, /* @ts-ignore */
981
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
982
+ /**
983
+ * Fires when Escape is pressed while the submenu is open, just before it
984
+ * closes. Call `preventDefault()` on the emitted veto to keep the submenu
985
+ * open and suppress the Escape-driven close.
986
+ */
987
+ escapeKeyDown = output();
988
+ /**
989
+ * Fires on a pointer-down outside the submenu (and outside its exempt
990
+ * parent content), just before it closes. Call `preventDefault()` on the
991
+ * veto to keep the submenu open.
992
+ */
993
+ pointerDownOutside = output();
994
+ /**
995
+ * Fires when focus moves outside the submenu, just before it closes. Call
996
+ * `preventDefault()` on the veto to keep the submenu open.
997
+ */
998
+ focusOutside = output();
999
+ /**
1000
+ * Composite outside-interaction channel: fires for either a
1001
+ * pointer-down-outside or a focus-outside, just before the submenu closes.
1002
+ * Call `preventDefault()` on the veto to keep the submenu open regardless of
1003
+ * which interaction triggered it.
1004
+ */
1005
+ interactOutside = output();
1006
+ /**
1007
+ * Fires just before the submenu sends focus to its first / last
1008
+ * enabled item on mount. Call `preventDefault()` on the emitted veto
1009
+ * to skip the imperative focus move.
1010
+ */
1011
+ autoFocusOnOpen = output();
1012
+ /**
1013
+ * Fires just before focus returns to the parent item on unmount. Call
1014
+ * `preventDefault()` on the veto to suppress the return-focus.
1015
+ */
1016
+ autoFocusOnClose = output();
1017
+ _overlay = createMenuOverlay('for-menu-sub', {
1018
+ open: this.open,
1019
+ disabled: this.disabled,
1020
+ dismissible: this.dismissible,
1021
+ loop: this.loop,
1022
+ escapeKeyDown: this.escapeKeyDown,
1023
+ pointerDownOutside: this.pointerDownOutside,
1024
+ focusOutside: this.focusOutside,
1025
+ interactOutside: this.interactOutside,
1026
+ autoFocusOnOpen: this.autoFocusOnOpen,
1027
+ autoFocusOnClose: this.autoFocusOnClose,
1028
+ // A keyboard / click open supersedes any in-flight hover scheduling and
1029
+ // moves focus into the submenu (unlike the pointer path).
1030
+ onOpen: () => {
1031
+ this.#cancelPointerScheduling();
1032
+ this.#suppressFocusMoves = false;
1033
+ },
1034
+ // Propagate up so item activation, Tab, and outside-pointer collapse
1035
+ // the entire menu chain. `'escape'` collapses only this level (per APG);
1036
+ // `'programmatic'` is the consumer's own write — no propagation either.
1037
+ onClose: (reason) => {
1038
+ this.#cancelPointerScheduling();
1039
+ this.#suppressFocusMoves = false;
1040
+ if (reason !== 'escape' && reason !== 'programmatic') {
1041
+ this.parentMenu.closeMenu(reason);
1042
+ }
1043
+ },
1044
+ });
1045
+ anchor = computed(() => this._overlay.trigger(), /* @ts-ignore */
1046
+ ...(ngDevMode ? [{ debugName: "anchor" }] : /* istanbul ignore next */ []));
1047
+ // --- Pointer-driven (hover) open/close state. Additive to click/keyboard. ---
1048
+ #openAction = createDebouncedAction(() => this.#openByPointer());
1049
+ #closeAction = createDebouncedAction(() => this.#closeByPointer());
1050
+ #graceTimer = null;
1051
+ #detachGrace = null;
1052
+ #detachContentPointer = null;
1053
+ /**
1054
+ * True while the current open/close transition is pointer-driven, so the
1055
+ * shared content's auto-focus-on-open and return-focus-on-close are vetoed:
1056
+ * hovering must never steal focus into the submenu nor yank it back to the
1057
+ * trigger. Reset to `false` by every keyboard / click / programmatic path.
1058
+ */
1059
+ #suppressFocusMoves = false;
1060
+ /**
1061
+ * Submenus exempt the parent menu's content. Clicks on parent menu items
1062
+ * activate via the item's own click handler (which propagates `closeMenu`
1063
+ * upward through the whole chain) instead of firing the submenu's
1064
+ * outside-close.
1065
+ */
1066
+ dismissableExemptions = computed(() => {
1067
+ const parentContent = this.parentMenu.content();
1068
+ return parentContent ? [parentContent] : [];
1069
+ }, /* @ts-ignore */
1070
+ ...(ngDevMode ? [{ debugName: "dismissableExemptions" }] : /* istanbul ignore next */ []));
1071
+ constructor() {
1072
+ super();
1073
+ const parent = inject(FOR_MENU_CONTEXT, { skipSelf: true, optional: true });
1074
+ if (!parent) {
1075
+ throw new Error('[forty-cdk/menu] [forMenuSub] must be inside a [forDropdownMenu], [forContextMenu], or another [forMenuSub] element.');
1076
+ }
1077
+ this.parentMenu = parent;
1078
+ inject(DestroyRef).onDestroy(() => this.#teardownPointer());
1079
+ }
1080
+ registerContent(el) {
1081
+ this._overlay.registerContent(el);
1082
+ this.#attachContentPointer(el);
1083
+ }
1084
+ unregisterContent(el) {
1085
+ if (this._overlay.content() === el) {
1086
+ this._overlay.unregisterContent(el);
1087
+ this.#detachContentPointer?.();
1088
+ this.#detachContentPointer = null;
1089
+ }
1090
+ }
1091
+ closeMenu(reason) {
1092
+ this._overlay.closeMenu(reason);
1093
+ }
1094
+ // --- Pointer-driven (hover) open/close. Additive to click / keyboard. ---
1095
+ /**
1096
+ * Schedule a hover-open of the submenu after `subMenuOpenDelay`. Re-entering
1097
+ * the trigger also keeps every ancestor menu alive and aborts a pending
1098
+ * close. No-op when disabled or already open / scheduled.
1099
+ */
1100
+ scheduleOpenByPointer() {
1101
+ if (this.disabled()) {
1102
+ return;
1103
+ }
1104
+ this.#keepChainOpen();
1105
+ this.cancelPendingClose();
1106
+ if (this.open() || this.#openAction.isPending()) {
1107
+ return;
1108
+ }
1109
+ this.#openAction.schedule(this.#defaults.subMenuOpenDelay);
1110
+ }
1111
+ /**
1112
+ * The pointer left the sub-trigger. Cancels a not-yet-fired hover-open; if
1113
+ * the submenu is already open, arms the pointer-grace "safe triangle" toward
1114
+ * the content so travelling into the submenu doesn't close it.
1115
+ */
1116
+ onTriggerPointerLeave(cursor) {
1117
+ this.#clearOpenTimer();
1118
+ if (!this.open()) {
1119
+ return;
1120
+ }
1121
+ this.#armPointerGrace(cursor);
1122
+ }
1123
+ /** Schedule a hover-close of the submenu after `subMenuCloseDelay`. */
1124
+ scheduleCloseByPointer() {
1125
+ this.#clearOpenTimer();
1126
+ if (!this.open() || this.#closeAction.isPending()) {
1127
+ return;
1128
+ }
1129
+ this.#closeAction.schedule(this.#defaults.subMenuCloseDelay);
1130
+ }
1131
+ /** Cancel a pending hover-close and disarm the pointer-grace tracker. */
1132
+ cancelPendingClose() {
1133
+ this.#closeAction.cancel();
1134
+ this.#disarmPointerGrace();
1135
+ }
1136
+ #openByPointer() {
1137
+ if (this.disabled()) {
1138
+ return;
1139
+ }
1140
+ this.cancelPendingClose();
1141
+ // Hover-open never steals focus — it stays on whatever the user was on.
1142
+ this.#suppressFocusMoves = true;
1143
+ this.open.set(true);
1144
+ }
1145
+ #closeByPointer() {
1146
+ this.#disarmPointerGrace();
1147
+ this.#closeAction.cancel();
1148
+ // Hover-close affects only this level (like Escape / programmatic — no
1149
+ // upward propagation) and suppresses the trigger return-focus.
1150
+ this.#suppressFocusMoves = true;
1151
+ this.open.set(false);
1152
+ }
1153
+ #armPointerGrace(cursor) {
1154
+ const content = this._overlay.content();
1155
+ if (!this.#isBrowser || !content) {
1156
+ this.scheduleCloseByPointer();
1157
+ return;
1158
+ }
1159
+ const rect = content.getBoundingClientRect();
1160
+ const trigger = this.trigger();
1161
+ const side = trigger ? resolveGraceSide(trigger.getBoundingClientRect(), rect) : this.side();
1162
+ const polygon = buildSubmenuGracePolygon(cursor, rect, side);
1163
+ this.#disarmPointerGrace();
1164
+ this.#detachGrace = attachPointerGrace(content.ownerDocument, polygon, () => {
1165
+ this.#disarmPointerGrace();
1166
+ this.scheduleCloseByPointer();
1167
+ });
1168
+ this.#graceTimer = setTimeout(() => {
1169
+ this.#graceTimer = null;
1170
+ this.#detachGrace?.();
1171
+ this.#detachGrace = null;
1172
+ this.scheduleCloseByPointer();
1173
+ }, Math.max(0, this.#defaults.subMenuPointerGraceDuration));
1174
+ }
1175
+ #disarmPointerGrace() {
1176
+ if (this.#detachGrace) {
1177
+ this.#detachGrace();
1178
+ this.#detachGrace = null;
1179
+ }
1180
+ if (this.#graceTimer !== null) {
1181
+ clearTimeout(this.#graceTimer);
1182
+ this.#graceTimer = null;
1183
+ }
1184
+ }
1185
+ #onContentPointerEnter() {
1186
+ // Arrived at (or moving within) the content — keep it and every ancestor
1187
+ // submenu open.
1188
+ this.cancelPendingClose();
1189
+ this.#keepChainOpen();
1190
+ }
1191
+ #onContentPointerLeave() {
1192
+ this.scheduleCloseByPointer();
1193
+ }
1194
+ /** Abort pending hover-closes on this submenu and every ancestor menu. */
1195
+ #keepChainOpen() {
1196
+ let parent = this.parentMenu;
1197
+ while (parent) {
1198
+ parent.cancelPendingClose?.();
1199
+ parent = parent.parentMenu;
1200
+ }
1201
+ }
1202
+ #cancelPointerScheduling() {
1203
+ this.#clearOpenTimer();
1204
+ this.cancelPendingClose();
1205
+ }
1206
+ #clearOpenTimer() {
1207
+ this.#openAction.cancel();
1208
+ }
1209
+ #attachContentPointer(el) {
1210
+ this.#detachContentPointer?.();
1211
+ const onEnter = (event) => {
1212
+ if (event.pointerType !== '' && event.pointerType !== 'mouse') {
1213
+ return;
1214
+ }
1215
+ this.#onContentPointerEnter();
1216
+ };
1217
+ const onLeave = (event) => {
1218
+ if (event.pointerType !== '' && event.pointerType !== 'mouse') {
1219
+ return;
1220
+ }
1221
+ this.#onContentPointerLeave();
1222
+ };
1223
+ el.addEventListener('pointerenter', onEnter);
1224
+ el.addEventListener('pointerleave', onLeave);
1225
+ this.#detachContentPointer = () => {
1226
+ el.removeEventListener('pointerenter', onEnter);
1227
+ el.removeEventListener('pointerleave', onLeave);
1228
+ };
1229
+ }
1230
+ #teardownPointer() {
1231
+ this.#clearOpenTimer();
1232
+ this.#closeAction.cancel();
1233
+ this.#disarmPointerGrace();
1234
+ this.#detachContentPointer?.();
1235
+ this.#detachContentPointer = null;
1236
+ }
1237
+ emitAutoFocusOnOpen() {
1238
+ // Hover-open leaves focus where it is — only keyboard / click moves it in.
1239
+ if (this.#suppressFocusMoves) {
1240
+ return true;
1241
+ }
1242
+ return emitVetoableEvent(this.autoFocusOnOpen);
1243
+ }
1244
+ emitAutoFocusOnClose() {
1245
+ // Hover-close must not yank focus back to the trigger.
1246
+ if (this.#suppressFocusMoves) {
1247
+ return true;
1248
+ }
1249
+ return emitVetoableEvent(this.autoFocusOnClose);
1250
+ }
1251
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuSub, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1252
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForMenuSub, isStandalone: true, selector: "[forMenuSub]", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, _dirInput: { classPropertyName: "_dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, _sideInput: { classPropertyName: "_sideInput", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, sideOffset: { classPropertyName: "sideOffset", publicName: "sideOffset", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", 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 }, 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 }, returnFocus: { classPropertyName: "returnFocus", publicName: "returnFocus", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", escapeKeyDown: "escapeKeyDown", pointerDownOutside: "pointerDownOutside", focusOutside: "focusOutside", interactOutside: "interactOutside", autoFocusOnOpen: "autoFocusOnOpen", autoFocusOnClose: "autoFocusOnClose" }, host: { properties: { "attr.data-state": "open() ? \"open\" : \"closed\"", "attr.data-disabled": "disabled() ? \"\" : null" } }, providers: [{ provide: FOR_MENU_CONTEXT, useExisting: ForMenuSub }], exportAs: ["forMenuSub"], usesInheritance: true, ngImport: i0 });
1253
+ }
1254
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuSub, decorators: [{
1255
+ type: Directive,
1256
+ args: [{
1257
+ selector: '[forMenuSub]',
1258
+ exportAs: 'forMenuSub',
1259
+ host: {
1260
+ '[attr.data-state]': 'open() ? "open" : "closed"',
1261
+ '[attr.data-disabled]': 'disabled() ? "" : null',
1262
+ },
1263
+ providers: [{ provide: FOR_MENU_CONTEXT, useExisting: ForMenuSub }],
1264
+ }]
1265
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], _dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], _sideInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], sideOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "sideOffset", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", 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 }] }], 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 }] }], returnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "returnFocus", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], escapeKeyDown: [{ type: i0.Output, args: ["escapeKeyDown"] }], pointerDownOutside: [{ type: i0.Output, args: ["pointerDownOutside"] }], focusOutside: [{ type: i0.Output, args: ["focusOutside"] }], interactOutside: [{ type: i0.Output, args: ["interactOutside"] }], autoFocusOnOpen: [{ type: i0.Output, args: ["autoFocusOnOpen"] }], autoFocusOnClose: [{ type: i0.Output, args: ["autoFocusOnClose"] }] } });
1266
+
1267
+ /**
1268
+ * The `menuitem` inside the parent menu that opens its `[forMenuSub]`.
1269
+ * Apply on a `<button>` so Space / Enter dispatch native click events that
1270
+ * toggle the submenu via `(click)`.
1271
+ *
1272
+ * Wires `aria-haspopup="menu"`, `aria-expanded`, and `aria-controls`
1273
+ * pointing to the submenu's content. Registers itself as a `menuitem` in
1274
+ * the **parent** menu's collection (so parent navigation reaches it),
1275
+ * while reading open state from the **submenu**.
1276
+ *
1277
+ * Keyboard:
1278
+ * - **Click / Enter / Space** — toggle the submenu (focus first item on open).
1279
+ * - **ArrowRight (LTR) / ArrowLeft (RTL)** — open the submenu and focus its first item.
1280
+ * - **ArrowDown / ArrowUp / Home / End** — navigate parent's items.
1281
+ * - **ArrowLeft (LTR) / ArrowRight (RTL)** — when the parent menu is itself
1282
+ * a submenu, close the parent (return to grandparent's trigger). No-op at
1283
+ * the top level.
1284
+ * - **Tab** — close the entire menu chain.
1285
+ * - **Typeahead** — printable keys delegate to parent's typeahead.
1286
+ *
1287
+ * Pointer (mouse):
1288
+ * - **pointermove** — drop the parent menu's hover highlight so no sibling item
1289
+ * stays active while the submenu opens. The sub-trigger reflects no
1290
+ * `data-highlighted` of its own and does not take focus (hover-open never
1291
+ * moves focus, per #332).
1292
+ * - **pointerenter** — open the submenu after the configured `subMenuOpenDelay`
1293
+ * ({@link provideForMenuDefaults}), without moving focus into it.
1294
+ * - **pointerleave** — close after `subMenuCloseDelay`, unless the pointer
1295
+ * travels toward the open submenu through the pointer-grace "safe triangle".
1296
+ *
1297
+ * Touch / pen never hover, so submenus open by tap (the native click) on those
1298
+ * pointer types — the hover listeners are gated to `pointerType === 'mouse'`.
1299
+ *
1300
+ * Pointer-driven opens (click / tap, detected by the `pointerdown` preceding
1301
+ * the click) move focus to the submenu's first item without highlighting it;
1302
+ * keyboard activation (Enter / Space / the open arrow) highlights it.
1303
+ */
1304
+ class ForMenuSubTrigger {
1305
+ submenu = injectMenuContext('ForMenuSubTrigger');
1306
+ #host = inject(ElementRef);
1307
+ #pointerActivation = false;
1308
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1309
+ effectiveDisabled = computed(() => this.disabled() || this.submenu.disabled(), /* @ts-ignore */
1310
+ ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
1311
+ constructor() {
1312
+ if (!this.submenu.parentMenu) {
1313
+ throw new Error('[forty-cdk/menu] [forMenuSubTrigger] must be inside a [forMenuSub] inside a parent menu.');
1314
+ }
1315
+ const parent = this.submenu.parentMenu;
1316
+ const handle = {
1317
+ host: this.#host.nativeElement,
1318
+ disabled: this.effectiveDisabled,
1319
+ };
1320
+ registerHandle(handle, (h) => parent.registerItem(h), (h) => parent.unregisterItem(h));
1321
+ registerHandle(this.#host.nativeElement, (el) => this.submenu.registerTrigger(el), (el) => this.submenu.unregisterTrigger(el));
1322
+ }
1323
+ onPointerDown() {
1324
+ this.#pointerActivation = true;
1325
+ }
1326
+ onClick() {
1327
+ const modality = this.#pointerActivation ? 'pointer' : 'keyboard';
1328
+ this.#pointerActivation = false;
1329
+ if (this.effectiveDisabled()) {
1330
+ return;
1331
+ }
1332
+ this.submenu.toggle('first', modality);
1333
+ }
1334
+ onPointerEnter(event) {
1335
+ // Hover is a mouse affordance; touch / pen open the submenu by tap (click).
1336
+ if (event.pointerType !== 'mouse') {
1337
+ return;
1338
+ }
1339
+ if (this.effectiveDisabled()) {
1340
+ return;
1341
+ }
1342
+ this.submenu.scheduleOpenByPointer?.();
1343
+ }
1344
+ onPointerMove(event) {
1345
+ if (event.pointerType !== '' && event.pointerType !== 'mouse') {
1346
+ return;
1347
+ }
1348
+ if (this.effectiveDisabled()) {
1349
+ return;
1350
+ }
1351
+ // The sub-trigger carries no highlight of its own, and hover-open must not
1352
+ // move focus into the submenu (#332). So hovering it just drops the parent
1353
+ // menu's hover highlight, leaving no sibling item active while the submenu
1354
+ // opens — focus stays where it was.
1355
+ this.submenu.parentMenu?.clearItemHighlights();
1356
+ }
1357
+ onPointerLeave(event) {
1358
+ if (event.pointerType !== 'mouse') {
1359
+ return;
1360
+ }
1361
+ this.submenu.onTriggerPointerLeave?.({ x: event.clientX, y: event.clientY });
1362
+ }
1363
+ onKeyDown(event) {
1364
+ this.#pointerActivation = false;
1365
+ if (this.effectiveDisabled()) {
1366
+ return;
1367
+ }
1368
+ const parent = this.submenu.parentMenu;
1369
+ const isRtl = this.submenu.dir() === 'rtl';
1370
+ const openKey = isRtl ? 'ArrowLeft' : 'ArrowRight';
1371
+ const closeParentKey = isRtl ? 'ArrowRight' : 'ArrowLeft';
1372
+ if (event.key === openKey) {
1373
+ event.preventDefault();
1374
+ this.submenu.openMenu('first');
1375
+ return;
1376
+ }
1377
+ // Close-parent key when the *parent* is itself a submenu: collapse the parent.
1378
+ if (event.key === closeParentKey && parent.parentMenu) {
1379
+ event.preventDefault();
1380
+ parent.closeMenu('escape');
1381
+ return;
1382
+ }
1383
+ // Close-parent key when the *parent* is the top menu of a menubar:
1384
+ // switch to the previous / next sibling menu (LTR-ArrowLeft → prev,
1385
+ // RTL-ArrowRight → prev). Open-key on the sub-trigger keeps its own
1386
+ // meaning (open this submenu) — only the close-direction propagates
1387
+ // up to the menubar.
1388
+ if (event.key === closeParentKey && parent.menubar) {
1389
+ event.preventDefault();
1390
+ parent.menubar.switchToSibling('prev');
1391
+ return;
1392
+ }
1393
+ const action = resolveListNavigation(event, { orientation: 'vertical' });
1394
+ if (action) {
1395
+ event.preventDefault();
1396
+ parent.navigate(this.#host.nativeElement, action);
1397
+ return;
1398
+ }
1399
+ if (event.key === 'Tab') {
1400
+ handleMenuTabOut(parent);
1401
+ return;
1402
+ }
1403
+ parent.handleTypeahead(event);
1404
+ }
1405
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuSubTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1406
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForMenuSubTrigger, isStandalone: true, selector: "[forMenuSubTrigger]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menuitem", "type": "button", "tabindex": "-1" }, listeners: { "pointerdown": "onPointerDown()", "click": "onClick()", "keydown": "onKeyDown($event)", "pointerenter": "onPointerEnter($event)", "pointerleave": "onPointerLeave($event)", "pointermove": "onPointerMove($event)" }, properties: { "id": "submenu.triggerId()", "attr.aria-haspopup": "\"menu\"", "attr.aria-expanded": "submenu.open() ? \"true\" : \"false\"", "attr.aria-controls": "submenu.open() ? submenu.contentId() : null", "attr.aria-disabled": "effectiveDisabled() ? \"true\" : null", "attr.data-state": "submenu.open() ? \"open\" : \"closed\"", "attr.data-disabled": "effectiveDisabled() ? \"\" : null" } }, exportAs: ["forMenuSubTrigger"], ngImport: i0 });
1407
+ }
1408
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForMenuSubTrigger, decorators: [{
1409
+ type: Directive,
1410
+ args: [{
1411
+ selector: '[forMenuSubTrigger]',
1412
+ exportAs: 'forMenuSubTrigger',
1413
+ host: {
1414
+ role: 'menuitem',
1415
+ type: 'button',
1416
+ tabindex: '-1',
1417
+ '[id]': 'submenu.triggerId()',
1418
+ '[attr.aria-haspopup]': '"menu"',
1419
+ '[attr.aria-expanded]': 'submenu.open() ? "true" : "false"',
1420
+ '[attr.aria-controls]': 'submenu.open() ? submenu.contentId() : null',
1421
+ '[attr.aria-disabled]': 'effectiveDisabled() ? "true" : null',
1422
+ '[attr.data-state]': 'submenu.open() ? "open" : "closed"',
1423
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
1424
+ '(pointerdown)': 'onPointerDown()',
1425
+ '(click)': 'onClick()',
1426
+ '(keydown)': 'onKeyDown($event)',
1427
+ '(pointerenter)': 'onPointerEnter($event)',
1428
+ '(pointerleave)': 'onPointerLeave($event)',
1429
+ '(pointermove)': 'onPointerMove($event)',
1430
+ },
1431
+ }]
1432
+ }], ctorParameters: () => [], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
1433
+
1434
+ /**
1435
+ * Generated bundle index. Do not edit.
1436
+ */
1437
+
1438
+ export { FOR_MENU_CHECKBOX_ITEM, FOR_MENU_DEFAULTS, FOR_MENU_GROUP_CONTEXT, FOR_MENU_RADIO_GROUP_CONTEXT, FOR_MENU_RADIO_ITEM, ForMenuCheckboxItem, ForMenuContent, ForMenuGroup, ForMenuGroupLabel, ForMenuItem, ForMenuItemIndicator, ForMenuRadioGroup, ForMenuRadioItem, ForMenuSeparator, ForMenuSub, ForMenuSubTrigger, provideForMenuDefaults };
1439
+ //# sourceMappingURL=forty-cdk-menu.mjs.map