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.
- package/accordion/README.md +122 -0
- package/aspect-ratio/README.md +76 -0
- package/avatar/README.md +100 -0
- package/breadcrumbs/README.md +49 -0
- package/breakpoints/README.md +81 -0
- package/button/README.md +49 -0
- package/calendar/README.md +458 -0
- package/carousel/README.md +358 -0
- package/checkbox/README.md +146 -0
- package/combobox/README.md +535 -0
- package/context-menu/README.md +139 -0
- package/date-field/README.md +184 -0
- package/date-picker/README.md +338 -0
- package/dialog/README.md +388 -0
- package/disclosure/README.md +114 -0
- package/drag-drop/README.md +359 -0
- package/drawer/README.md +560 -0
- package/dropdown-menu/README.md +176 -0
- package/fesm2022/forty-cdk-accordion.mjs +348 -0
- package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
- package/fesm2022/forty-cdk-avatar.mjs +308 -0
- package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
- package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
- package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
- package/fesm2022/forty-cdk-button.mjs +134 -0
- package/fesm2022/forty-cdk-button.mjs.map +1 -0
- package/fesm2022/forty-cdk-calendar.mjs +2034 -0
- package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
- package/fesm2022/forty-cdk-carousel.mjs +968 -0
- package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
- package/fesm2022/forty-cdk-checkbox.mjs +226 -0
- package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-combobox.mjs +2596 -0
- package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
- package/fesm2022/forty-cdk-context-menu.mjs +413 -0
- package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-core.mjs +9022 -0
- package/fesm2022/forty-cdk-core.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-field.mjs +744 -0
- package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
- package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-dialog.mjs +707 -0
- package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
- package/fesm2022/forty-cdk-disclosure.mjs +190 -0
- package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
- package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
- package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
- package/fesm2022/forty-cdk-drawer.mjs +1641 -0
- package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-field.mjs +425 -0
- package/fesm2022/forty-cdk-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-fieldset.mjs +164 -0
- package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
- package/fesm2022/forty-cdk-file-upload.mjs +221 -0
- package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
- package/fesm2022/forty-cdk-hover-card.mjs +496 -0
- package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
- package/fesm2022/forty-cdk-input.mjs +274 -0
- package/fesm2022/forty-cdk-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
- package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
- package/fesm2022/forty-cdk-listbox.mjs +1279 -0
- package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-menu.mjs +1439 -0
- package/fesm2022/forty-cdk-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-menubar.mjs +787 -0
- package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
- package/fesm2022/forty-cdk-meter.mjs +211 -0
- package/fesm2022/forty-cdk-meter.mjs.map +1 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-number-input.mjs +559 -0
- package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-otp-input.mjs +527 -0
- package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-pagination.mjs +323 -0
- package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
- package/fesm2022/forty-cdk-popover.mjs +698 -0
- package/fesm2022/forty-cdk-popover.mjs.map +1 -0
- package/fesm2022/forty-cdk-progress.mjs +226 -0
- package/fesm2022/forty-cdk-progress.mjs.map +1 -0
- package/fesm2022/forty-cdk-radio-group.mjs +378 -0
- package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
- package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
- package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
- package/fesm2022/forty-cdk-search.mjs +205 -0
- package/fesm2022/forty-cdk-search.mjs.map +1 -0
- package/fesm2022/forty-cdk-select.mjs +1661 -0
- package/fesm2022/forty-cdk-select.mjs.map +1 -0
- package/fesm2022/forty-cdk-separator.mjs +82 -0
- package/fesm2022/forty-cdk-separator.mjs.map +1 -0
- package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
- package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
- package/fesm2022/forty-cdk-slider.mjs +803 -0
- package/fesm2022/forty-cdk-slider.mjs.map +1 -0
- package/fesm2022/forty-cdk-stepper.mjs +886 -0
- package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
- package/fesm2022/forty-cdk-switch.mjs +137 -0
- package/fesm2022/forty-cdk-switch.mjs.map +1 -0
- package/fesm2022/forty-cdk-table.mjs +1518 -0
- package/fesm2022/forty-cdk-table.mjs.map +1 -0
- package/fesm2022/forty-cdk-tabs.mjs +400 -0
- package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-field.mjs +593 -0
- package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
- package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-toast.mjs +1153 -0
- package/fesm2022/forty-cdk-toast.mjs.map +1 -0
- package/fesm2022/forty-cdk-toggle.mjs +516 -0
- package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
- package/fesm2022/forty-cdk-toolbar.mjs +374 -0
- package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
- package/fesm2022/forty-cdk-tooltip.mjs +672 -0
- package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
- package/fesm2022/forty-cdk-tree.mjs +2007 -0
- package/fesm2022/forty-cdk-tree.mjs.map +1 -0
- package/fesm2022/forty-cdk-virtualization.mjs +1 -1
- package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
- package/fesm2022/forty-cdk.mjs +0 -43310
- package/fesm2022/forty-cdk.mjs.map +1 -1
- package/field/README.md +97 -0
- package/fieldset/README.md +86 -0
- package/file-upload/README.md +73 -0
- package/hover-card/README.md +171 -0
- package/input/README.md +156 -0
- package/listbox/README.md +424 -0
- package/menu/README.md +181 -0
- package/menubar/README.md +140 -0
- package/meter/README.md +128 -0
- package/navigation-menu/README.md +253 -0
- package/number-input/README.md +171 -0
- package/otp-input/README.md +198 -0
- package/package.json +213 -1
- package/pagination/README.md +61 -0
- package/pane-resizer/README.md +136 -0
- package/popover/README.md +262 -0
- package/progress/README.md +115 -0
- package/radio-group/README.md +129 -0
- package/scroll-area/README.md +184 -0
- package/search/README.md +42 -0
- package/select/README.md +488 -0
- package/separator/README.md +84 -0
- package/signal-forms/README.md +72 -0
- package/slider/README.md +152 -0
- package/stepper/README.md +292 -0
- package/switch/README.md +116 -0
- package/table/README.md +769 -0
- package/tabs/README.md +130 -0
- package/time-field/README.md +157 -0
- package/time-picker/README.md +172 -0
- package/toast/README.md +398 -0
- package/toggle/README.md +224 -0
- package/toolbar/README.md +109 -0
- package/tooltip/README.md +274 -0
- package/tree/README.md +708 -0
- package/types/forty-cdk-accordion.d.ts +242 -0
- package/types/forty-cdk-aspect-ratio.d.ts +59 -0
- package/types/forty-cdk-avatar.d.ts +133 -0
- package/types/forty-cdk-breadcrumbs.d.ts +92 -0
- package/types/forty-cdk-breakpoints.d.ts +141 -0
- package/types/forty-cdk-button.d.ts +80 -0
- package/types/forty-cdk-calendar.d.ts +914 -0
- package/types/forty-cdk-carousel.d.ts +530 -0
- package/types/forty-cdk-checkbox.d.ts +141 -0
- package/types/forty-cdk-combobox.d.ts +1259 -0
- package/types/forty-cdk-context-menu.d.ts +313 -0
- package/types/forty-cdk-core.d.ts +5774 -0
- package/types/forty-cdk-date-field.d.ts +307 -0
- package/types/forty-cdk-date-picker.d.ts +622 -0
- package/types/forty-cdk-dialog.d.ts +546 -0
- package/types/forty-cdk-disclosure.d.ts +127 -0
- package/types/forty-cdk-drag-drop.d.ts +456 -0
- package/types/forty-cdk-drawer.d.ts +871 -0
- package/types/forty-cdk-dropdown-menu.d.ts +242 -0
- package/types/forty-cdk-field.d.ts +236 -0
- package/types/forty-cdk-fieldset.d.ts +119 -0
- package/types/forty-cdk-file-upload.d.ts +124 -0
- package/types/forty-cdk-hover-card.d.ts +320 -0
- package/types/forty-cdk-input.d.ts +169 -0
- package/types/forty-cdk-internationalized-date.d.ts +1 -1
- package/types/forty-cdk-listbox.d.ts +513 -0
- package/types/forty-cdk-menu.d.ts +629 -0
- package/types/forty-cdk-menubar.d.ts +451 -0
- package/types/forty-cdk-meter.d.ts +122 -0
- package/types/forty-cdk-navigation-menu.d.ts +514 -0
- package/types/forty-cdk-number-input.d.ts +319 -0
- package/types/forty-cdk-otp-input.d.ts +248 -0
- package/types/forty-cdk-pagination.d.ts +214 -0
- package/types/forty-cdk-pane-resizer.d.ts +145 -0
- package/types/forty-cdk-popover.d.ts +509 -0
- package/types/forty-cdk-progress.d.ts +143 -0
- package/types/forty-cdk-radio-group.d.ts +222 -0
- package/types/forty-cdk-scroll-area.d.ts +258 -0
- package/types/forty-cdk-search.d.ts +142 -0
- package/types/forty-cdk-select.d.ts +899 -0
- package/types/forty-cdk-separator.d.ts +59 -0
- package/types/forty-cdk-signal-forms.d.ts +58 -0
- package/types/forty-cdk-slider.d.ts +379 -0
- package/types/forty-cdk-stepper.d.ts +650 -0
- package/types/forty-cdk-switch.d.ts +87 -0
- package/types/forty-cdk-table.d.ts +723 -0
- package/types/forty-cdk-tabs.d.ts +235 -0
- package/types/forty-cdk-time-field.d.ts +307 -0
- package/types/forty-cdk-time-picker.d.ts +578 -0
- package/types/forty-cdk-toast.d.ts +598 -0
- package/types/forty-cdk-toggle.d.ts +310 -0
- package/types/forty-cdk-toolbar.d.ts +217 -0
- package/types/forty-cdk-tooltip.d.ts +436 -0
- package/types/forty-cdk-tree.d.ts +688 -0
- 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
|