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