@radix-ng/primitives 0.33.0 → 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/fesm2022/radix-ng-primitives-hover-card.mjs +0 -1
- package/fesm2022/radix-ng-primitives-hover-card.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs +296 -171
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
- package/navigation-menu/src/navigation-menu-viewport.directive.d.ts +30 -7
- package/navigation-menu/src/utils.d.ts +1 -1
- package/package.json +1 -1
- package/popover/src/popover-root.directive.d.ts +4 -4
- package/tooltip/src/tooltip-root.directive.d.ts +4 -4
@@ -1,5 +1,5 @@
|
|
1
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, NgModule } 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
3
|
import { RdxVisuallyHiddenDirective } from '@radix-ng/primitives/visually-hidden';
|
4
4
|
import { injectDocument, ESCAPE, ENTER, SPACE, TAB, injectWindow, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ARROW_UP } from '@radix-ng/primitives/core';
|
5
5
|
import * as i1 from '@radix-ng/primitives/roving-focus';
|
@@ -7,6 +7,7 @@ import { RdxRovingFocusItemDirective, RdxRovingFocusGroupDirective } from '@radi
|
|
7
7
|
import { FocusKeyManager } from '@angular/cdk/a11y';
|
8
8
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
9
9
|
import { Subject, map, debounce, timer, tap } from 'rxjs';
|
10
|
+
import { usePresence } from '@radix-ng/primitives/presence';
|
10
11
|
|
11
12
|
class RdxNavigationMenuFocusProxyComponent {
|
12
13
|
constructor() {
|
@@ -138,26 +139,38 @@ function makeContentId(baseId, value) {
|
|
138
139
|
function getMotionAttribute(currentValue, previousValue, itemValue, itemValues, dir) {
|
139
140
|
// reverse values in RTL
|
140
141
|
const values = dir === 'rtl' ? [...itemValues].reverse() : itemValues;
|
141
|
-
const currentIndex = values.indexOf(currentValue);
|
142
|
-
const prevIndex = values.indexOf(previousValue);
|
142
|
+
const currentIndex = currentValue !== null ? values.indexOf(currentValue) : -1;
|
143
|
+
const prevIndex = previousValue !== null ? values.indexOf(previousValue) : -1;
|
143
144
|
const isSelected = itemValue === currentValue;
|
144
|
-
const wasSelected =
|
145
|
-
//
|
146
|
-
|
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) {
|
147
151
|
return null;
|
148
|
-
|
152
|
+
}
|
153
|
+
// handle transitions between items
|
149
154
|
if (currentIndex !== -1 && prevIndex !== -1) {
|
150
|
-
// if moving to this item
|
151
|
-
if (isSelected
|
155
|
+
// if moving to this item (isSelected)
|
156
|
+
if (isSelected) {
|
152
157
|
return currentIndex > prevIndex ? 'from-end' : 'from-start';
|
153
158
|
}
|
154
|
-
// if
|
155
|
-
if (wasSelected
|
159
|
+
// if moving away from this item (wasSelected)
|
160
|
+
if (wasSelected) {
|
156
161
|
return currentIndex > prevIndex ? 'to-start' : 'to-end';
|
157
162
|
}
|
158
163
|
}
|
159
|
-
//
|
160
|
-
|
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;
|
161
174
|
}
|
162
175
|
/**
|
163
176
|
* Focus the first element in a list of candidates
|
@@ -1355,47 +1368,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImpor
|
|
1355
1368
|
}], ctorParameters: () => [] });
|
1356
1369
|
|
1357
1370
|
class RdxNavigationMenuViewportDirective {
|
1358
|
-
get open() {
|
1359
|
-
return Boolean(this.context.value() || this.forceMount());
|
1360
|
-
}
|
1361
|
-
onKeydown(event) {
|
1362
|
-
// only handle if viewport is open
|
1363
|
-
if (!this.open)
|
1364
|
-
return;
|
1365
|
-
// get all tabbable elements in the viewport
|
1366
|
-
const tabbableElements = getTabbableCandidates(this.elementRef.nativeElement);
|
1367
|
-
if (!tabbableElements.length)
|
1368
|
-
return;
|
1369
|
-
// find the currently focused element
|
1370
|
-
const activeElement = this.document.activeElement;
|
1371
|
-
const currentIndex = tabbableElements.findIndex((el) => el === activeElement);
|
1372
|
-
if (event.key === ARROW_DOWN) {
|
1373
|
-
event.preventDefault();
|
1374
|
-
if (currentIndex >= 0 && currentIndex < tabbableElements.length - 1) {
|
1375
|
-
// focus the next element
|
1376
|
-
tabbableElements[currentIndex + 1].focus();
|
1377
|
-
}
|
1378
|
-
else if (currentIndex === -1 || currentIndex === tabbableElements.length - 1) {
|
1379
|
-
// if no element is focused or we're at the end, focus the first element
|
1380
|
-
tabbableElements[0].focus();
|
1381
|
-
}
|
1382
|
-
}
|
1383
|
-
else if (event.key === ARROW_UP) {
|
1384
|
-
event.preventDefault();
|
1385
|
-
if (currentIndex > 0) {
|
1386
|
-
// focus the previous element
|
1387
|
-
tabbableElements[currentIndex - 1].focus();
|
1388
|
-
}
|
1389
|
-
else if (currentIndex === 0) {
|
1390
|
-
// if at the first element, loop to the last element
|
1391
|
-
tabbableElements[tabbableElements.length - 1].focus();
|
1392
|
-
}
|
1393
|
-
else if (currentIndex === -1) {
|
1394
|
-
// if no element is focused, focus the last element
|
1395
|
-
tabbableElements[tabbableElements.length - 1].focus();
|
1396
|
-
}
|
1397
|
-
}
|
1398
|
-
}
|
1399
1371
|
constructor() {
|
1400
1372
|
this.context = injectNavigationMenu();
|
1401
1373
|
this.document = injectDocument();
|
@@ -1403,6 +1375,8 @@ class RdxNavigationMenuViewportDirective {
|
|
1403
1375
|
this.elementRef = inject(ElementRef);
|
1404
1376
|
this.viewContainerRef = inject(ViewContainerRef);
|
1405
1377
|
this.renderer = inject(Renderer2);
|
1378
|
+
this.zone = inject(NgZone);
|
1379
|
+
this.destroyRef = inject(DestroyRef);
|
1406
1380
|
/**
|
1407
1381
|
* Used to keep the viewport rendered and available in the DOM, even when closed.
|
1408
1382
|
* Useful for animations.
|
@@ -1411,61 +1385,59 @@ class RdxNavigationMenuViewportDirective {
|
|
1411
1385
|
this.forceMount = input(false, { transform: booleanAttribute });
|
1412
1386
|
this._contentNodes = signal(new Map());
|
1413
1387
|
this._activeContentNode = signal(null);
|
1388
|
+
this._leavingContentNode = signal(null);
|
1414
1389
|
this._viewportSize = signal(null);
|
1415
|
-
this._resizeObserver = new ResizeObserver(() => this.updateSize());
|
1416
|
-
// compute the active content value - either current value if open, or previous value if closing
|
1417
1390
|
this.activeContentValue = computed(() => {
|
1418
|
-
|
1391
|
+
if (!isRootNavigationMenu(this.context))
|
1392
|
+
return null;
|
1393
|
+
return this.context.value() || this.context.previousValue();
|
1419
1394
|
});
|
1420
|
-
|
1421
|
-
|
1422
|
-
|
1423
|
-
|
1424
|
-
const activeValue = this.activeContentValue();
|
1425
|
-
const open = this.open;
|
1426
|
-
untracked(() => {
|
1427
|
-
// handle visibility based on open state
|
1428
|
-
this.renderer.setStyle(this.elementRef.nativeElement, 'display', open ? 'block' : 'none');
|
1429
|
-
if (isRootNavigationMenu(this.context) && this.context.viewportContent) {
|
1430
|
-
const viewportContent = this.context.viewportContent();
|
1431
|
-
if (viewportContent.has(activeValue)) {
|
1432
|
-
const contentData = viewportContent.get(activeValue);
|
1433
|
-
// only render content when we have a templateRef
|
1434
|
-
if (contentData?.templateRef) {
|
1435
|
-
this.renderContent(contentData.templateRef, activeValue);
|
1436
|
-
}
|
1437
|
-
}
|
1438
|
-
}
|
1439
|
-
});
|
1395
|
+
this.isOpen = computed(() => {
|
1396
|
+
if (!isRootNavigationMenu(this.context))
|
1397
|
+
return false;
|
1398
|
+
return Boolean(this.context.value() || this.forceMount());
|
1440
1399
|
});
|
1400
|
+
this.dataState = computed(() => getOpenStateLabel(this.isOpen()));
|
1401
|
+
this.viewportSize = computed(() => this._viewportSize());
|
1402
|
+
this._resizeObserver = new ResizeObserver(() => this.updateSize());
|
1403
|
+
this.setupViewportEffect();
|
1441
1404
|
}
|
1442
1405
|
ngOnInit() {
|
1443
|
-
// register viewport with context
|
1444
1406
|
if (isRootNavigationMenu(this.context) && this.context.onViewportChange) {
|
1445
1407
|
this.context.onViewportChange(this.elementRef.nativeElement);
|
1446
1408
|
}
|
1447
1409
|
}
|
1448
1410
|
ngOnDestroy() {
|
1449
1411
|
this._resizeObserver.disconnect();
|
1450
|
-
//
|
1451
|
-
this._contentNodes().forEach((node) =>
|
1452
|
-
if (node.embeddedView) {
|
1453
|
-
node.embeddedView.destroy();
|
1454
|
-
}
|
1455
|
-
});
|
1456
|
-
// unregister viewport
|
1412
|
+
// clean up any remaining nodes/views/subscriptions
|
1413
|
+
this._contentNodes().forEach((node) => this.cleanupAfterLeave(node));
|
1457
1414
|
if (isRootNavigationMenu(this.context) && this.context.onViewportChange) {
|
1458
1415
|
this.context.onViewportChange(null);
|
1459
1416
|
}
|
1460
1417
|
}
|
1461
|
-
|
1462
|
-
|
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
|
+
}
|
1463
1436
|
}
|
1464
1437
|
onPointerEnter() {
|
1465
1438
|
if (isRootNavigationMenu(this.context) && this.context.onContentEnter) {
|
1466
1439
|
this.context.onContentEnter();
|
1467
1440
|
}
|
1468
|
-
// update pointer tracking state
|
1469
1441
|
if (isRootNavigationMenu(this.context) && this.context.setContentPointerState) {
|
1470
1442
|
this.context.setContentPointerState(true);
|
1471
1443
|
}
|
@@ -1474,114 +1446,267 @@ class RdxNavigationMenuViewportDirective {
|
|
1474
1446
|
if (isRootNavigationMenu(this.context) && this.context.onContentLeave) {
|
1475
1447
|
this.context.onContentLeave();
|
1476
1448
|
}
|
1477
|
-
// Update pointer tracking state
|
1478
1449
|
if (isRootNavigationMenu(this.context) && this.context.setContentPointerState) {
|
1479
1450
|
this.context.setContentPointerState(false);
|
1480
1451
|
}
|
1481
1452
|
}
|
1482
|
-
|
1483
|
-
|
1484
|
-
|
1485
|
-
|
1486
|
-
|
1487
|
-
|
1488
|
-
|
1489
|
-
|
1490
|
-
const height = Math.ceil(firstChild.offsetHeight);
|
1491
|
-
// update size with valid dimensions (but only if not zero)
|
1492
|
-
if (width !== 0 && height !== 0) {
|
1493
|
-
this._viewportSize.set({ width, height });
|
1494
|
-
}
|
1495
|
-
}
|
1496
|
-
renderContent(templateRef, contentValue) {
|
1497
|
-
// check if we already have a view for this content
|
1498
|
-
let contentNode = this._contentNodes().get(contentValue);
|
1499
|
-
if (!contentNode) {
|
1500
|
-
try {
|
1501
|
-
// create a new embedded view
|
1502
|
-
const embeddedView = this.viewContainerRef.createEmbeddedView(templateRef);
|
1503
|
-
embeddedView.detectChanges();
|
1504
|
-
// create a container for the view
|
1505
|
-
const container = this.renderer.createElement('div');
|
1506
|
-
this.renderer.setAttribute(container, 'class', 'NavigationMenuContentWrapper');
|
1507
|
-
this.renderer.setAttribute(container, 'data-content-value', contentValue);
|
1508
|
-
this.renderer.setStyle(container, 'width', '100%');
|
1509
|
-
const viewportContent = this.context.viewportContent && this.context.viewportContent();
|
1510
|
-
if (!viewportContent)
|
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) {
|
1511
1461
|
return;
|
1512
|
-
|
1513
|
-
|
1514
|
-
|
1515
|
-
|
1516
|
-
|
1517
|
-
|
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);
|
1518
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);
|
1519
1500
|
}
|
1520
|
-
//
|
1521
|
-
if (
|
1522
|
-
|
1523
|
-
|
1524
|
-
|
1525
|
-
|
1526
|
-
|
1527
|
-
|
1528
|
-
|
1529
|
-
|
1530
|
-
});
|
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);
|
1531
1511
|
}
|
1532
1512
|
}
|
1533
|
-
|
1534
|
-
|
1535
|
-
|
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);
|
1536
1607
|
});
|
1537
|
-
|
1538
|
-
|
1539
|
-
|
1540
|
-
|
1541
|
-
|
1542
|
-
|
1543
|
-
|
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
|
1544
1644
|
const newMap = new Map(this._contentNodes());
|
1545
|
-
newMap.
|
1645
|
+
newMap.delete(node.contentValue);
|
1546
1646
|
this._contentNodes.set(newMap);
|
1547
1647
|
}
|
1548
|
-
|
1549
|
-
|
1550
|
-
return;
|
1551
|
-
}
|
1648
|
+
node.transitionSubscription = null;
|
1649
|
+
this._leavingContentNode.set(null);
|
1552
1650
|
}
|
1553
|
-
|
1554
|
-
this
|
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;
|
1555
1657
|
}
|
1556
1658
|
}
|
1557
|
-
|
1558
|
-
|
1559
|
-
|
1560
|
-
|
1561
|
-
|
1562
|
-
|
1563
|
-
|
1564
|
-
|
1565
|
-
|
1566
|
-
|
1567
|
-
|
1568
|
-
|
1569
|
-
|
1570
|
-
|
1571
|
-
|
1572
|
-
//
|
1573
|
-
|
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);
|
1574
1676
|
}
|
1575
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
|
+
}
|
1576
1701
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuViewportDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
1577
|
-
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": "
|
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 }); }
|
1578
1703
|
}
|
1579
1704
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuViewportDirective, decorators: [{
|
1580
1705
|
type: Directive,
|
1581
1706
|
args: [{
|
1582
1707
|
selector: '[rdxNavigationMenuViewport]',
|
1583
1708
|
host: {
|
1584
|
-
'[attr.data-state]': '
|
1709
|
+
'[attr.data-state]': 'dataState()',
|
1585
1710
|
'[attr.data-orientation]': 'context.orientation',
|
1586
1711
|
'[style.--radix-navigation-menu-viewport-width.px]': 'viewportSize()?.width',
|
1587
1712
|
'[style.--radix-navigation-menu-viewport-height.px]': 'viewportSize()?.height',
|