@radix-ng/primitives 0.32.4 → 0.33.1
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/core/index.d.ts +1 -0
- package/core/src/focus-initial.directive.d.ts +9 -0
- package/fesm2022/radix-ng-primitives-accordion.mjs +19 -19
- package/fesm2022/radix-ng-primitives-alert-dialog.mjs +22 -22
- package/fesm2022/radix-ng-primitives-aspect-ratio.mjs +3 -3
- package/fesm2022/radix-ng-primitives-avatar.mjs +16 -16
- package/fesm2022/radix-ng-primitives-checkbox.mjs +16 -16
- package/fesm2022/radix-ng-primitives-collapsible.mjs +9 -9
- package/fesm2022/radix-ng-primitives-config.mjs +3 -3
- package/fesm2022/radix-ng-primitives-context-menu.mjs +34 -34
- package/fesm2022/radix-ng-primitives-core.mjs +26 -7
- package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-dialog.mjs +25 -25
- package/fesm2022/radix-ng-primitives-dropdown-menu.mjs +34 -34
- package/fesm2022/radix-ng-primitives-hover-card.mjs +28 -29
- package/fesm2022/radix-ng-primitives-hover-card.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-label.mjs +3 -3
- package/fesm2022/radix-ng-primitives-menu.mjs +37 -37
- package/fesm2022/radix-ng-primitives-menubar.mjs +31 -31
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs +1771 -0
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -0
- package/fesm2022/radix-ng-primitives-pagination.mjs +28 -28
- package/fesm2022/radix-ng-primitives-popover.mjs +50 -32
- package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-progress.mjs +10 -10
- package/fesm2022/radix-ng-primitives-radio.mjs +12 -12
- package/fesm2022/radix-ng-primitives-roving-focus.mjs +6 -6
- package/fesm2022/radix-ng-primitives-select.mjs +34 -34
- package/fesm2022/radix-ng-primitives-separator.mjs +3 -3
- package/fesm2022/radix-ng-primitives-slider.mjs +31 -31
- package/fesm2022/radix-ng-primitives-stepper.mjs +25 -25
- package/fesm2022/radix-ng-primitives-switch.mjs +13 -13
- package/fesm2022/radix-ng-primitives-tabs.mjs +16 -16
- package/fesm2022/radix-ng-primitives-toggle-group.mjs +9 -9
- package/fesm2022/radix-ng-primitives-toggle.mjs +6 -6
- package/fesm2022/radix-ng-primitives-toolbar.mjs +22 -22
- package/fesm2022/radix-ng-primitives-tooltip.mjs +28 -29
- package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-visually-hidden.mjs +31 -19
- package/fesm2022/radix-ng-primitives-visually-hidden.mjs.map +1 -1
- package/hover-card/src/hover-card-content.directive.d.ts +2 -2
- package/hover-card/src/hover-card-root.directive.d.ts +4 -4
- package/navigation-menu/README.md +3 -0
- package/navigation-menu/index.d.ts +28 -0
- package/navigation-menu/src/navigation-menu-a11y.component.d.ts +15 -0
- package/navigation-menu/src/navigation-menu-content.directive.d.ts +31 -0
- package/navigation-menu/src/navigation-menu-indicator.directive.d.ts +29 -0
- package/navigation-menu/src/navigation-menu-item.directive.d.ts +44 -0
- package/navigation-menu/src/navigation-menu-link.directive.d.ts +17 -0
- package/navigation-menu/src/navigation-menu-list.directive.d.ts +38 -0
- package/navigation-menu/src/navigation-menu-sub.directive.d.ts +19 -0
- package/navigation-menu/src/navigation-menu-trigger.directive.d.ts +33 -0
- package/navigation-menu/src/navigation-menu-viewport.directive.d.ts +61 -0
- package/navigation-menu/src/navigation-menu.directive.d.ts +72 -0
- package/navigation-menu/src/navigation-menu.token.d.ts +36 -0
- package/navigation-menu/src/navigation-menu.types.d.ts +13 -0
- package/navigation-menu/src/utils.d.ts +44 -0
- package/package.json +11 -7
- package/popover/src/popover-content-attributes.component.d.ts +7 -1
- package/popover/src/popover-content.directive.d.ts +2 -2
- package/popover/src/popover-root.directive.d.ts +4 -4
- package/tooltip/src/tooltip-content.directive.d.ts +2 -2
- package/tooltip/src/tooltip-root.directive.d.ts +4 -4
- package/visually-hidden/src/visually-hidden-input-bubble.directive.d.ts +4 -2
- package/visually-hidden/src/visually-hidden.directive.d.ts +2 -0
@@ -0,0 +1,1771 @@
|
|
1
|
+
import * as i0 from '@angular/core';
|
2
|
+
import { EventEmitter, Output, Input, Component, InjectionToken, inject, ElementRef, input, contentChild, signal, Directive, NgZone, TemplateRef, booleanAttribute, Renderer2, computed, effect, untracked, runInInjectionContext, contentChildren, forwardRef, numberAttribute, output, ViewContainerRef, DestroyRef, NgModule } from '@angular/core';
|
3
|
+
import { RdxVisuallyHiddenDirective } from '@radix-ng/primitives/visually-hidden';
|
4
|
+
import { injectDocument, ESCAPE, ENTER, SPACE, TAB, injectWindow, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ARROW_UP } from '@radix-ng/primitives/core';
|
5
|
+
import * as i1 from '@radix-ng/primitives/roving-focus';
|
6
|
+
import { RdxRovingFocusItemDirective, RdxRovingFocusGroupDirective } from '@radix-ng/primitives/roving-focus';
|
7
|
+
import { FocusKeyManager } from '@angular/cdk/a11y';
|
8
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
9
|
+
import { Subject, map, debounce, timer, tap } from 'rxjs';
|
10
|
+
import { usePresence } from '@radix-ng/primitives/presence';
|
11
|
+
|
12
|
+
class RdxNavigationMenuFocusProxyComponent {
|
13
|
+
constructor() {
|
14
|
+
this.triggerElement = null;
|
15
|
+
this.contentElement = null;
|
16
|
+
this.proxyFocus = new EventEmitter();
|
17
|
+
}
|
18
|
+
onFocus(event) {
|
19
|
+
const prevFocusedElement = event.relatedTarget;
|
20
|
+
const wasTriggerFocused = prevFocusedElement === this.triggerElement;
|
21
|
+
const wasFocusFromContent = this.contentElement ? this.contentElement.contains(prevFocusedElement) : false;
|
22
|
+
if (wasTriggerFocused || !wasFocusFromContent) {
|
23
|
+
this.proxyFocus.emit(wasTriggerFocused ? 'start' : 'end');
|
24
|
+
}
|
25
|
+
}
|
26
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuFocusProxyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
27
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.4", type: RdxNavigationMenuFocusProxyComponent, isStandalone: true, selector: "rdx-navigation-menu-focus-proxy", inputs: { triggerElement: "triggerElement", contentElement: "contentElement" }, outputs: { proxyFocus: "proxyFocus" }, ngImport: i0, template: `
|
28
|
+
<span
|
29
|
+
[attr.tabindex]="0"
|
30
|
+
[attr.aria-hidden]="true"
|
31
|
+
(focus)="onFocus($event)"
|
32
|
+
rdxVisuallyHidden
|
33
|
+
feature="focusable"
|
34
|
+
></span>
|
35
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: RdxVisuallyHiddenDirective, selector: "[rdxVisuallyHidden]", inputs: ["feature"] }] }); }
|
36
|
+
}
|
37
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuFocusProxyComponent, decorators: [{
|
38
|
+
type: Component,
|
39
|
+
args: [{
|
40
|
+
selector: 'rdx-navigation-menu-focus-proxy',
|
41
|
+
template: `
|
42
|
+
<span
|
43
|
+
[attr.tabindex]="0"
|
44
|
+
[attr.aria-hidden]="true"
|
45
|
+
(focus)="onFocus($event)"
|
46
|
+
rdxVisuallyHidden
|
47
|
+
feature="focusable"
|
48
|
+
></span>
|
49
|
+
`,
|
50
|
+
imports: [RdxVisuallyHiddenDirective]
|
51
|
+
}]
|
52
|
+
}], propDecorators: { triggerElement: [{
|
53
|
+
type: Input
|
54
|
+
}], contentElement: [{
|
55
|
+
type: Input
|
56
|
+
}], proxyFocus: [{
|
57
|
+
type: Output
|
58
|
+
}] } });
|
59
|
+
class RdxNavigationMenuAriaOwnsComponent {
|
60
|
+
constructor() {
|
61
|
+
this.contentId = '';
|
62
|
+
}
|
63
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuAriaOwnsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
64
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.4", type: RdxNavigationMenuAriaOwnsComponent, isStandalone: true, selector: "rdx-navigation-menu-aria-owns", inputs: { contentId: "contentId" }, ngImport: i0, template: `
|
65
|
+
<span [attr.aria-owns]="contentId" rdxVisuallyHidden feature="fully-hidden"></span>
|
66
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: RdxVisuallyHiddenDirective, selector: "[rdxVisuallyHidden]", inputs: ["feature"] }] }); }
|
67
|
+
}
|
68
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuAriaOwnsComponent, decorators: [{
|
69
|
+
type: Component,
|
70
|
+
args: [{
|
71
|
+
selector: 'rdx-navigation-menu-aria-owns',
|
72
|
+
template: `
|
73
|
+
<span [attr.aria-owns]="contentId" rdxVisuallyHidden feature="fully-hidden"></span>
|
74
|
+
`,
|
75
|
+
imports: [RdxVisuallyHiddenDirective]
|
76
|
+
}]
|
77
|
+
}], propDecorators: { contentId: [{
|
78
|
+
type: Input
|
79
|
+
}] } });
|
80
|
+
|
81
|
+
const RDX_NAVIGATION_MENU_TOKEN = new InjectionToken('RdxNavigationMenuToken');
|
82
|
+
function injectNavigationMenu() {
|
83
|
+
return inject(RDX_NAVIGATION_MENU_TOKEN);
|
84
|
+
}
|
85
|
+
function isRootNavigationMenu(context) {
|
86
|
+
return context.isRootMenu;
|
87
|
+
}
|
88
|
+
function provideNavigationMenuContext(provider) {
|
89
|
+
return {
|
90
|
+
provide: RDX_NAVIGATION_MENU_TOKEN,
|
91
|
+
useExisting: provider
|
92
|
+
};
|
93
|
+
}
|
94
|
+
|
95
|
+
var RdxNavigationMenuAnimationStatus;
|
96
|
+
(function (RdxNavigationMenuAnimationStatus) {
|
97
|
+
RdxNavigationMenuAnimationStatus["OPEN_STARTED"] = "open_started";
|
98
|
+
RdxNavigationMenuAnimationStatus["OPEN_ENDED"] = "open_ended";
|
99
|
+
RdxNavigationMenuAnimationStatus["CLOSED_STARTED"] = "closed_started";
|
100
|
+
RdxNavigationMenuAnimationStatus["CLOSED_ENDED"] = "closed_ended";
|
101
|
+
})(RdxNavigationMenuAnimationStatus || (RdxNavigationMenuAnimationStatus = {}));
|
102
|
+
/**
|
103
|
+
* A stub class solely used to query a single type of focusable element in the navigation menu.
|
104
|
+
*/
|
105
|
+
class RdxNavigationMenuFocusableOption {
|
106
|
+
focus() {
|
107
|
+
throw new Error('Method not implemented.');
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
const ROOT_CONTENT_DISMISS$1 = 'navigationMenu.rootContentDismiss';
|
112
|
+
/**
|
113
|
+
* Generate a unique ID
|
114
|
+
*/
|
115
|
+
function generateId() {
|
116
|
+
return Math.random().toString(36).substring(2, 11);
|
117
|
+
}
|
118
|
+
/**
|
119
|
+
* Get the open state for data-state attribute
|
120
|
+
*/
|
121
|
+
function getOpenStateLabel(open) {
|
122
|
+
return open ? 'open' : 'closed';
|
123
|
+
}
|
124
|
+
/**
|
125
|
+
* Create a trigger ID from base ID and value
|
126
|
+
*/
|
127
|
+
function makeTriggerId(baseId, value) {
|
128
|
+
return `${baseId}-trigger-${value}`;
|
129
|
+
}
|
130
|
+
/**
|
131
|
+
* Create a content ID from base ID and value
|
132
|
+
*/
|
133
|
+
function makeContentId(baseId, value) {
|
134
|
+
return `${baseId}-content-${value}`;
|
135
|
+
}
|
136
|
+
/**
|
137
|
+
* Get the motion attribute for animations
|
138
|
+
*/
|
139
|
+
function getMotionAttribute(currentValue, previousValue, itemValue, itemValues, dir) {
|
140
|
+
// reverse values in RTL
|
141
|
+
const values = dir === 'rtl' ? [...itemValues].reverse() : itemValues;
|
142
|
+
const currentIndex = currentValue !== null ? values.indexOf(currentValue) : -1;
|
143
|
+
const prevIndex = previousValue !== null ? values.indexOf(previousValue) : -1;
|
144
|
+
const isSelected = itemValue === currentValue;
|
145
|
+
const wasSelected = itemValue === previousValue && previousValue !== null;
|
146
|
+
// Preserve motion attribute for items not directly involved in the transition
|
147
|
+
// (This matches React's behaviour, using a ref/signal might be needed
|
148
|
+
// in the component using this function to fully replicate React's prevMotionAttributeRef)
|
149
|
+
// For now, returning null if not involved, as per the original code's intent here.
|
150
|
+
if (!isSelected && !wasSelected) {
|
151
|
+
return null;
|
152
|
+
}
|
153
|
+
// handle transitions between items
|
154
|
+
if (currentIndex !== -1 && prevIndex !== -1) {
|
155
|
+
// if moving to this item (isSelected)
|
156
|
+
if (isSelected) {
|
157
|
+
return currentIndex > prevIndex ? 'from-end' : 'from-start';
|
158
|
+
}
|
159
|
+
// if moving away from this item (wasSelected)
|
160
|
+
if (wasSelected) {
|
161
|
+
return currentIndex > prevIndex ? 'to-start' : 'to-end';
|
162
|
+
}
|
163
|
+
}
|
164
|
+
// handle initial open (prevIndex is -1, currentIndex is valid)
|
165
|
+
if (isSelected && prevIndex === -1) {
|
166
|
+
return null;
|
167
|
+
}
|
168
|
+
// handle closing entirely (currentIndex is -1, prevIndex is valid)
|
169
|
+
if (wasSelected && currentIndex === -1) {
|
170
|
+
return null;
|
171
|
+
}
|
172
|
+
// fallback if none of the above conditions met (should ideally not happen with clear states)
|
173
|
+
return null;
|
174
|
+
}
|
175
|
+
/**
|
176
|
+
* Focus the first element in a list of candidates
|
177
|
+
* @param candidates Array of elements that can receive focus
|
178
|
+
* @param preventScroll Whether to prevent scrolling when focusing
|
179
|
+
* @param activateKeyboardNav Whether to dispatch a dummy keydown event to activate keyboard navigation handlers
|
180
|
+
* @returns Whether focus was successfully moved
|
181
|
+
*/
|
182
|
+
function focusFirst(candidates, preventScroll = false, activateKeyboardNav = true) {
|
183
|
+
const prevFocusedElement = document.activeElement;
|
184
|
+
// sort candidates by tabindex to ensure proper order
|
185
|
+
const sortedCandidates = [...candidates].sort((a, b) => {
|
186
|
+
const aIndex = a.tabIndex || 0;
|
187
|
+
const bIndex = b.tabIndex || 0;
|
188
|
+
return aIndex - bIndex;
|
189
|
+
});
|
190
|
+
const success = sortedCandidates.some((candidate) => {
|
191
|
+
// if focus is already where we want it, do nothing
|
192
|
+
if (candidate === prevFocusedElement)
|
193
|
+
return true;
|
194
|
+
try {
|
195
|
+
candidate.focus({ preventScroll });
|
196
|
+
return document.activeElement !== prevFocusedElement;
|
197
|
+
}
|
198
|
+
catch (e) {
|
199
|
+
console.error('Error focusing element:', e);
|
200
|
+
return false;
|
201
|
+
}
|
202
|
+
});
|
203
|
+
// if focus was moved successfully and we want to activate keyboard navigation,
|
204
|
+
// dispatch a dummy keypress to ensure keyboard handlers are activated
|
205
|
+
if (success && activateKeyboardNav && document.activeElement !== prevFocusedElement) {
|
206
|
+
try {
|
207
|
+
// dispatch a no-op keydown event to activate any keyboard handlers
|
208
|
+
document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {
|
209
|
+
bubbles: true,
|
210
|
+
cancelable: true,
|
211
|
+
key: 'Tab',
|
212
|
+
code: 'Tab'
|
213
|
+
}));
|
214
|
+
}
|
215
|
+
catch (e) {
|
216
|
+
console.error('Error dispatching keyboard event:', e);
|
217
|
+
}
|
218
|
+
}
|
219
|
+
return success;
|
220
|
+
}
|
221
|
+
/**
|
222
|
+
* Get all tabbable candidates in a container
|
223
|
+
*/
|
224
|
+
function getTabbableCandidates(container) {
|
225
|
+
if (!container || !container.querySelectorAll)
|
226
|
+
return [];
|
227
|
+
const TABBABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), ' +
|
228
|
+
'select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), ' +
|
229
|
+
'[contenteditable="true"]:not([tabindex="-1"])';
|
230
|
+
// use querySelector for better browser support
|
231
|
+
const elements = Array.from(container.querySelectorAll(TABBABLE_SELECTOR));
|
232
|
+
// filter out elements that are hidden, have display:none, etc.
|
233
|
+
return elements.filter((element) => {
|
234
|
+
if (element.tabIndex < 0)
|
235
|
+
return false;
|
236
|
+
if (element.hasAttribute('disabled'))
|
237
|
+
return false;
|
238
|
+
if (element.hasAttribute('aria-hidden') && element.getAttribute('aria-hidden') === 'true')
|
239
|
+
return false;
|
240
|
+
// Check if element or any parent is hidden
|
241
|
+
let current = element;
|
242
|
+
while (current) {
|
243
|
+
const style = window.getComputedStyle(current);
|
244
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
245
|
+
return false;
|
246
|
+
}
|
247
|
+
current = current.parentElement;
|
248
|
+
}
|
249
|
+
return true;
|
250
|
+
});
|
251
|
+
}
|
252
|
+
/**
|
253
|
+
* Remove elements from tab order and return a function to restore them
|
254
|
+
*/
|
255
|
+
function removeFromTabOrder(candidates) {
|
256
|
+
const originalValues = new Map();
|
257
|
+
candidates.forEach((candidate) => {
|
258
|
+
// Store original tabindex
|
259
|
+
originalValues.set(candidate, candidate.getAttribute('tabindex'));
|
260
|
+
// Set to -1 to remove from tab order
|
261
|
+
candidate.setAttribute('tabindex', '-1');
|
262
|
+
});
|
263
|
+
// Return restore function
|
264
|
+
return () => {
|
265
|
+
candidates.forEach((candidate) => {
|
266
|
+
const originalValue = originalValues.get(candidate);
|
267
|
+
if (originalValue == null) {
|
268
|
+
candidate.removeAttribute('tabindex');
|
269
|
+
}
|
270
|
+
else {
|
271
|
+
candidate.setAttribute('tabindex', originalValue);
|
272
|
+
}
|
273
|
+
});
|
274
|
+
};
|
275
|
+
}
|
276
|
+
/**
|
277
|
+
* Wrap array around itself at given start index
|
278
|
+
*/
|
279
|
+
function wrapArray(array, startIndex) {
|
280
|
+
return array.map((_, index) => array[(startIndex + index) % array.length]);
|
281
|
+
}
|
282
|
+
|
283
|
+
class RdxNavigationMenuItemDirective {
|
284
|
+
constructor() {
|
285
|
+
this.elementRef = inject(ElementRef);
|
286
|
+
this.context = injectNavigationMenu();
|
287
|
+
this.value = input('');
|
288
|
+
/**
|
289
|
+
* @ignore
|
290
|
+
*/
|
291
|
+
this.triggerOrLink = contentChild(RdxNavigationMenuFocusableOption);
|
292
|
+
this.triggerRef = signal(null);
|
293
|
+
this.contentRef = signal(null);
|
294
|
+
this.focusProxyRef = signal(null);
|
295
|
+
this.wasEscapeCloseRef = signal(false);
|
296
|
+
this._restoreContentTabOrderRef = signal(null);
|
297
|
+
}
|
298
|
+
get restoreContentTabOrderRef() {
|
299
|
+
return this._restoreContentTabOrderRef;
|
300
|
+
}
|
301
|
+
/**
|
302
|
+
* Handle keyboard entry into content from trigger
|
303
|
+
*/
|
304
|
+
onEntryKeyDown() {
|
305
|
+
// Check if we're using a viewport in a root menu
|
306
|
+
if (isRootNavigationMenu(this.context) && this.context.viewport && this.context.viewport()) {
|
307
|
+
const viewport = this.context.viewport();
|
308
|
+
if (viewport) {
|
309
|
+
// find tabbable elements in the viewport
|
310
|
+
const candidates = getTabbableCandidates(viewport);
|
311
|
+
if (candidates.length) {
|
312
|
+
this.ensureTabOrder();
|
313
|
+
// focus the first element
|
314
|
+
focusFirst(candidates);
|
315
|
+
return;
|
316
|
+
}
|
317
|
+
}
|
318
|
+
}
|
319
|
+
// fallback to content if no viewport or no tabbable elements in viewport
|
320
|
+
if (this.contentRef()) {
|
321
|
+
// restore tab order if needed
|
322
|
+
const restoreFn = this._restoreContentTabOrderRef();
|
323
|
+
if (restoreFn)
|
324
|
+
restoreFn();
|
325
|
+
// find and focus first tabbable element
|
326
|
+
const candidates = getTabbableCandidates(this.contentRef());
|
327
|
+
if (candidates.length) {
|
328
|
+
focusFirst(candidates);
|
329
|
+
}
|
330
|
+
}
|
331
|
+
}
|
332
|
+
focus() {
|
333
|
+
this.triggerOrLink()?.focus();
|
334
|
+
}
|
335
|
+
/**
|
336
|
+
* Ensure elements are in the tab order by restoring any previously removed tabindex values
|
337
|
+
*/
|
338
|
+
ensureTabOrder() {
|
339
|
+
const restoreFn = this._restoreContentTabOrderRef();
|
340
|
+
if (restoreFn) {
|
341
|
+
restoreFn();
|
342
|
+
this._restoreContentTabOrderRef.set(null);
|
343
|
+
}
|
344
|
+
}
|
345
|
+
/**
|
346
|
+
* Handle focus coming from the focus proxy element
|
347
|
+
* @param side Which side the focus is coming from (start = from trigger, end = from after content)
|
348
|
+
*/
|
349
|
+
onFocusProxyEnter(side = 'start') {
|
350
|
+
// check for viewport first
|
351
|
+
if (isRootNavigationMenu(this.context) && this.context.viewport && this.context.viewport()) {
|
352
|
+
const viewport = this.context.viewport();
|
353
|
+
if (viewport) {
|
354
|
+
const candidates = getTabbableCandidates(viewport);
|
355
|
+
if (candidates.length) {
|
356
|
+
this.ensureTabOrder();
|
357
|
+
// focus first or last element depending on direction
|
358
|
+
focusFirst(side === 'start' ? candidates : [...candidates].reverse());
|
359
|
+
return;
|
360
|
+
}
|
361
|
+
}
|
362
|
+
}
|
363
|
+
// fallback to content
|
364
|
+
if (this.contentRef()) {
|
365
|
+
// restore tab order if needed
|
366
|
+
const restoreFn = this._restoreContentTabOrderRef();
|
367
|
+
if (restoreFn)
|
368
|
+
restoreFn();
|
369
|
+
// find and focus appropriate element based on direction
|
370
|
+
const candidates = getTabbableCandidates(this.contentRef());
|
371
|
+
if (candidates.length) {
|
372
|
+
// Focus first or last element depending on which direction we're coming from
|
373
|
+
focusFirst(side === 'start' ? candidates : [...candidates].reverse());
|
374
|
+
}
|
375
|
+
}
|
376
|
+
}
|
377
|
+
/**
|
378
|
+
* Handle focus moving outside of the content
|
379
|
+
* Remove elements from tab order when not focused
|
380
|
+
*/
|
381
|
+
onContentFocusOutside() {
|
382
|
+
// get all tabbable elements from both viewport and content
|
383
|
+
let allCandidates = [];
|
384
|
+
// check viewport first
|
385
|
+
if (isRootNavigationMenu(this.context) && this.context.viewport && this.context.viewport()) {
|
386
|
+
const viewport = this.context.viewport();
|
387
|
+
if (viewport) {
|
388
|
+
allCandidates = getTabbableCandidates(viewport);
|
389
|
+
}
|
390
|
+
}
|
391
|
+
// ... also check direct content
|
392
|
+
if (this.contentRef()) {
|
393
|
+
const contentCandidates = getTabbableCandidates(this.contentRef());
|
394
|
+
allCandidates = [...allCandidates, ...contentCandidates];
|
395
|
+
}
|
396
|
+
// remove from tab order and store restore function
|
397
|
+
if (allCandidates.length) {
|
398
|
+
this._restoreContentTabOrderRef.set(removeFromTabOrder(allCandidates));
|
399
|
+
}
|
400
|
+
}
|
401
|
+
/**
|
402
|
+
* Handle content being closed from root menu
|
403
|
+
*/
|
404
|
+
onRootContentClose() {
|
405
|
+
this.onContentFocusOutside();
|
406
|
+
}
|
407
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
408
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "19.2.4", type: RdxNavigationMenuItemDirective, isStandalone: true, selector: "[rdxNavigationMenuItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.value": "value()" } }, queries: [{ propertyName: "triggerOrLink", first: true, predicate: RdxNavigationMenuFocusableOption, descendants: true, isSignal: true }], exportAs: ["rdxNavigationMenuItem"], ngImport: i0 }); }
|
409
|
+
}
|
410
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuItemDirective, decorators: [{
|
411
|
+
type: Directive,
|
412
|
+
args: [{
|
413
|
+
selector: '[rdxNavigationMenuItem]',
|
414
|
+
host: {
|
415
|
+
'[attr.value]': 'value()'
|
416
|
+
},
|
417
|
+
exportAs: 'rdxNavigationMenuItem'
|
418
|
+
}]
|
419
|
+
}] });
|
420
|
+
|
421
|
+
class RdxNavigationMenuContentDirective {
|
422
|
+
constructor() {
|
423
|
+
this.elementRef = inject(ElementRef);
|
424
|
+
this.ngZone = inject(NgZone);
|
425
|
+
this.template = inject(TemplateRef);
|
426
|
+
this.document = injectDocument();
|
427
|
+
this.item = inject(RdxNavigationMenuItemDirective);
|
428
|
+
this.context = injectNavigationMenu();
|
429
|
+
/**
|
430
|
+
* Used to keep the content rendered and available in the DOM, even when closed.
|
431
|
+
* Useful for animations or SEO.
|
432
|
+
* @default false
|
433
|
+
*/
|
434
|
+
this.forceMount = input(false, { transform: booleanAttribute });
|
435
|
+
/** @ignore */
|
436
|
+
this.contentId = makeContentId(this.context.baseId, this.item.value());
|
437
|
+
/** @ignore */
|
438
|
+
this.triggerId = makeTriggerId(this.context.baseId, this.item.value());
|
439
|
+
this.escapeHandler = null;
|
440
|
+
}
|
441
|
+
set rdxNavigationMenuContent(value) {
|
442
|
+
// structural directive requires this input even if unused
|
443
|
+
}
|
444
|
+
/** @ignore */
|
445
|
+
ngOnInit() {
|
446
|
+
this.item.contentRef.set(this.elementRef.nativeElement);
|
447
|
+
// register template with viewport in root menu via context
|
448
|
+
if (isRootNavigationMenu(this.context) && this.context.onViewportContentChange) {
|
449
|
+
this.context.onViewportContentChange(this.item.value(), {
|
450
|
+
ref: this.elementRef,
|
451
|
+
templateRef: this.template,
|
452
|
+
forceMount: this.forceMount(),
|
453
|
+
value: this.item.value(),
|
454
|
+
getMotionAttribute: this.getMotionAttribute.bind(this),
|
455
|
+
additionalAttrs: {
|
456
|
+
id: this.contentId,
|
457
|
+
'aria-labelledby': this.triggerId,
|
458
|
+
role: 'menu'
|
459
|
+
}
|
460
|
+
});
|
461
|
+
}
|
462
|
+
// add Escape key handler
|
463
|
+
this.escapeHandler = (event) => {
|
464
|
+
if (event.key === ESCAPE && this.context.value() === this.item.value()) {
|
465
|
+
// mark that this close was triggered by Escape
|
466
|
+
this.item.wasEscapeCloseRef.set(true);
|
467
|
+
// close the content
|
468
|
+
if (this.context.onItemDismiss) {
|
469
|
+
this.context.onItemDismiss();
|
470
|
+
}
|
471
|
+
// refocus the trigger
|
472
|
+
setTimeout(() => {
|
473
|
+
const trigger = this.item.triggerRef();
|
474
|
+
if (trigger)
|
475
|
+
trigger.focus();
|
476
|
+
}, 0);
|
477
|
+
event.preventDefault();
|
478
|
+
event.stopPropagation();
|
479
|
+
}
|
480
|
+
};
|
481
|
+
this.ngZone.runOutsideAngular(() => {
|
482
|
+
if (this.escapeHandler) {
|
483
|
+
this.document.addEventListener('keydown', this.escapeHandler);
|
484
|
+
}
|
485
|
+
});
|
486
|
+
}
|
487
|
+
/** @ignore */
|
488
|
+
ngOnDestroy() {
|
489
|
+
// unregister from viewport
|
490
|
+
if (isRootNavigationMenu(this.context) && this.context.onViewportContentRemove) {
|
491
|
+
this.context.onViewportContentRemove(this.item.value());
|
492
|
+
}
|
493
|
+
// remove escape key handler
|
494
|
+
if (this.escapeHandler) {
|
495
|
+
this.document.removeEventListener('keydown', this.escapeHandler);
|
496
|
+
this.escapeHandler = null;
|
497
|
+
}
|
498
|
+
}
|
499
|
+
/** @ignore - Compute motion attribute for animations */
|
500
|
+
getMotionAttribute() {
|
501
|
+
if (!isRootNavigationMenu(this.context))
|
502
|
+
return null;
|
503
|
+
const itemValues = Array.from(this.context.viewportContent?.() ?? new Map()).map(([value]) => value);
|
504
|
+
return getMotionAttribute(this.context.value(), this.context.previousValue(), this.item.value(), itemValues, this.context.dir);
|
505
|
+
}
|
506
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
507
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.4", type: RdxNavigationMenuContentDirective, isStandalone: true, selector: "[rdxNavigationMenuContent]", inputs: { rdxNavigationMenuContent: { classPropertyName: "rdxNavigationMenuContent", publicName: "rdxNavigationMenuContent", isSignal: false, isRequired: false, transformFunction: booleanAttribute }, forceMount: { classPropertyName: "forceMount", publicName: "forceMount", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
|
508
|
+
}
|
509
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuContentDirective, decorators: [{
|
510
|
+
type: Directive,
|
511
|
+
args: [{
|
512
|
+
selector: '[rdxNavigationMenuContent]'
|
513
|
+
}]
|
514
|
+
}], propDecorators: { rdxNavigationMenuContent: [{
|
515
|
+
type: Input,
|
516
|
+
args: [{ transform: booleanAttribute }]
|
517
|
+
}] } });
|
518
|
+
|
519
|
+
class RdxNavigationMenuIndicatorDirective {
|
520
|
+
constructor() {
|
521
|
+
this.context = injectNavigationMenu();
|
522
|
+
this.elementRef = inject(ElementRef);
|
523
|
+
this.renderer = inject(Renderer2);
|
524
|
+
/**
|
525
|
+
* Used to keep the indicator rendered and available in the DOM, even when hidden.
|
526
|
+
* Useful for animations.
|
527
|
+
* @default false
|
528
|
+
*/
|
529
|
+
this.forceMount = input(false, { transform: booleanAttribute });
|
530
|
+
/** @ignore */
|
531
|
+
this._position = signal(null);
|
532
|
+
/** @ignore */
|
533
|
+
this._activeTrigger = signal(null);
|
534
|
+
/** @ignore */
|
535
|
+
this._resizeObserver = new ResizeObserver(() => this.updatePosition());
|
536
|
+
this.isVisible = computed(() => Boolean(this.context.value() || this.forceMount()));
|
537
|
+
// set up effect for tracking active trigger and position
|
538
|
+
effect(() => {
|
539
|
+
// this effect runs when the current value changes
|
540
|
+
const value = this.context.value();
|
541
|
+
untracked(() => {
|
542
|
+
if (value && isRootNavigationMenu(this.context)) {
|
543
|
+
this.findAndSetActiveTrigger();
|
544
|
+
}
|
545
|
+
});
|
546
|
+
});
|
547
|
+
// initialize observers for position tracking
|
548
|
+
runInInjectionContext(this.context, () => {
|
549
|
+
if (isRootNavigationMenu(this.context) && this.context.indicatorTrack) {
|
550
|
+
const track = this.context.indicatorTrack();
|
551
|
+
if (track) {
|
552
|
+
// observe size changes on the track
|
553
|
+
this._resizeObserver.observe(track);
|
554
|
+
}
|
555
|
+
// initial position update if menu is open
|
556
|
+
if (this.context.value()) {
|
557
|
+
setTimeout(() => this.findAndSetActiveTrigger(), 0);
|
558
|
+
}
|
559
|
+
}
|
560
|
+
});
|
561
|
+
}
|
562
|
+
/** @ignore */
|
563
|
+
ngOnDestroy() {
|
564
|
+
this._resizeObserver.disconnect();
|
565
|
+
}
|
566
|
+
/** @ignore */
|
567
|
+
findAndSetActiveTrigger() {
|
568
|
+
if (!isRootNavigationMenu(this.context) || !this.context.indicatorTrack)
|
569
|
+
return;
|
570
|
+
const track = this.context.indicatorTrack();
|
571
|
+
if (!track)
|
572
|
+
return;
|
573
|
+
// find all triggers within the track
|
574
|
+
const triggers = Array.from(track.querySelectorAll('[rdxNavigationMenuTrigger]'));
|
575
|
+
// find the active trigger based on the current menu value
|
576
|
+
const activeTrigger = triggers.find((trigger) => {
|
577
|
+
const item = trigger.closest('[rdxNavigationMenuItem]');
|
578
|
+
if (!item)
|
579
|
+
return false;
|
580
|
+
const value = item.getAttribute('value');
|
581
|
+
return value === this.context.value();
|
582
|
+
});
|
583
|
+
if (activeTrigger && activeTrigger !== this._activeTrigger()) {
|
584
|
+
this._activeTrigger.set(activeTrigger);
|
585
|
+
this.updatePosition();
|
586
|
+
}
|
587
|
+
}
|
588
|
+
/** @ignore */
|
589
|
+
updatePosition() {
|
590
|
+
const trigger = this._activeTrigger();
|
591
|
+
if (!trigger)
|
592
|
+
return;
|
593
|
+
const isHorizontal = this.context.orientation === 'horizontal';
|
594
|
+
// calculate new position
|
595
|
+
const newPosition = {
|
596
|
+
size: isHorizontal ? trigger.offsetWidth : trigger.offsetHeight,
|
597
|
+
offset: isHorizontal ? trigger.offsetLeft : trigger.offsetTop
|
598
|
+
};
|
599
|
+
// only update if position has changed
|
600
|
+
if (JSON.stringify(newPosition) !== JSON.stringify(this._position())) {
|
601
|
+
this._position.set(newPosition);
|
602
|
+
// apply position styles
|
603
|
+
const styles = isHorizontal
|
604
|
+
? {
|
605
|
+
position: 'absolute',
|
606
|
+
left: '0',
|
607
|
+
width: `${newPosition.size}px`,
|
608
|
+
transform: `translateX(${newPosition.offset}px)`
|
609
|
+
}
|
610
|
+
: {
|
611
|
+
position: 'absolute',
|
612
|
+
top: '0',
|
613
|
+
height: `${newPosition.size}px`,
|
614
|
+
transform: `translateY(${newPosition.offset}px)`
|
615
|
+
};
|
616
|
+
Object.entries(styles).forEach(([key, value]) => {
|
617
|
+
this.renderer.setStyle(this.elementRef.nativeElement, key, value);
|
618
|
+
});
|
619
|
+
}
|
620
|
+
}
|
621
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuIndicatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
622
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.4", type: RdxNavigationMenuIndicatorDirective, isStandalone: true, selector: "[rdxNavigationMenuIndicator]", inputs: { forceMount: { classPropertyName: "forceMount", publicName: "forceMount", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, properties: { "attr.data-state": "isVisible() ? \"visible\" : \"hidden\"", "attr.data-orientation": "context.orientation", "style.display": "isVisible() ? null : \"none\"" } }, ngImport: i0 }); }
|
623
|
+
}
|
624
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuIndicatorDirective, decorators: [{
|
625
|
+
type: Directive,
|
626
|
+
args: [{
|
627
|
+
selector: '[rdxNavigationMenuIndicator]',
|
628
|
+
host: {
|
629
|
+
'[attr.data-state]': 'isVisible() ? "visible" : "hidden"',
|
630
|
+
'[attr.data-orientation]': 'context.orientation',
|
631
|
+
'[style.display]': 'isVisible() ? null : "none"',
|
632
|
+
'aria-hidden': 'true'
|
633
|
+
}
|
634
|
+
}]
|
635
|
+
}], ctorParameters: () => [] });
|
636
|
+
|
637
|
+
const LINK_SELECT = 'navigationMenu.linkSelect';
|
638
|
+
const ROOT_CONTENT_DISMISS = 'navigationMenu.rootContentDismiss';
|
639
|
+
class RdxNavigationMenuLinkDirective extends RdxNavigationMenuFocusableOption {
|
640
|
+
constructor() {
|
641
|
+
super(...arguments);
|
642
|
+
this.rovingFocusItem = inject(RdxRovingFocusItemDirective, { self: true });
|
643
|
+
this.uniqueId = generateId();
|
644
|
+
this.active = input(false, { transform: booleanAttribute });
|
645
|
+
this.onSelect = input();
|
646
|
+
this.elementRef = inject(ElementRef);
|
647
|
+
}
|
648
|
+
ngOnInit() {
|
649
|
+
this.rovingFocusItem.tabStopId = this.elementRef.nativeElement.id || `link-${this.uniqueId}`;
|
650
|
+
}
|
651
|
+
focus() {
|
652
|
+
this.elementRef.nativeElement.focus();
|
653
|
+
}
|
654
|
+
onClick(event) {
|
655
|
+
const target = event.target;
|
656
|
+
// dispatch link select event
|
657
|
+
const linkSelectEvent = new CustomEvent(LINK_SELECT, {
|
658
|
+
bubbles: true,
|
659
|
+
cancelable: true
|
660
|
+
});
|
661
|
+
// add one-time listener for onSelect handler
|
662
|
+
const onSelect = this.onSelect();
|
663
|
+
if (onSelect) {
|
664
|
+
target.addEventListener(LINK_SELECT, onSelect, { once: true });
|
665
|
+
}
|
666
|
+
// dispatch event
|
667
|
+
target.dispatchEvent(linkSelectEvent);
|
668
|
+
// if not prevented and not meta key, dismiss content
|
669
|
+
if (!linkSelectEvent.defaultPrevented && !event.metaKey) {
|
670
|
+
const dismissEvent = new CustomEvent(ROOT_CONTENT_DISMISS, {
|
671
|
+
bubbles: true,
|
672
|
+
cancelable: true
|
673
|
+
});
|
674
|
+
target.dispatchEvent(dismissEvent);
|
675
|
+
}
|
676
|
+
}
|
677
|
+
onKeydown(event) {
|
678
|
+
// activate link on Enter or Space
|
679
|
+
if (event.key === ENTER || event.key === SPACE) {
|
680
|
+
// prevent default behavior like scrolling (Space) or form submission (Enter) BEFORE simulating the click.
|
681
|
+
event.preventDefault();
|
682
|
+
// simulate a click event on the link element itself
|
683
|
+
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
|
684
|
+
this.elementRef.nativeElement.dispatchEvent(clickEvent);
|
685
|
+
return;
|
686
|
+
}
|
687
|
+
}
|
688
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuLinkDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
|
689
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.4", type: RdxNavigationMenuLinkDirective, isStandalone: true, selector: "[rdxNavigationMenuLink]", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, onSelect: { classPropertyName: "onSelect", publicName: "onSelect", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)" }, properties: { "attr.data-active": "active() ? \"\" : undefined", "attr.aria-current": "active() ? \"page\" : undefined" } }, providers: [{ provide: RdxNavigationMenuFocusableOption, useExisting: RdxNavigationMenuLinkDirective }], usesInheritance: true, hostDirectives: [{ directive: i1.RdxRovingFocusItemDirective, inputs: ["focusable", "focusable"] }], ngImport: i0 }); }
|
690
|
+
}
|
691
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuLinkDirective, decorators: [{
|
692
|
+
type: Directive,
|
693
|
+
args: [{
|
694
|
+
selector: '[rdxNavigationMenuLink]',
|
695
|
+
hostDirectives: [{ directive: RdxRovingFocusItemDirective, inputs: ['focusable'] }],
|
696
|
+
host: {
|
697
|
+
'[attr.data-active]': 'active() ? "" : undefined',
|
698
|
+
'[attr.aria-current]': 'active() ? "page" : undefined',
|
699
|
+
'(click)': 'onClick($event)',
|
700
|
+
'(keydown)': 'onKeydown($event)'
|
701
|
+
},
|
702
|
+
providers: [{ provide: RdxNavigationMenuFocusableOption, useExisting: RdxNavigationMenuLinkDirective }]
|
703
|
+
}]
|
704
|
+
}] });
|
705
|
+
|
706
|
+
class RdxNavigationMenuListDirective {
|
707
|
+
constructor() {
|
708
|
+
this.context = injectNavigationMenu();
|
709
|
+
this.elementRef = inject((ElementRef));
|
710
|
+
this.renderer = inject(Renderer2);
|
711
|
+
this.rovingFocusGroup = inject(RdxRovingFocusGroupDirective, { self: true });
|
712
|
+
/**
|
713
|
+
* @private
|
714
|
+
* @ignore
|
715
|
+
*/
|
716
|
+
this.items = contentChildren(forwardRef(() => RdxNavigationMenuItemDirective), { descendants: true });
|
717
|
+
}
|
718
|
+
/**
|
719
|
+
* @ignore
|
720
|
+
*/
|
721
|
+
ngAfterContentInit() {
|
722
|
+
const items = this.items();
|
723
|
+
this.keyManager = new FocusKeyManager(items);
|
724
|
+
if (this.context.orientation === 'horizontal') {
|
725
|
+
this.keyManager.withHorizontalOrientation(this.context.dir || 'ltr');
|
726
|
+
}
|
727
|
+
else {
|
728
|
+
this.keyManager.withVerticalOrientation();
|
729
|
+
}
|
730
|
+
}
|
731
|
+
/**
|
732
|
+
* @ignore
|
733
|
+
*/
|
734
|
+
ngAfterViewInit() {
|
735
|
+
this.rovingFocusGroup.orientation = this.context.orientation;
|
736
|
+
this.rovingFocusGroup.dir = this.context.dir;
|
737
|
+
// looping typically only applies to the root menu bar
|
738
|
+
if (isRootNavigationMenu(this.context)) {
|
739
|
+
this.rovingFocusGroup.loop = this.context.loop ?? false;
|
740
|
+
}
|
741
|
+
else {
|
742
|
+
this.rovingFocusGroup.loop = false;
|
743
|
+
}
|
744
|
+
if (isRootNavigationMenu(this.context) && this.context.onIndicatorTrackChange) {
|
745
|
+
const listElement = this.elementRef.nativeElement;
|
746
|
+
const parent = listElement.parentNode;
|
747
|
+
// ensure parent exists and list hasn't already been wrapped
|
748
|
+
if (parent && !listElement.parentElement?.hasAttribute('data-radix-navigation-menu-list-wrapper')) {
|
749
|
+
// create a wrapper div with relative positioning
|
750
|
+
const wrapper = this.renderer.createElement('div');
|
751
|
+
this.renderer.setAttribute(wrapper, 'data-radix-navigation-menu-list-wrapper', ''); // Add marker
|
752
|
+
this.renderer.setStyle(wrapper, 'position', 'relative');
|
753
|
+
// insert the wrapper before the list element in the parent
|
754
|
+
this.renderer.insertBefore(parent, wrapper, listElement);
|
755
|
+
// move the list element inside the new wrapper
|
756
|
+
this.renderer.appendChild(wrapper, listElement);
|
757
|
+
// register the wrapper element as the track for the indicator positioning
|
758
|
+
this.context.onIndicatorTrackChange(wrapper);
|
759
|
+
}
|
760
|
+
else if (listElement.parentElement?.hasAttribute('data-radix-navigation-menu-list-wrapper')) {
|
761
|
+
// if wrapper somehow already exists, ensure context has the correct reference
|
762
|
+
this.context.onIndicatorTrackChange(listElement.parentElement);
|
763
|
+
}
|
764
|
+
}
|
765
|
+
}
|
766
|
+
/**
|
767
|
+
* @ignore
|
768
|
+
*/
|
769
|
+
onKeydown(event) {
|
770
|
+
if (!this.keyManager.activeItem) {
|
771
|
+
this.keyManager.setFirstItemActive();
|
772
|
+
}
|
773
|
+
if (event.key === TAB && event.shiftKey) {
|
774
|
+
if (this.keyManager.activeItemIndex === 0)
|
775
|
+
return;
|
776
|
+
this.keyManager.setPreviousItemActive();
|
777
|
+
event.preventDefault();
|
778
|
+
}
|
779
|
+
else if (event.key === TAB) {
|
780
|
+
const items = this.items();
|
781
|
+
if (this.keyManager.activeItemIndex === items.length - 1) {
|
782
|
+
return;
|
783
|
+
}
|
784
|
+
this.keyManager.setNextItemActive();
|
785
|
+
event.preventDefault();
|
786
|
+
}
|
787
|
+
else {
|
788
|
+
this.keyManager.onKeydown(event);
|
789
|
+
}
|
790
|
+
}
|
791
|
+
/**
|
792
|
+
* @ignore
|
793
|
+
*/
|
794
|
+
setActiveItem(item) {
|
795
|
+
this.keyManager.setActiveItem(item);
|
796
|
+
}
|
797
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuListDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
798
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "19.2.4", type: RdxNavigationMenuListDirective, isStandalone: true, selector: "[rdxNavigationMenuList]", host: { attributes: { "role": "menubar" }, listeners: { "keydown": "onKeydown($event)" } }, queries: [{ propertyName: "items", predicate: i0.forwardRef(() => RdxNavigationMenuItemDirective), descendants: true, isSignal: true }], hostDirectives: [{ directive: i1.RdxRovingFocusGroupDirective }], ngImport: i0 }); }
|
799
|
+
}
|
800
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuListDirective, decorators: [{
|
801
|
+
type: Directive,
|
802
|
+
args: [{
|
803
|
+
selector: '[rdxNavigationMenuList]',
|
804
|
+
hostDirectives: [RdxRovingFocusGroupDirective],
|
805
|
+
host: {
|
806
|
+
role: 'menubar',
|
807
|
+
'(keydown)': 'onKeydown($event)'
|
808
|
+
}
|
809
|
+
}]
|
810
|
+
}] });
|
811
|
+
|
812
|
+
// define action types for clearer intent
|
813
|
+
var RdxNavigationMenuAction;
|
814
|
+
(function (RdxNavigationMenuAction) {
|
815
|
+
RdxNavigationMenuAction["OPEN"] = "open";
|
816
|
+
RdxNavigationMenuAction["CLOSE"] = "close";
|
817
|
+
})(RdxNavigationMenuAction || (RdxNavigationMenuAction = {}));
|
818
|
+
class RdxNavigationMenuDirective {
|
819
|
+
// State
|
820
|
+
#value;
|
821
|
+
#previousValue;
|
822
|
+
#indicatorTrack;
|
823
|
+
#viewport;
|
824
|
+
#viewportContent;
|
825
|
+
#rootNavigationMenu;
|
826
|
+
#userDismissedByClick;
|
827
|
+
#isOpenDelayed;
|
828
|
+
// pointer tracking
|
829
|
+
#isPointerOverContent;
|
830
|
+
#isPointerOverTrigger;
|
831
|
+
constructor() {
|
832
|
+
this.elementRef = inject(ElementRef);
|
833
|
+
this.document = injectDocument();
|
834
|
+
this.window = injectWindow();
|
835
|
+
// State
|
836
|
+
this.#value = signal('');
|
837
|
+
this.#previousValue = signal('');
|
838
|
+
this.baseId = `rdx-nav-menu-${generateId()}`;
|
839
|
+
this.#indicatorTrack = signal(null);
|
840
|
+
this.#viewport = signal(null);
|
841
|
+
this.#viewportContent = signal(new Map());
|
842
|
+
this.#rootNavigationMenu = signal(this.elementRef.nativeElement);
|
843
|
+
this.#userDismissedByClick = signal(false);
|
844
|
+
this.userDismissedByClick = () => this.#userDismissedByClick();
|
845
|
+
this.resetUserDismissed = () => this.#userDismissedByClick.set(false);
|
846
|
+
// delay timers
|
847
|
+
this.openTimerRef = 0;
|
848
|
+
this.closeTimerRef = 0;
|
849
|
+
this.skipDelayTimerRef = 0;
|
850
|
+
this.#isOpenDelayed = signal(true);
|
851
|
+
// pointer tracking
|
852
|
+
this.#isPointerOverContent = signal(false);
|
853
|
+
this.#isPointerOverTrigger = signal(false);
|
854
|
+
this.documentMouseLeaveHandler = null;
|
855
|
+
this.actionSubject$ = new Subject();
|
856
|
+
this.orientation = 'horizontal';
|
857
|
+
this.dir = 'ltr';
|
858
|
+
this.delayDuration = 200;
|
859
|
+
this.skipDelayDuration = 300;
|
860
|
+
this.loop = false;
|
861
|
+
this.cssAnimation = false;
|
862
|
+
this.cssOpeningAnimation = false;
|
863
|
+
this.cssClosingAnimation = false;
|
864
|
+
this.isRootMenu = true;
|
865
|
+
this.cssAnimationStatus = signal(null);
|
866
|
+
// exposed state as functions for the token
|
867
|
+
this.value = () => this.#value();
|
868
|
+
this.previousValue = () => this.#previousValue();
|
869
|
+
this.rootNavigationMenu = () => this.#rootNavigationMenu();
|
870
|
+
this.indicatorTrack = () => this.#indicatorTrack();
|
871
|
+
this.viewport = () => this.#viewport();
|
872
|
+
this.viewportContent = () => this.#viewportContent();
|
873
|
+
// exposed pointer state
|
874
|
+
this.setTriggerPointerState = (isOver) => this.#isPointerOverTrigger.set(isOver);
|
875
|
+
this.setContentPointerState = (isOver) => this.#isPointerOverContent.set(isOver);
|
876
|
+
this.isPointerInSystem = () => this.#isPointerOverContent() || this.#isPointerOverTrigger();
|
877
|
+
// exposed animation state
|
878
|
+
this.getCssAnimation = () => this.cssAnimation;
|
879
|
+
this.getCssOpeningAnimation = () => this.cssOpeningAnimation;
|
880
|
+
this.getCssClosingAnimation = () => this.cssClosingAnimation;
|
881
|
+
effect(() => {
|
882
|
+
const value = this.#value();
|
883
|
+
if (value) {
|
884
|
+
this.window.clearTimeout(this.skipDelayTimerRef);
|
885
|
+
if (this.skipDelayDuration > 0) {
|
886
|
+
this.#isOpenDelayed.set(false);
|
887
|
+
}
|
888
|
+
}
|
889
|
+
else {
|
890
|
+
// menu is closed, start skip delay timer
|
891
|
+
this.window.clearTimeout(this.skipDelayTimerRef);
|
892
|
+
this.skipDelayTimerRef = this.window.setTimeout(() => {
|
893
|
+
this.#isOpenDelayed.set(true);
|
894
|
+
}, this.skipDelayDuration);
|
895
|
+
}
|
896
|
+
});
|
897
|
+
this.actionSubject$
|
898
|
+
.pipe(map((config) => {
|
899
|
+
// different delays for open vs close (better ux)
|
900
|
+
const duration = config.action === RdxNavigationMenuAction.OPEN ? this.delayDuration : 150;
|
901
|
+
return { ...config, duration };
|
902
|
+
}), debounce((config) => timer(config.duration)), tap((config) => {
|
903
|
+
switch (config.action) {
|
904
|
+
case RdxNavigationMenuAction.OPEN:
|
905
|
+
if (config.itemValue) {
|
906
|
+
this.setValue(config.itemValue);
|
907
|
+
}
|
908
|
+
break;
|
909
|
+
case RdxNavigationMenuAction.CLOSE:
|
910
|
+
// only close if not hovering over any part of the system
|
911
|
+
if (!this.isPointerInSystem()) {
|
912
|
+
this.setValue('');
|
913
|
+
}
|
914
|
+
break;
|
915
|
+
}
|
916
|
+
}), takeUntilDestroyed())
|
917
|
+
.subscribe();
|
918
|
+
// set up document mouseleave handler to close menu when mouse leaves window
|
919
|
+
this.documentMouseLeaveHandler = () => this.handleClose();
|
920
|
+
this.document.addEventListener('mouseleave', this.documentMouseLeaveHandler);
|
921
|
+
}
|
922
|
+
ngOnDestroy() {
|
923
|
+
this.window.clearTimeout(this.openTimerRef);
|
924
|
+
this.window.clearTimeout(this.closeTimerRef);
|
925
|
+
this.window.clearTimeout(this.skipDelayTimerRef);
|
926
|
+
// clean up document event listener
|
927
|
+
if (this.documentMouseLeaveHandler) {
|
928
|
+
document.removeEventListener('mouseleave', this.documentMouseLeaveHandler);
|
929
|
+
}
|
930
|
+
}
|
931
|
+
onIndicatorTrackChange(track) {
|
932
|
+
this.#indicatorTrack.set(track);
|
933
|
+
}
|
934
|
+
onViewportChange(viewport) {
|
935
|
+
this.#viewport.set(viewport);
|
936
|
+
}
|
937
|
+
onTriggerEnter(itemValue) {
|
938
|
+
// skip opening if user explicitly dismissed this menu
|
939
|
+
if (this.#userDismissedByClick() && itemValue === this.#previousValue()) {
|
940
|
+
return;
|
941
|
+
}
|
942
|
+
this.window.clearTimeout(this.openTimerRef);
|
943
|
+
this.window.clearTimeout(this.closeTimerRef);
|
944
|
+
if (this.#isOpenDelayed()) {
|
945
|
+
this.handleDelayedOpen(itemValue);
|
946
|
+
}
|
947
|
+
else {
|
948
|
+
this.handleOpen(itemValue);
|
949
|
+
}
|
950
|
+
}
|
951
|
+
onTriggerLeave() {
|
952
|
+
this.window.clearTimeout(this.openTimerRef);
|
953
|
+
this.startCloseTimer();
|
954
|
+
}
|
955
|
+
onContentEnter() {
|
956
|
+
this.window.clearTimeout(this.closeTimerRef);
|
957
|
+
}
|
958
|
+
onContentLeave() {
|
959
|
+
this.startCloseTimer();
|
960
|
+
}
|
961
|
+
handleClose() {
|
962
|
+
this.actionSubject$.next({ action: RdxNavigationMenuAction.CLOSE });
|
963
|
+
}
|
964
|
+
onItemSelect(itemValue) {
|
965
|
+
const wasOpen = this.#value() === itemValue;
|
966
|
+
const newValue = wasOpen ? '' : itemValue;
|
967
|
+
// if user is closing an open menu, mark as user-dismissed
|
968
|
+
if (wasOpen) {
|
969
|
+
this.#userDismissedByClick.set(true);
|
970
|
+
}
|
971
|
+
else {
|
972
|
+
this.#userDismissedByClick.set(false);
|
973
|
+
}
|
974
|
+
this.setValue(newValue);
|
975
|
+
}
|
976
|
+
onItemDismiss() {
|
977
|
+
this.setValue('');
|
978
|
+
}
|
979
|
+
onViewportContentChange(contentValue, contentData) {
|
980
|
+
const newMap = new Map(this.#viewportContent());
|
981
|
+
newMap.set(contentValue, contentData);
|
982
|
+
this.#viewportContent.set(newMap);
|
983
|
+
}
|
984
|
+
onViewportContentRemove(contentValue) {
|
985
|
+
const newMap = new Map(this.#viewportContent());
|
986
|
+
if (newMap.has(contentValue)) {
|
987
|
+
newMap.delete(contentValue);
|
988
|
+
this.#viewportContent.set(newMap);
|
989
|
+
}
|
990
|
+
}
|
991
|
+
setValue(value) {
|
992
|
+
// Store previous value before changing
|
993
|
+
this.#previousValue.set(this.#value());
|
994
|
+
this.#value.set(value);
|
995
|
+
}
|
996
|
+
startCloseTimer() {
|
997
|
+
this.window.clearTimeout(this.closeTimerRef);
|
998
|
+
this.closeTimerRef = this.window.setTimeout(() => {
|
999
|
+
// only close if not hovering over any part of the system
|
1000
|
+
if (!this.isPointerInSystem()) {
|
1001
|
+
this.setValue('');
|
1002
|
+
}
|
1003
|
+
}, 150);
|
1004
|
+
}
|
1005
|
+
handleOpen(itemValue) {
|
1006
|
+
this.window.clearTimeout(this.closeTimerRef);
|
1007
|
+
this.setValue(itemValue);
|
1008
|
+
}
|
1009
|
+
handleDelayedOpen(itemValue) {
|
1010
|
+
const isOpenItem = this.#value() === itemValue;
|
1011
|
+
if (isOpenItem) {
|
1012
|
+
// if the item is already open, clear close timer
|
1013
|
+
this.window.clearTimeout(this.closeTimerRef);
|
1014
|
+
}
|
1015
|
+
else {
|
1016
|
+
// otherwise, start the open timer
|
1017
|
+
this.openTimerRef = this.window.setTimeout(() => {
|
1018
|
+
this.window.clearTimeout(this.closeTimerRef);
|
1019
|
+
this.setValue(itemValue);
|
1020
|
+
}, this.delayDuration);
|
1021
|
+
}
|
1022
|
+
}
|
1023
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
1024
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.4", type: RdxNavigationMenuDirective, isStandalone: true, selector: "[rdxNavigationMenu]", inputs: { orientation: "orientation", dir: "dir", delayDuration: ["delayDuration", "delayDuration", numberAttribute], skipDelayDuration: ["skipDelayDuration", "skipDelayDuration", numberAttribute], loop: ["loop", "loop", booleanAttribute], cssAnimation: ["cssAnimation", "cssAnimation", booleanAttribute], cssOpeningAnimation: ["cssOpeningAnimation", "cssOpeningAnimation", booleanAttribute], cssClosingAnimation: ["cssClosingAnimation", "cssClosingAnimation", booleanAttribute] }, host: { attributes: { "aria-label": "Main", "role": "navigation" }, properties: { "attr.data-orientation": "orientation", "attr.dir": "dir" } }, providers: [provideNavigationMenuContext(RdxNavigationMenuDirective)], exportAs: ["rdxNavigationMenu"], ngImport: i0 }); }
|
1025
|
+
}
|
1026
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuDirective, decorators: [{
|
1027
|
+
type: Directive,
|
1028
|
+
args: [{
|
1029
|
+
selector: '[rdxNavigationMenu]',
|
1030
|
+
providers: [provideNavigationMenuContext(RdxNavigationMenuDirective)],
|
1031
|
+
host: {
|
1032
|
+
'[attr.data-orientation]': 'orientation',
|
1033
|
+
'[attr.dir]': 'dir',
|
1034
|
+
'aria-label': 'Main',
|
1035
|
+
role: 'navigation'
|
1036
|
+
},
|
1037
|
+
exportAs: 'rdxNavigationMenu'
|
1038
|
+
}]
|
1039
|
+
}], ctorParameters: () => [], propDecorators: { orientation: [{
|
1040
|
+
type: Input
|
1041
|
+
}], dir: [{
|
1042
|
+
type: Input
|
1043
|
+
}], delayDuration: [{
|
1044
|
+
type: Input,
|
1045
|
+
args: [{ transform: numberAttribute }]
|
1046
|
+
}], skipDelayDuration: [{
|
1047
|
+
type: Input,
|
1048
|
+
args: [{ transform: numberAttribute }]
|
1049
|
+
}], loop: [{
|
1050
|
+
type: Input,
|
1051
|
+
args: [{ transform: booleanAttribute }]
|
1052
|
+
}], cssAnimation: [{
|
1053
|
+
type: Input,
|
1054
|
+
args: [{ transform: booleanAttribute }]
|
1055
|
+
}], cssOpeningAnimation: [{
|
1056
|
+
type: Input,
|
1057
|
+
args: [{ transform: booleanAttribute }]
|
1058
|
+
}], cssClosingAnimation: [{
|
1059
|
+
type: Input,
|
1060
|
+
args: [{ transform: booleanAttribute }]
|
1061
|
+
}] } });
|
1062
|
+
|
1063
|
+
class RdxNavigationMenuSubDirective {
|
1064
|
+
constructor() {
|
1065
|
+
this.orientation = input('horizontal');
|
1066
|
+
this.valueChange = output();
|
1067
|
+
this.value = signal('');
|
1068
|
+
this.previousValue = signal('');
|
1069
|
+
this.baseId = `rdx-nav-menu-sub-${generateId()}`;
|
1070
|
+
this.isRootMenu = false;
|
1071
|
+
this.parent = inject(RdxNavigationMenuDirective, { optional: true });
|
1072
|
+
}
|
1073
|
+
set defaultValue(val) {
|
1074
|
+
if (val)
|
1075
|
+
this.value.set(val);
|
1076
|
+
}
|
1077
|
+
get dir() {
|
1078
|
+
if (!this.parent) {
|
1079
|
+
return 'ltr';
|
1080
|
+
}
|
1081
|
+
return this.parent.dir || 'ltr';
|
1082
|
+
}
|
1083
|
+
get rootNavigationMenu() {
|
1084
|
+
return this.parent?.rootNavigationMenu() || null;
|
1085
|
+
}
|
1086
|
+
onTriggerEnter(itemValue) {
|
1087
|
+
this.setValue(itemValue);
|
1088
|
+
}
|
1089
|
+
onItemSelect(itemValue) {
|
1090
|
+
this.setValue(itemValue);
|
1091
|
+
}
|
1092
|
+
onItemDismiss() {
|
1093
|
+
this.setValue('');
|
1094
|
+
}
|
1095
|
+
setValue(value) {
|
1096
|
+
this.previousValue.set(this.value());
|
1097
|
+
this.value.set(value);
|
1098
|
+
this.valueChange.emit(value);
|
1099
|
+
}
|
1100
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuSubDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
1101
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.4", type: RdxNavigationMenuSubDirective, isStandalone: true, selector: "[rdxNavigationMenuSub]", inputs: { orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { properties: { "attr.data-orientation": "orientation()" } }, providers: [provideNavigationMenuContext(RdxNavigationMenuSubDirective)], ngImport: i0 }); }
|
1102
|
+
}
|
1103
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuSubDirective, decorators: [{
|
1104
|
+
type: Directive,
|
1105
|
+
args: [{
|
1106
|
+
selector: '[rdxNavigationMenuSub]',
|
1107
|
+
providers: [provideNavigationMenuContext(RdxNavigationMenuSubDirective)],
|
1108
|
+
host: {
|
1109
|
+
'[attr.data-orientation]': 'orientation()'
|
1110
|
+
}
|
1111
|
+
}]
|
1112
|
+
}], propDecorators: { defaultValue: [{
|
1113
|
+
type: Input
|
1114
|
+
}] } });
|
1115
|
+
|
1116
|
+
class RdxNavigationMenuTriggerDirective extends RdxNavigationMenuFocusableOption {
|
1117
|
+
constructor() {
|
1118
|
+
super();
|
1119
|
+
this.context = injectNavigationMenu();
|
1120
|
+
this.item = inject(RdxNavigationMenuItemDirective);
|
1121
|
+
this.list = inject(RdxNavigationMenuListDirective);
|
1122
|
+
this.rovingFocusItem = inject(RdxRovingFocusItemDirective, { self: true });
|
1123
|
+
this.elementRef = inject(ElementRef);
|
1124
|
+
this.viewContainerRef = inject(ViewContainerRef);
|
1125
|
+
this.disabled = input(false, { transform: booleanAttribute });
|
1126
|
+
this.triggerId = makeTriggerId(this.context.baseId, this.item.value());
|
1127
|
+
this.contentId = makeContentId(this.context.baseId, this.item.value());
|
1128
|
+
this.open = computed(() => {
|
1129
|
+
return this.item.value() === this.context.value();
|
1130
|
+
});
|
1131
|
+
this.focusProxyRef = null;
|
1132
|
+
this.ariaOwnsRef = null;
|
1133
|
+
this.hasPointerMoveOpened = false;
|
1134
|
+
this.wasClickClose = false;
|
1135
|
+
effect(() => {
|
1136
|
+
this.rovingFocusItem.focusable = !this.disabled();
|
1137
|
+
});
|
1138
|
+
effect(() => {
|
1139
|
+
const isOpen = this.open();
|
1140
|
+
untracked(() => {
|
1141
|
+
// handle focus proxy and aria-owns when open state changes
|
1142
|
+
if (isOpen) {
|
1143
|
+
this.createAccessibilityComponents();
|
1144
|
+
}
|
1145
|
+
else {
|
1146
|
+
this.removeAccessibilityComponents();
|
1147
|
+
if (!this.item.wasEscapeCloseRef()) {
|
1148
|
+
this.item.onRootContentClose();
|
1149
|
+
}
|
1150
|
+
this.hasPointerMoveOpened = false;
|
1151
|
+
}
|
1152
|
+
});
|
1153
|
+
});
|
1154
|
+
}
|
1155
|
+
ngOnInit() {
|
1156
|
+
this.item.triggerRef.set(this.elementRef.nativeElement);
|
1157
|
+
// configure the static part of the roving focus item directive instance
|
1158
|
+
this.rovingFocusItem.tabStopId = this.item.value();
|
1159
|
+
}
|
1160
|
+
ngOnDestroy() {
|
1161
|
+
this.removeAccessibilityComponents();
|
1162
|
+
}
|
1163
|
+
focus() {
|
1164
|
+
this.elementRef.nativeElement.focus();
|
1165
|
+
}
|
1166
|
+
createAccessibilityComponents() {
|
1167
|
+
if (this.focusProxyRef || this.ariaOwnsRef) {
|
1168
|
+
return;
|
1169
|
+
}
|
1170
|
+
// create focus proxy component
|
1171
|
+
this.focusProxyRef = this.viewContainerRef.createComponent(RdxNavigationMenuFocusProxyComponent);
|
1172
|
+
this.focusProxyRef.instance.triggerElement = this.elementRef.nativeElement;
|
1173
|
+
this.focusProxyRef.instance.contentElement = this.item.contentRef();
|
1174
|
+
this.focusProxyRef.instance.proxyFocus.subscribe((direction) => {
|
1175
|
+
this.item.onFocusProxyEnter(direction);
|
1176
|
+
});
|
1177
|
+
// store reference in item directive
|
1178
|
+
this.item.focusProxyRef.set(this.focusProxyRef.location.nativeElement);
|
1179
|
+
// only add aria-owns component if using viewport
|
1180
|
+
if (isRootNavigationMenu(this.context) && this.context.viewport && this.context.viewport()) {
|
1181
|
+
this.ariaOwnsRef = this.viewContainerRef.createComponent(RdxNavigationMenuAriaOwnsComponent);
|
1182
|
+
this.ariaOwnsRef.instance.contentId = this.contentId;
|
1183
|
+
}
|
1184
|
+
}
|
1185
|
+
removeAccessibilityComponents() {
|
1186
|
+
if (this.focusProxyRef) {
|
1187
|
+
this.focusProxyRef.destroy();
|
1188
|
+
this.focusProxyRef = null;
|
1189
|
+
this.item.focusProxyRef.set(null);
|
1190
|
+
}
|
1191
|
+
if (this.ariaOwnsRef) {
|
1192
|
+
this.ariaOwnsRef.destroy();
|
1193
|
+
this.ariaOwnsRef = null;
|
1194
|
+
}
|
1195
|
+
}
|
1196
|
+
onPointerEnter() {
|
1197
|
+
// ignore if disabled or not the root menu (hover logic primarily for root)
|
1198
|
+
if (this.disabled() || !isRootNavigationMenu(this.context))
|
1199
|
+
return;
|
1200
|
+
this.wasClickClose = false; // Reset click close flag on enter
|
1201
|
+
this.item.wasEscapeCloseRef.set(false); // Reset escape flag
|
1202
|
+
this.context.setTriggerPointerState?.(true); // Update context state
|
1203
|
+
// if the menu isn't already open for this item, trigger the enter logic (handles delays)
|
1204
|
+
if (!this.open()) {
|
1205
|
+
this.context.onTriggerEnter?.(this.item.value());
|
1206
|
+
}
|
1207
|
+
}
|
1208
|
+
onPointerMove(event) {
|
1209
|
+
// ignore if not a mouse event, disabled, closed by click/escape, or already opened by this move
|
1210
|
+
if (event.pointerType !== 'mouse' ||
|
1211
|
+
this.disabled() ||
|
1212
|
+
this.wasClickClose ||
|
1213
|
+
this.item.wasEscapeCloseRef() ||
|
1214
|
+
this.hasPointerMoveOpened ||
|
1215
|
+
!isRootNavigationMenu(this.context)) {
|
1216
|
+
return;
|
1217
|
+
}
|
1218
|
+
// trigger enter logic (handles delays) and mark that this move initiated an open attempt
|
1219
|
+
this.context.onTriggerEnter?.(this.item.value());
|
1220
|
+
this.hasPointerMoveOpened = true;
|
1221
|
+
}
|
1222
|
+
onPointerLeave(event) {
|
1223
|
+
// ignore if not a mouse event or disabled
|
1224
|
+
if (event.pointerType !== 'mouse' || this.disabled() || !isRootNavigationMenu(this.context)) {
|
1225
|
+
return;
|
1226
|
+
}
|
1227
|
+
this.context.setTriggerPointerState?.(false); // Update context state
|
1228
|
+
this.context.onTriggerLeave?.(); // Trigger leave logic (handles delays)
|
1229
|
+
this.hasPointerMoveOpened = false; // Reset flag
|
1230
|
+
// reset user dismissal flag if pointer leaves the whole system (trigger + content)
|
1231
|
+
if (this.context.resetUserDismissed) {
|
1232
|
+
// relay slightly to allow pointer movement to content area without resetting dismissal state
|
1233
|
+
setTimeout(() => {
|
1234
|
+
if (!this.context.isPointerInSystem?.()) {
|
1235
|
+
this.context.resetUserDismissed?.();
|
1236
|
+
}
|
1237
|
+
}, 50); // small delay for tolerance
|
1238
|
+
}
|
1239
|
+
}
|
1240
|
+
onClick() {
|
1241
|
+
if (this.disabled())
|
1242
|
+
return;
|
1243
|
+
// manually set the `KeyManager` active item to this trigger
|
1244
|
+
this.list.setActiveItem(this.item);
|
1245
|
+
if (this.context.onItemSelect) {
|
1246
|
+
this.context.onItemSelect(this.item.value());
|
1247
|
+
// track if this click action resulted in closing the menu
|
1248
|
+
this.wasClickClose = !this.open();
|
1249
|
+
// reset escape flag if menu was opened by click
|
1250
|
+
if (this.open()) {
|
1251
|
+
this.item.wasEscapeCloseRef.set(false);
|
1252
|
+
}
|
1253
|
+
}
|
1254
|
+
}
|
1255
|
+
onKeydown(event) {
|
1256
|
+
if (this.disabled())
|
1257
|
+
return;
|
1258
|
+
if (event.key === ENTER || event.key === SPACE) {
|
1259
|
+
event.preventDefault(); // prevent default button behavior
|
1260
|
+
this.onClick();
|
1261
|
+
// if menu was opened by this keypress, move focus into the content
|
1262
|
+
if (this.open()) {
|
1263
|
+
// defer focus slightly to ensure content is ready
|
1264
|
+
setTimeout(() => this.item.onEntryKeyDown(), 0);
|
1265
|
+
}
|
1266
|
+
return;
|
1267
|
+
}
|
1268
|
+
const isHorizontal = this.context.orientation === 'horizontal';
|
1269
|
+
const isRTL = this.context.dir === 'rtl';
|
1270
|
+
// handle `ArrowDown` specifically for viewport navigation
|
1271
|
+
if (event.key === ARROW_DOWN || event.key === TAB) {
|
1272
|
+
if (event.key === ARROW_DOWN) {
|
1273
|
+
event.preventDefault();
|
1274
|
+
}
|
1275
|
+
// if the menu is open, focus into the content
|
1276
|
+
if (this.open()) {
|
1277
|
+
if (event.key === TAB) {
|
1278
|
+
// needed to ensure that the `keyManager` on the list directive does not activate
|
1279
|
+
// any focus updates, shifting focus to the subsequent focusable list item
|
1280
|
+
event.stopImmediatePropagation();
|
1281
|
+
}
|
1282
|
+
// direct focus handling for viewport case
|
1283
|
+
if (isRootNavigationMenu(this.context) && this.context.viewport && this.context.viewport()) {
|
1284
|
+
// get the viewport element
|
1285
|
+
const viewport = this.context.viewport();
|
1286
|
+
if (viewport) {
|
1287
|
+
// find all tabbable elements in the viewport
|
1288
|
+
const tabbables = getTabbableCandidates(viewport);
|
1289
|
+
if (tabbables.length > 0) {
|
1290
|
+
// focus the first tabbable element directly
|
1291
|
+
setTimeout(() => {
|
1292
|
+
tabbables[0].focus();
|
1293
|
+
}, 0);
|
1294
|
+
return;
|
1295
|
+
}
|
1296
|
+
}
|
1297
|
+
}
|
1298
|
+
// fallback to the standard entry key down approach
|
1299
|
+
setTimeout(() => this.item.onEntryKeyDown(), 0);
|
1300
|
+
return;
|
1301
|
+
}
|
1302
|
+
// if not open but in horizontal orientation, emulate right key navigation
|
1303
|
+
if (isHorizontal) {
|
1304
|
+
const nextEvent = new KeyboardEvent('keydown', {
|
1305
|
+
key: isRTL ? ARROW_LEFT : ARROW_RIGHT,
|
1306
|
+
bubbles: true
|
1307
|
+
});
|
1308
|
+
this.elementRef.nativeElement.dispatchEvent(nextEvent);
|
1309
|
+
return;
|
1310
|
+
}
|
1311
|
+
}
|
1312
|
+
// handle ArrowUp in horizontal orientation
|
1313
|
+
if (isHorizontal && event.key === ARROW_UP) {
|
1314
|
+
event.preventDefault();
|
1315
|
+
// emulate a left key press to move to the previous item
|
1316
|
+
const nextEvent = new KeyboardEvent('keydown', {
|
1317
|
+
key: isRTL ? ARROW_RIGHT : ARROW_LEFT,
|
1318
|
+
bubbles: true
|
1319
|
+
});
|
1320
|
+
this.elementRef.nativeElement.dispatchEvent(nextEvent);
|
1321
|
+
return;
|
1322
|
+
}
|
1323
|
+
// handle vertical navigation and entry into content
|
1324
|
+
const verticalEntryKey = isRTL ? ARROW_LEFT : ARROW_RIGHT;
|
1325
|
+
const entryKey = isHorizontal ? ARROW_DOWN : verticalEntryKey;
|
1326
|
+
if (this.item.contentRef() && event.key === entryKey && event.key !== ARROW_DOWN) {
|
1327
|
+
// Skip if it's ArrowDown as we already handled it above
|
1328
|
+
event.preventDefault();
|
1329
|
+
if (!this.open()) {
|
1330
|
+
// if closed, open the menu first
|
1331
|
+
this.context.onItemSelect?.(this.item.value());
|
1332
|
+
// defer focus movement into content until after state update and render
|
1333
|
+
setTimeout(() => this.item.onEntryKeyDown(), 0);
|
1334
|
+
}
|
1335
|
+
else {
|
1336
|
+
// if already open, just move focus into the content
|
1337
|
+
this.item.onEntryKeyDown();
|
1338
|
+
}
|
1339
|
+
return;
|
1340
|
+
}
|
1341
|
+
}
|
1342
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
1343
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.4", type: RdxNavigationMenuTriggerDirective, isStandalone: true, selector: "[rdxNavigationMenuTrigger]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button" }, listeners: { "pointerenter": "onPointerEnter()", "pointermove": "onPointerMove($event)", "pointerleave": "onPointerLeave($event)", "click": "onClick()", "keydown": "onKeydown($event)" }, properties: { "id": "triggerId", "attr.data-state": "open() ? \"open\" : \"closed\"", "attr.data-orientation": "context.orientation", "attr.data-disabled": "disabled() ? \"\" : undefined", "disabled": "disabled() ? true : null", "attr.aria-expanded": "open()", "attr.aria-controls": "contentId", "attr.aria-haspopup": "\"menu\"" } }, providers: [{ provide: RdxNavigationMenuFocusableOption, useExisting: RdxNavigationMenuTriggerDirective }], usesInheritance: true, hostDirectives: [{ directive: i1.RdxRovingFocusItemDirective }], ngImport: i0 }); }
|
1344
|
+
}
|
1345
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuTriggerDirective, decorators: [{
|
1346
|
+
type: Directive,
|
1347
|
+
args: [{
|
1348
|
+
selector: '[rdxNavigationMenuTrigger]',
|
1349
|
+
hostDirectives: [RdxRovingFocusItemDirective],
|
1350
|
+
host: {
|
1351
|
+
'[id]': 'triggerId',
|
1352
|
+
'[attr.data-state]': 'open() ? "open" : "closed"',
|
1353
|
+
'[attr.data-orientation]': 'context.orientation',
|
1354
|
+
'[attr.data-disabled]': 'disabled() ? "" : undefined',
|
1355
|
+
'[disabled]': 'disabled() ? true : null',
|
1356
|
+
'[attr.aria-expanded]': 'open()',
|
1357
|
+
'[attr.aria-controls]': 'contentId',
|
1358
|
+
'[attr.aria-haspopup]': '"menu"',
|
1359
|
+
'(pointerenter)': 'onPointerEnter()',
|
1360
|
+
'(pointermove)': 'onPointerMove($event)',
|
1361
|
+
'(pointerleave)': 'onPointerLeave($event)',
|
1362
|
+
'(click)': 'onClick()',
|
1363
|
+
'(keydown)': 'onKeydown($event)',
|
1364
|
+
type: 'button'
|
1365
|
+
},
|
1366
|
+
providers: [{ provide: RdxNavigationMenuFocusableOption, useExisting: RdxNavigationMenuTriggerDirective }]
|
1367
|
+
}]
|
1368
|
+
}], ctorParameters: () => [] });
|
1369
|
+
|
1370
|
+
class RdxNavigationMenuViewportDirective {
|
1371
|
+
constructor() {
|
1372
|
+
this.context = injectNavigationMenu();
|
1373
|
+
this.document = injectDocument();
|
1374
|
+
this.window = injectWindow();
|
1375
|
+
this.elementRef = inject(ElementRef);
|
1376
|
+
this.viewContainerRef = inject(ViewContainerRef);
|
1377
|
+
this.renderer = inject(Renderer2);
|
1378
|
+
this.zone = inject(NgZone);
|
1379
|
+
this.destroyRef = inject(DestroyRef);
|
1380
|
+
/**
|
1381
|
+
* Used to keep the viewport rendered and available in the DOM, even when closed.
|
1382
|
+
* Useful for animations.
|
1383
|
+
* @default false
|
1384
|
+
*/
|
1385
|
+
this.forceMount = input(false, { transform: booleanAttribute });
|
1386
|
+
this._contentNodes = signal(new Map());
|
1387
|
+
this._activeContentNode = signal(null);
|
1388
|
+
this._leavingContentNode = signal(null);
|
1389
|
+
this._viewportSize = signal(null);
|
1390
|
+
this.activeContentValue = computed(() => {
|
1391
|
+
if (!isRootNavigationMenu(this.context))
|
1392
|
+
return null;
|
1393
|
+
return this.context.value() || this.context.previousValue();
|
1394
|
+
});
|
1395
|
+
this.isOpen = computed(() => {
|
1396
|
+
if (!isRootNavigationMenu(this.context))
|
1397
|
+
return false;
|
1398
|
+
return Boolean(this.context.value() || this.forceMount());
|
1399
|
+
});
|
1400
|
+
this.dataState = computed(() => getOpenStateLabel(this.isOpen()));
|
1401
|
+
this.viewportSize = computed(() => this._viewportSize());
|
1402
|
+
this._resizeObserver = new ResizeObserver(() => this.updateSize());
|
1403
|
+
this.setupViewportEffect();
|
1404
|
+
}
|
1405
|
+
ngOnInit() {
|
1406
|
+
if (isRootNavigationMenu(this.context) && this.context.onViewportChange) {
|
1407
|
+
this.context.onViewportChange(this.elementRef.nativeElement);
|
1408
|
+
}
|
1409
|
+
}
|
1410
|
+
ngOnDestroy() {
|
1411
|
+
this._resizeObserver.disconnect();
|
1412
|
+
// clean up any remaining nodes/views/subscriptions
|
1413
|
+
this._contentNodes().forEach((node) => this.cleanupAfterLeave(node));
|
1414
|
+
if (isRootNavigationMenu(this.context) && this.context.onViewportChange) {
|
1415
|
+
this.context.onViewportChange(null);
|
1416
|
+
}
|
1417
|
+
}
|
1418
|
+
onKeydown(event) {
|
1419
|
+
if (!this.isOpen())
|
1420
|
+
return;
|
1421
|
+
const tabbableElements = getTabbableCandidates(this.elementRef.nativeElement);
|
1422
|
+
if (!tabbableElements.length)
|
1423
|
+
return;
|
1424
|
+
const activeElement = this.document.activeElement;
|
1425
|
+
const currentIndex = tabbableElements.findIndex((el) => el === activeElement);
|
1426
|
+
if (event.key === ARROW_DOWN) {
|
1427
|
+
event.preventDefault();
|
1428
|
+
const nextIndex = currentIndex >= 0 && currentIndex < tabbableElements.length - 1 ? currentIndex + 1 : 0;
|
1429
|
+
tabbableElements[nextIndex]?.focus();
|
1430
|
+
}
|
1431
|
+
else if (event.key === ARROW_UP) {
|
1432
|
+
event.preventDefault();
|
1433
|
+
const prevIndex = currentIndex > 0 ? currentIndex - 1 : tabbableElements.length - 1;
|
1434
|
+
tabbableElements[prevIndex]?.focus();
|
1435
|
+
}
|
1436
|
+
}
|
1437
|
+
onPointerEnter() {
|
1438
|
+
if (isRootNavigationMenu(this.context) && this.context.onContentEnter) {
|
1439
|
+
this.context.onContentEnter();
|
1440
|
+
}
|
1441
|
+
if (isRootNavigationMenu(this.context) && this.context.setContentPointerState) {
|
1442
|
+
this.context.setContentPointerState(true);
|
1443
|
+
}
|
1444
|
+
}
|
1445
|
+
onPointerLeave() {
|
1446
|
+
if (isRootNavigationMenu(this.context) && this.context.onContentLeave) {
|
1447
|
+
this.context.onContentLeave();
|
1448
|
+
}
|
1449
|
+
if (isRootNavigationMenu(this.context) && this.context.setContentPointerState) {
|
1450
|
+
this.context.setContentPointerState(false);
|
1451
|
+
}
|
1452
|
+
}
|
1453
|
+
setupViewportEffect() {
|
1454
|
+
effect(() => {
|
1455
|
+
const currentActiveValue = this.context.value();
|
1456
|
+
const previousActiveValue = this.context.previousValue();
|
1457
|
+
const forceMount = this.forceMount();
|
1458
|
+
untracked(() => {
|
1459
|
+
// ensure context is root before proceeding
|
1460
|
+
if (!isRootNavigationMenu(this.context) || !this.context.viewportContent) {
|
1461
|
+
return;
|
1462
|
+
}
|
1463
|
+
const allContentData = this.context.viewportContent();
|
1464
|
+
const currentNodesMap = this._contentNodes();
|
1465
|
+
let enteringNode = null;
|
1466
|
+
let leavingNode = this._leavingContentNode(); // get potentially already leaving node
|
1467
|
+
// 1. Identify Entering Node
|
1468
|
+
if (currentActiveValue && allContentData.has(currentActiveValue)) {
|
1469
|
+
enteringNode = this.getOrCreateContentNode(currentActiveValue);
|
1470
|
+
}
|
1471
|
+
// 2. Identify Leaving Node
|
1472
|
+
const nodeThatWasActive = previousActiveValue ? currentNodesMap.get(previousActiveValue) : null;
|
1473
|
+
// if there was a previously active node, it's different from the entering one,
|
1474
|
+
// and it's not already leaving, mark it for removal.
|
1475
|
+
if (nodeThatWasActive && nodeThatWasActive !== enteringNode && nodeThatWasActive !== leavingNode) {
|
1476
|
+
// if another node was already leaving, force complete its transition
|
1477
|
+
if (leavingNode) {
|
1478
|
+
this.forceCompleteLeaveTransition(leavingNode);
|
1479
|
+
}
|
1480
|
+
leavingNode = nodeThatWasActive;
|
1481
|
+
this._leavingContentNode.set(leavingNode);
|
1482
|
+
}
|
1483
|
+
// 3. Handle Entering Node
|
1484
|
+
if (enteringNode) {
|
1485
|
+
// cancel any pending leave transition for this node if it was leaving
|
1486
|
+
if (enteringNode === leavingNode) {
|
1487
|
+
this.cancelLeaveTransition(enteringNode);
|
1488
|
+
leavingNode = null;
|
1489
|
+
this._leavingContentNode.set(null);
|
1490
|
+
}
|
1491
|
+
// ensure it's in the DOM and set state to open
|
1492
|
+
this.addNodeToDOM(enteringNode);
|
1493
|
+
this.setNodeState(enteringNode, 'open'); // Triggers enter animation via data-state
|
1494
|
+
this._activeContentNode.set(enteringNode);
|
1495
|
+
this.updateSize(); // Update size based on the entering node
|
1496
|
+
}
|
1497
|
+
else {
|
1498
|
+
// no node entering, clear active node state
|
1499
|
+
this._activeContentNode.set(null);
|
1500
|
+
}
|
1501
|
+
// 4. Handle Leaving Node
|
1502
|
+
if (leavingNode) {
|
1503
|
+
if (forceMount) {
|
1504
|
+
// if forceMount, just mark as closed, don't trigger removal animation
|
1505
|
+
this.setNodeState(leavingNode, 'closed');
|
1506
|
+
this._leavingContentNode.set(null); // No longer considered "leaving"
|
1507
|
+
}
|
1508
|
+
else {
|
1509
|
+
// start the leave transition (usePresence handles DOM removal)
|
1510
|
+
this.startLeaveTransition(leavingNode);
|
1511
|
+
}
|
1512
|
+
}
|
1513
|
+
});
|
1514
|
+
});
|
1515
|
+
}
|
1516
|
+
// gets or creates the ContentNode (wrapper + view)
|
1517
|
+
getOrCreateContentNode(contentValue) {
|
1518
|
+
const existingNode = this._contentNodes().get(contentValue);
|
1519
|
+
if (existingNode && !existingNode.embeddedView.destroyed) {
|
1520
|
+
return existingNode;
|
1521
|
+
}
|
1522
|
+
// create if doesn't exist or view was destroyed
|
1523
|
+
if (!isRootNavigationMenu(this.context) || !this.context.viewportContent)
|
1524
|
+
return null;
|
1525
|
+
const allContentData = this.context.viewportContent();
|
1526
|
+
const contentData = allContentData.get(contentValue);
|
1527
|
+
const templateRef = contentData?.templateRef;
|
1528
|
+
if (!templateRef) {
|
1529
|
+
console.error(`No templateRef found for content value: ${contentValue}`);
|
1530
|
+
return null;
|
1531
|
+
}
|
1532
|
+
try {
|
1533
|
+
const embeddedView = this.viewContainerRef.createEmbeddedView(templateRef);
|
1534
|
+
const container = this.renderer.createElement('div');
|
1535
|
+
this.renderer.setAttribute(container, 'class', 'NavigationMenuContentWrapper');
|
1536
|
+
this.renderer.setAttribute(container, 'data-content-value', contentValue);
|
1537
|
+
embeddedView.rootNodes.forEach((node) => this.renderer.appendChild(container, node));
|
1538
|
+
const newNode = {
|
1539
|
+
embeddedView,
|
1540
|
+
element: container,
|
1541
|
+
contentValue,
|
1542
|
+
state: 'closed'
|
1543
|
+
};
|
1544
|
+
const newMap = new Map(this._contentNodes());
|
1545
|
+
newMap.set(contentValue, newNode);
|
1546
|
+
this._contentNodes.set(newMap);
|
1547
|
+
return newNode;
|
1548
|
+
}
|
1549
|
+
catch (error) {
|
1550
|
+
console.error(`Error creating content node for ${contentValue}:`, error);
|
1551
|
+
return null;
|
1552
|
+
}
|
1553
|
+
}
|
1554
|
+
// adds node element to viewport DOM if not already present
|
1555
|
+
addNodeToDOM(node) {
|
1556
|
+
if (!this.elementRef.nativeElement.contains(node.element)) {
|
1557
|
+
this.renderer.appendChild(this.elementRef.nativeElement, node.element);
|
1558
|
+
// observe size only when added to DOM
|
1559
|
+
this._resizeObserver.observe(node.element);
|
1560
|
+
}
|
1561
|
+
}
|
1562
|
+
// removes node element from viewport DOM
|
1563
|
+
removeNodeFromDOM(node) {
|
1564
|
+
if (this.elementRef.nativeElement.contains(node.element)) {
|
1565
|
+
this._resizeObserver.unobserve(node.element); // stop observing before removal
|
1566
|
+
this.renderer.removeChild(this.elementRef.nativeElement, node.element);
|
1567
|
+
}
|
1568
|
+
}
|
1569
|
+
// updates the data-state and motion attributes
|
1570
|
+
setNodeState(node, state) {
|
1571
|
+
if (node.state === state)
|
1572
|
+
return; // avoid redundant updates
|
1573
|
+
node.state = state;
|
1574
|
+
this.renderer.setAttribute(node.element, 'data-state', state);
|
1575
|
+
// apply motion attribute based on context
|
1576
|
+
if (isRootNavigationMenu(this.context) && this.context.viewportContent) {
|
1577
|
+
const contentData = this.context.viewportContent().get(node.contentValue);
|
1578
|
+
if (contentData?.getMotionAttribute) {
|
1579
|
+
// get motion based on current state transition
|
1580
|
+
const motionAttr = contentData.getMotionAttribute();
|
1581
|
+
if (motionAttr) {
|
1582
|
+
this.renderer.setAttribute(node.element, 'data-motion', motionAttr);
|
1583
|
+
}
|
1584
|
+
else {
|
1585
|
+
this.renderer.removeAttribute(node.element, 'data-motion');
|
1586
|
+
}
|
1587
|
+
}
|
1588
|
+
else {
|
1589
|
+
this.renderer.removeAttribute(node.element, 'data-motion');
|
1590
|
+
}
|
1591
|
+
}
|
1592
|
+
// apply A11y attributes (might only be needed on open?)
|
1593
|
+
if (state === 'open') {
|
1594
|
+
this.applyA11yAttributes(node);
|
1595
|
+
}
|
1596
|
+
}
|
1597
|
+
// apply A11y attributes to the first child element
|
1598
|
+
applyA11yAttributes(node) {
|
1599
|
+
if (!isRootNavigationMenu(this.context) || !this.context.viewportContent)
|
1600
|
+
return;
|
1601
|
+
const contentData = this.context.viewportContent().get(node.contentValue);
|
1602
|
+
if (contentData?.additionalAttrs && node.embeddedView.rootNodes.length > 0) {
|
1603
|
+
const firstRootNode = node.embeddedView.rootNodes[0];
|
1604
|
+
if (firstRootNode) {
|
1605
|
+
Object.entries(contentData.additionalAttrs).forEach(([attr, value]) => {
|
1606
|
+
this.renderer.setAttribute(firstRootNode, attr, value);
|
1607
|
+
});
|
1608
|
+
}
|
1609
|
+
}
|
1610
|
+
}
|
1611
|
+
startLeaveTransition(node) {
|
1612
|
+
// ensure node exists and isn't already leaving with an active subscription
|
1613
|
+
if (!node || node.transitionSubscription) {
|
1614
|
+
node.transitionSubscription?.unsubscribe();
|
1615
|
+
return;
|
1616
|
+
}
|
1617
|
+
const startFn = () => {
|
1618
|
+
this.setNodeState(node, 'closed');
|
1619
|
+
return () => this.cleanupAfterLeave(node);
|
1620
|
+
};
|
1621
|
+
const options = {
|
1622
|
+
animation: true, // assuming CSS animations/transitions handle the exit
|
1623
|
+
state: 'continue' // start the leave process
|
1624
|
+
};
|
1625
|
+
node.transitionSubscription = usePresence(this.zone, node.element, startFn, options)
|
1626
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
1627
|
+
.subscribe({
|
1628
|
+
complete: () => {
|
1629
|
+
this.cleanupAfterLeave(node);
|
1630
|
+
}
|
1631
|
+
});
|
1632
|
+
}
|
1633
|
+
/**
|
1634
|
+
* Cleanup function called after leave animation finishes
|
1635
|
+
* @param node The node that is leaving
|
1636
|
+
*/
|
1637
|
+
cleanupAfterLeave(node) {
|
1638
|
+
// check if this node is still marked as the one leaving
|
1639
|
+
if (this._leavingContentNode() === node) {
|
1640
|
+
this.removeNodeFromDOM(node);
|
1641
|
+
if (!this.forceMount() && node.embeddedView && !node.embeddedView.destroyed) {
|
1642
|
+
node.embeddedView.destroy();
|
1643
|
+
// Remove from cache if destroyed
|
1644
|
+
const newMap = new Map(this._contentNodes());
|
1645
|
+
newMap.delete(node.contentValue);
|
1646
|
+
this._contentNodes.set(newMap);
|
1647
|
+
}
|
1648
|
+
node.transitionSubscription = null;
|
1649
|
+
this._leavingContentNode.set(null);
|
1650
|
+
}
|
1651
|
+
else {
|
1652
|
+
// if this node is NOT the one currently marked as leaving, it means
|
1653
|
+
// a new transition started before this one finished. Just clean up DOM/Sub.
|
1654
|
+
this.removeNodeFromDOM(node);
|
1655
|
+
node.transitionSubscription?.unsubscribe();
|
1656
|
+
node.transitionSubscription = null;
|
1657
|
+
}
|
1658
|
+
}
|
1659
|
+
/**
|
1660
|
+
* Cancels an ongoing leave transition (e.g., if user hovers back)
|
1661
|
+
* @param node The node that is leaving
|
1662
|
+
*/
|
1663
|
+
cancelLeaveTransition(node) {
|
1664
|
+
node.transitionSubscription?.unsubscribe();
|
1665
|
+
node.transitionSubscription = null;
|
1666
|
+
}
|
1667
|
+
/**
|
1668
|
+
* Force completes a leave transition (e.g., if another leave starts)
|
1669
|
+
* @param node The node that is leaving
|
1670
|
+
*/
|
1671
|
+
forceCompleteLeaveTransition(node) {
|
1672
|
+
if (node && node.transitionSubscription) {
|
1673
|
+
node.transitionSubscription.unsubscribe();
|
1674
|
+
// perform cleanup immediately
|
1675
|
+
this.cleanupAfterLeave(node);
|
1676
|
+
}
|
1677
|
+
}
|
1678
|
+
updateSize() {
|
1679
|
+
const activeNode = this._activeContentNode()?.element; // measure the currently active node
|
1680
|
+
if (!activeNode || !activeNode.isConnected)
|
1681
|
+
return;
|
1682
|
+
const firstChild = activeNode.firstChild;
|
1683
|
+
if (!firstChild)
|
1684
|
+
return;
|
1685
|
+
this.window.requestAnimationFrame(() => {
|
1686
|
+
// keep rAF here for measurement stability
|
1687
|
+
activeNode.getBoundingClientRect(); // force layout
|
1688
|
+
const width = Math.ceil(firstChild.offsetWidth);
|
1689
|
+
const height = Math.ceil(firstChild.offsetHeight);
|
1690
|
+
if (width !== 0 || height !== 0) {
|
1691
|
+
const currentSize = this._viewportSize();
|
1692
|
+
if (!currentSize || currentSize.width !== width || currentSize.height !== height) {
|
1693
|
+
this._viewportSize.set({ width, height });
|
1694
|
+
}
|
1695
|
+
}
|
1696
|
+
else if (this._viewportSize() !== null) {
|
1697
|
+
this._viewportSize.set(null);
|
1698
|
+
}
|
1699
|
+
});
|
1700
|
+
}
|
1701
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuViewportDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
1702
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.4", type: RdxNavigationMenuViewportDirective, isStandalone: true, selector: "[rdxNavigationMenuViewport]", inputs: { forceMount: { classPropertyName: "forceMount", publicName: "forceMount", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keydown": "onKeydown($event)", "pointerenter": "onPointerEnter()", "pointerleave": "onPointerLeave()" }, properties: { "attr.data-state": "dataState()", "attr.data-orientation": "context.orientation", "style.--radix-navigation-menu-viewport-width.px": "viewportSize()?.width", "style.--radix-navigation-menu-viewport-height.px": "viewportSize()?.height" } }, ngImport: i0 }); }
|
1703
|
+
}
|
1704
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuViewportDirective, decorators: [{
|
1705
|
+
type: Directive,
|
1706
|
+
args: [{
|
1707
|
+
selector: '[rdxNavigationMenuViewport]',
|
1708
|
+
host: {
|
1709
|
+
'[attr.data-state]': 'dataState()',
|
1710
|
+
'[attr.data-orientation]': 'context.orientation',
|
1711
|
+
'[style.--radix-navigation-menu-viewport-width.px]': 'viewportSize()?.width',
|
1712
|
+
'[style.--radix-navigation-menu-viewport-height.px]': 'viewportSize()?.height',
|
1713
|
+
'(keydown)': 'onKeydown($event)',
|
1714
|
+
'(pointerenter)': 'onPointerEnter()',
|
1715
|
+
'(pointerleave)': 'onPointerLeave()'
|
1716
|
+
}
|
1717
|
+
}]
|
1718
|
+
}], ctorParameters: () => [] });
|
1719
|
+
|
1720
|
+
const _imports = [
|
1721
|
+
RdxNavigationMenuDirective,
|
1722
|
+
RdxNavigationMenuSubDirective,
|
1723
|
+
RdxNavigationMenuListDirective,
|
1724
|
+
RdxNavigationMenuItemDirective,
|
1725
|
+
RdxNavigationMenuTriggerDirective,
|
1726
|
+
RdxNavigationMenuLinkDirective,
|
1727
|
+
RdxNavigationMenuIndicatorDirective,
|
1728
|
+
RdxNavigationMenuContentDirective,
|
1729
|
+
RdxNavigationMenuViewportDirective,
|
1730
|
+
RdxNavigationMenuFocusProxyComponent,
|
1731
|
+
RdxNavigationMenuAriaOwnsComponent
|
1732
|
+
];
|
1733
|
+
class RdxNavigationMenuModule {
|
1734
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
1735
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuModule, imports: [RdxNavigationMenuDirective,
|
1736
|
+
RdxNavigationMenuSubDirective,
|
1737
|
+
RdxNavigationMenuListDirective,
|
1738
|
+
RdxNavigationMenuItemDirective,
|
1739
|
+
RdxNavigationMenuTriggerDirective,
|
1740
|
+
RdxNavigationMenuLinkDirective,
|
1741
|
+
RdxNavigationMenuIndicatorDirective,
|
1742
|
+
RdxNavigationMenuContentDirective,
|
1743
|
+
RdxNavigationMenuViewportDirective,
|
1744
|
+
RdxNavigationMenuFocusProxyComponent,
|
1745
|
+
RdxNavigationMenuAriaOwnsComponent], exports: [RdxNavigationMenuDirective,
|
1746
|
+
RdxNavigationMenuSubDirective,
|
1747
|
+
RdxNavigationMenuListDirective,
|
1748
|
+
RdxNavigationMenuItemDirective,
|
1749
|
+
RdxNavigationMenuTriggerDirective,
|
1750
|
+
RdxNavigationMenuLinkDirective,
|
1751
|
+
RdxNavigationMenuIndicatorDirective,
|
1752
|
+
RdxNavigationMenuContentDirective,
|
1753
|
+
RdxNavigationMenuViewportDirective,
|
1754
|
+
RdxNavigationMenuFocusProxyComponent,
|
1755
|
+
RdxNavigationMenuAriaOwnsComponent] }); }
|
1756
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuModule }); }
|
1757
|
+
}
|
1758
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuModule, decorators: [{
|
1759
|
+
type: NgModule,
|
1760
|
+
args: [{
|
1761
|
+
imports: [..._imports],
|
1762
|
+
exports: [..._imports]
|
1763
|
+
}]
|
1764
|
+
}] });
|
1765
|
+
|
1766
|
+
/**
|
1767
|
+
* Generated bundle index. Do not edit.
|
1768
|
+
*/
|
1769
|
+
|
1770
|
+
export { RDX_NAVIGATION_MENU_TOKEN, RdxNavigationMenuAction, RdxNavigationMenuAnimationStatus, RdxNavigationMenuAriaOwnsComponent, RdxNavigationMenuContentDirective, RdxNavigationMenuDirective, RdxNavigationMenuFocusProxyComponent, RdxNavigationMenuFocusableOption, RdxNavigationMenuIndicatorDirective, RdxNavigationMenuItemDirective, RdxNavigationMenuLinkDirective, RdxNavigationMenuListDirective, RdxNavigationMenuModule, RdxNavigationMenuSubDirective, RdxNavigationMenuTriggerDirective, RdxNavigationMenuViewportDirective, injectNavigationMenu, isRootNavigationMenu, provideNavigationMenuContext };
|
1771
|
+
//# sourceMappingURL=radix-ng-primitives-navigation-menu.mjs.map
|