@radix-ng/primitives 1.0.0-beta.0 → 1.0.0-beta.2
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-accordion.mjs +2 -2
- package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-calendar.mjs +109 -84
- package/fesm2022/radix-ng-primitives-calendar.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-checkbox.mjs +2 -2
- package/fesm2022/radix-ng-primitives-checkbox.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-collapsible.mjs +1 -1
- package/fesm2022/radix-ng-primitives-collapsible.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-combobox.mjs +1923 -0
- package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -0
- package/fesm2022/radix-ng-primitives-context-menu.mjs +1 -1
- package/fesm2022/radix-ng-primitives-context-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-core.mjs +591 -470
- package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-cropper.mjs +287 -308
- package/fesm2022/radix-ng-primitives-cropper.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-date-field.mjs +66 -15
- package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-dialog.mjs +1 -1
- package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-drawer.mjs +7 -106
- package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-editable.mjs +305 -24
- package/fesm2022/radix-ng-primitives-editable.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-field.mjs +86 -6
- package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-fieldset.mjs +1 -1
- package/fesm2022/radix-ng-primitives-fieldset.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-focus-scope.mjs +1 -1
- package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-form.mjs +207 -0
- package/fesm2022/radix-ng-primitives-form.mjs.map +1 -0
- package/fesm2022/radix-ng-primitives-input.mjs +85 -4
- package/fesm2022/radix-ng-primitives-input.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-menu.mjs +413 -5
- package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-menubar.mjs +1 -1
- package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-meter.mjs +1 -1
- package/fesm2022/radix-ng-primitives-meter.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs +1 -1
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-number-field.mjs +2 -2
- package/fesm2022/radix-ng-primitives-number-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-popover.mjs +1 -1
- package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-popper.mjs +22 -5
- package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-preview-card.mjs +1 -1
- package/fesm2022/radix-ng-primitives-preview-card.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-progress.mjs +1 -1
- package/fesm2022/radix-ng-primitives-progress.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-roving-focus.mjs +1 -1
- package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-scroll-area.mjs +923 -0
- package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -0
- package/fesm2022/radix-ng-primitives-select.mjs +421 -224
- package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-slider.mjs +1 -1
- package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-switch.mjs +3 -2
- package/fesm2022/radix-ng-primitives-switch.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-tabs.mjs +12 -3
- package/fesm2022/radix-ng-primitives-tabs.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-time-field.mjs +27 -3
- package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-toast.mjs +839 -0
- package/fesm2022/radix-ng-primitives-toast.mjs.map +1 -0
- package/fesm2022/radix-ng-primitives-toggle-group.mjs +1 -1
- package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-toolbar.mjs +2 -2
- package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-tooltip.mjs +11 -3
- package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
- package/package.json +18 -2
- package/schematics/ng-add/index.js +57 -0
- package/schematics/ng-add/index.js.map +1 -1
- package/schematics/ng-add/schema.d.ts +1 -0
- package/schematics/ng-add/schema.json +6 -0
- package/types/radix-ng-primitives-accordion.d.ts +3 -2
- package/types/radix-ng-primitives-calendar.d.ts +38 -18
- package/types/radix-ng-primitives-checkbox.d.ts +5 -5
- package/types/radix-ng-primitives-collapsible.d.ts +2 -1
- package/types/radix-ng-primitives-combobox.d.ts +1265 -0
- package/types/radix-ng-primitives-context-menu.d.ts +3 -2
- package/types/radix-ng-primitives-core.d.ts +187 -56
- package/types/radix-ng-primitives-cropper.d.ts +89 -56
- package/types/radix-ng-primitives-date-field.d.ts +11 -5
- package/types/radix-ng-primitives-dialog.d.ts +2 -1
- package/types/radix-ng-primitives-drawer.d.ts +5 -27
- package/types/radix-ng-primitives-editable.d.ts +90 -13
- package/types/radix-ng-primitives-field.d.ts +74 -4
- package/types/radix-ng-primitives-fieldset.d.ts +3 -2
- package/types/radix-ng-primitives-focus-scope.d.ts +2 -1
- package/types/radix-ng-primitives-form.d.ts +124 -0
- package/types/radix-ng-primitives-input.d.ts +75 -5
- package/types/radix-ng-primitives-menu.d.ts +16 -4
- package/types/radix-ng-primitives-menubar.d.ts +2 -1
- package/types/radix-ng-primitives-meter.d.ts +3 -2
- package/types/radix-ng-primitives-navigation-menu.d.ts +1 -1
- package/types/radix-ng-primitives-number-field.d.ts +6 -6
- package/types/radix-ng-primitives-popover.d.ts +2 -1
- package/types/radix-ng-primitives-popper.d.ts +19 -2
- package/types/radix-ng-primitives-preview-card.d.ts +1 -1
- package/types/radix-ng-primitives-progress.d.ts +3 -2
- package/types/radix-ng-primitives-roving-focus.d.ts +4 -3
- package/types/radix-ng-primitives-scroll-area.d.ts +253 -0
- package/types/radix-ng-primitives-select.d.ts +296 -136
- package/types/radix-ng-primitives-slider.d.ts +1 -1
- package/types/radix-ng-primitives-switch.d.ts +1 -1
- package/types/radix-ng-primitives-tabs.d.ts +1 -1
- package/types/radix-ng-primitives-toast.d.ts +378 -0
- package/types/radix-ng-primitives-toggle-group.d.ts +2 -1
- package/types/radix-ng-primitives-toolbar.d.ts +3 -2
- package/types/radix-ng-primitives-tooltip.d.ts +3 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, model, input, booleanAttribute, output, signal, computed, effect, untracked, Directive, ElementRef, DestroyRef, numberAttribute, afterNextRender, NgModule } from '@angular/core';
|
|
2
|
+
import { inject, model, input, booleanAttribute, output, signal, computed, effect, untracked, Directive, ElementRef, DestroyRef, numberAttribute, PLATFORM_ID, afterNextRender, NgModule } from '@angular/core';
|
|
3
3
|
import * as i1 from '@radix-ng/primitives/popper';
|
|
4
4
|
import { RdxPopper, RdxPopperContentWrapper, RdxPopperArrow, RdxPopperContent, provideRdxPopperContentConfig, RdxPopperAnchor } from '@radix-ng/primitives/popper';
|
|
5
5
|
import { createContext, useTransitionStatus, getMaxTransitionDuration } from '@radix-ng/primitives/core';
|
|
@@ -10,8 +10,9 @@ import * as i3 from '@radix-ng/primitives/focus-scope';
|
|
|
10
10
|
import { RdxFocusScope, provideRdxFocusScopeConfig } from '@radix-ng/primitives/focus-scope';
|
|
11
11
|
import * as i1$1 from '@radix-ng/primitives/portal';
|
|
12
12
|
import { RdxPortal } from '@radix-ng/primitives/portal';
|
|
13
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
13
14
|
|
|
14
|
-
const [injectRdxMenuRootContext, provideRdxMenuRootContext] = createContext('RdxMenuRootContext');
|
|
15
|
+
const [injectRdxMenuRootContext, provideRdxMenuRootContext] = createContext('RdxMenuRootContext', 'components/menu');
|
|
15
16
|
function buildContext(instance) {
|
|
16
17
|
return {
|
|
17
18
|
isOpen: instance.open,
|
|
@@ -25,12 +26,14 @@ function buildContext(instance) {
|
|
|
25
26
|
isSubmenu: instance.isSubmenu.asReadonly(),
|
|
26
27
|
hasTriggerInteractionHandler: instance.hasTriggerInteractionHandler.asReadonly(),
|
|
27
28
|
trigger: instance.trigger.asReadonly(),
|
|
29
|
+
popupElement: instance.popupElement.asReadonly(),
|
|
28
30
|
transitionStatus: instance.transitionStatus,
|
|
29
31
|
close: () => instance.close(),
|
|
30
32
|
toggle: () => instance.toggle(),
|
|
31
33
|
show: (autoFocus) => instance.show(autoFocus),
|
|
32
34
|
showWithoutAutoFocus: () => instance.show(false),
|
|
33
35
|
registerTrigger: (el) => instance.registerTrigger(el),
|
|
36
|
+
registerPopup: (el) => instance.registerPopup(el),
|
|
34
37
|
registerTransitionElement: (el) => instance.registerTransitionElement(el),
|
|
35
38
|
registerPopupArrowNavigationHandler: (handler) => instance.registerPopupArrowNavigationHandler(handler),
|
|
36
39
|
registerTriggerInteractionHandler: (handler) => instance.registerTriggerInteractionHandler(handler),
|
|
@@ -71,6 +74,7 @@ class RdxMenuRoot {
|
|
|
71
74
|
/** Emits when the open/close CSS transition or animation finishes. */
|
|
72
75
|
this.onOpenChangeComplete = output();
|
|
73
76
|
this.trigger = signal(undefined, ...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
|
|
77
|
+
this.popupElement = signal(undefined, ...(ngDevMode ? [{ debugName: "popupElement" }] : /* istanbul ignore next */ []));
|
|
74
78
|
this.transitionStatus = this.transition.status;
|
|
75
79
|
/** Whether the popup grabs focus when it opens. Set false for menubar hover-switching. */
|
|
76
80
|
this.autoFocus = signal('first', ...(ngDevMode ? [{ debugName: "autoFocus" }] : /* istanbul ignore next */ []));
|
|
@@ -131,6 +135,14 @@ class RdxMenuRoot {
|
|
|
131
135
|
}
|
|
132
136
|
};
|
|
133
137
|
}
|
|
138
|
+
registerPopup(el) {
|
|
139
|
+
this.popupElement.set(el);
|
|
140
|
+
return () => {
|
|
141
|
+
if (this.popupElement() === el) {
|
|
142
|
+
this.popupElement.set(undefined);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
134
146
|
registerTransitionElement(element) {
|
|
135
147
|
return this.transition.registerElement(element);
|
|
136
148
|
}
|
|
@@ -242,7 +254,7 @@ function getCheckedState(checked) {
|
|
|
242
254
|
return isIndeterminate(checked) ? 'indeterminate' : checked ? 'checked' : 'unchecked';
|
|
243
255
|
}
|
|
244
256
|
|
|
245
|
-
const [injectRdxMenuCheckboxItemContext, provideRdxMenuCheckboxItemContext] = createContext('RdxMenuCheckboxItemContext');
|
|
257
|
+
const [injectRdxMenuCheckboxItemContext, provideRdxMenuCheckboxItemContext] = createContext('RdxMenuCheckboxItemContext', 'components/menu');
|
|
246
258
|
const checkboxItemContextFactory = () => {
|
|
247
259
|
const instance = inject(RdxMenuCheckboxItem);
|
|
248
260
|
return {
|
|
@@ -637,8 +649,10 @@ class RdxMenuPopup {
|
|
|
637
649
|
*/
|
|
638
650
|
this.closeAutoFocus = outputFromObservable(outputToObservable(this.focusScope.unmountAutoFocus));
|
|
639
651
|
const unregister = this.rootContext.registerTransitionElement(this.elementRef.nativeElement);
|
|
652
|
+
const unregisterPopup = this.rootContext.registerPopup(this.elementRef.nativeElement);
|
|
640
653
|
inject(DestroyRef).onDestroy(() => {
|
|
641
654
|
unregister();
|
|
655
|
+
unregisterPopup();
|
|
642
656
|
clearTimeout(this.searchTimer);
|
|
643
657
|
});
|
|
644
658
|
effect((onCleanup) => {
|
|
@@ -979,7 +993,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
979
993
|
}]
|
|
980
994
|
}], propDecorators: { anchor: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchor", required: false }] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], sideOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "sideOffset", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], alignOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignOffset", required: false }] }], arrowPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "arrowPadding", required: false }] }], avoidCollisions: [{ type: i0.Input, args: [{ isSignal: true, alias: "avoidCollisions", required: false }] }], collisionBoundary: [{ type: i0.Input, args: [{ isSignal: true, alias: "collisionBoundary", required: false }] }], collisionPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "collisionPadding", required: false }] }], sticky: [{ type: i0.Input, args: [{ isSignal: true, alias: "sticky", required: false }] }], hideWhenDetached: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideWhenDetached", required: false }] }], positionStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "positionStrategy", required: false }] }], updatePositionStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "updatePositionStrategy", required: false }] }], placed: [{ type: i0.Output, args: ["placed"] }] } });
|
|
981
995
|
|
|
982
|
-
const [injectRdxMenuRadioGroupContext, provideRdxMenuRadioGroupContext] = createContext('RdxMenuRadioGroupContext');
|
|
996
|
+
const [injectRdxMenuRadioGroupContext, provideRdxMenuRadioGroupContext] = createContext('RdxMenuRadioGroupContext', 'components/menu');
|
|
983
997
|
const radioGroupContextFactory = () => {
|
|
984
998
|
const instance = inject(RdxMenuRadioGroup);
|
|
985
999
|
return {
|
|
@@ -1020,7 +1034,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1020
1034
|
}]
|
|
1021
1035
|
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }] } });
|
|
1022
1036
|
|
|
1023
|
-
const [injectRdxMenuRadioItemContext, provideRdxMenuRadioItemContext] = createContext('RdxMenuRadioItemContext');
|
|
1037
|
+
const [injectRdxMenuRadioItemContext, provideRdxMenuRadioItemContext] = createContext('RdxMenuRadioItemContext', 'components/menu');
|
|
1024
1038
|
const radioItemContextFactory = () => {
|
|
1025
1039
|
const instance = inject(RdxMenuRadioItem);
|
|
1026
1040
|
return {
|
|
@@ -1174,6 +1188,336 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1174
1188
|
}]
|
|
1175
1189
|
}] });
|
|
1176
1190
|
|
|
1191
|
+
/**
|
|
1192
|
+
* Submenu "safe polygon" — a faithful port of Floating UI's `safePolygon` algorithm
|
|
1193
|
+
* (https://floating-ui.com/docs/useHover#safepolygon), adapted to this library.
|
|
1194
|
+
*
|
|
1195
|
+
* While a submenu is open by hover, the parent submenu owns the decision to close itself: a
|
|
1196
|
+
* document-level `mousemove` handler keeps it open as long as the cursor is heading toward the
|
|
1197
|
+
* popup inside a safe quadrilateral (built from the cursor's exit point and the popup rect), and
|
|
1198
|
+
* closes it once the cursor leaves that area. Combined with the pointer-events "tunnel" below
|
|
1199
|
+
* (`applyPointerTunnel`), siblings cannot steal the open submenu during a diagonal traversal.
|
|
1200
|
+
*
|
|
1201
|
+
* Differences from the upstream implementation:
|
|
1202
|
+
* - `elements.domReference` / `elements.floating` → `reference` / `floating` (plain elements).
|
|
1203
|
+
* - `placement.split('-')[0]` → the `side` option (read live from the popup's `data-side`).
|
|
1204
|
+
* - the Floating UI tree (`tree` / `nodeId`) → the `hasOpenChild` callback, backed by the
|
|
1205
|
+
* module-level open-submenu registry.
|
|
1206
|
+
*
|
|
1207
|
+
* This deliberately does NOT reuse the core `useGraceArea` composable (tooltip / navigation-menu /
|
|
1208
|
+
* popover): that one is a simpler convex-hull grace area with no velocity gating, trough handling, or
|
|
1209
|
+
* pointer-events tunnel — none of which it needs, but all of which the submenu does to match Base UI
|
|
1210
|
+
* and to stop sibling triggers from stealing the open submenu mid-traversal.
|
|
1211
|
+
*/
|
|
1212
|
+
const CURSOR_SPEED_THRESHOLD = 0.1;
|
|
1213
|
+
const CURSOR_SPEED_THRESHOLD_SQUARED = CURSOR_SPEED_THRESHOLD * CURSOR_SPEED_THRESHOLD;
|
|
1214
|
+
const POLYGON_BUFFER = 0.5;
|
|
1215
|
+
/** Re-check delay when the cursor is inside the polygon but has not landed on the popup yet. */
|
|
1216
|
+
const REST_CHECK_MS = 40;
|
|
1217
|
+
function hasIntersectingEdge(pointX, pointY, xi, yi, xj, yj) {
|
|
1218
|
+
return yi >= pointY !== yj >= pointY && pointX <= ((xj - xi) * (pointY - yi)) / (yj - yi) + xi;
|
|
1219
|
+
}
|
|
1220
|
+
function isPointInQuadrilateral(pointX, pointY, x1, y1, x2, y2, x3, y3, x4, y4) {
|
|
1221
|
+
let inside = false;
|
|
1222
|
+
if (hasIntersectingEdge(pointX, pointY, x1, y1, x2, y2))
|
|
1223
|
+
inside = !inside;
|
|
1224
|
+
if (hasIntersectingEdge(pointX, pointY, x2, y2, x3, y3))
|
|
1225
|
+
inside = !inside;
|
|
1226
|
+
if (hasIntersectingEdge(pointX, pointY, x3, y3, x4, y4))
|
|
1227
|
+
inside = !inside;
|
|
1228
|
+
if (hasIntersectingEdge(pointX, pointY, x4, y4, x1, y1))
|
|
1229
|
+
inside = !inside;
|
|
1230
|
+
return inside;
|
|
1231
|
+
}
|
|
1232
|
+
function isInsideRect(pointX, pointY, rect) {
|
|
1233
|
+
return pointX >= rect.left && pointX <= rect.right && pointY >= rect.top && pointY <= rect.bottom;
|
|
1234
|
+
}
|
|
1235
|
+
function isInsideAxisAlignedRect(pointX, pointY, x1, y1, x2, y2) {
|
|
1236
|
+
const minX = Math.min(x1, x2);
|
|
1237
|
+
const maxX = Math.max(x1, x2);
|
|
1238
|
+
const minY = Math.min(y1, y2);
|
|
1239
|
+
const maxY = Math.max(y1, y2);
|
|
1240
|
+
return pointX >= minX && pointX <= maxX && pointY >= minY && pointY <= maxY;
|
|
1241
|
+
}
|
|
1242
|
+
function getTarget(event) {
|
|
1243
|
+
const path = event.composedPath?.();
|
|
1244
|
+
return path?.[0] ?? event.target;
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Builds the `mousemove` handler that decides whether the open submenu should stay open, plus a
|
|
1248
|
+
* `dispose()` that clears its internal rest-check timer (so a pending close can't fire after the
|
|
1249
|
+
* listener is removed).
|
|
1250
|
+
*/
|
|
1251
|
+
function createSafePolygonHandler(options) {
|
|
1252
|
+
const { reference, floating, side: sideOf, x, y, onClose, cancelClose, hasOpenChild, onLanded } = options;
|
|
1253
|
+
let hasLanded = false;
|
|
1254
|
+
let lastX = null;
|
|
1255
|
+
let lastY = null;
|
|
1256
|
+
let lastCursorTime = typeof performance !== 'undefined' ? performance.now() : 0;
|
|
1257
|
+
let restTimer;
|
|
1258
|
+
function isCursorMovingSlowly(nextX, nextY) {
|
|
1259
|
+
const currentTime = typeof performance !== 'undefined' ? performance.now() : 0;
|
|
1260
|
+
const elapsedTime = currentTime - lastCursorTime;
|
|
1261
|
+
if (lastX === null || lastY === null || elapsedTime === 0) {
|
|
1262
|
+
lastX = nextX;
|
|
1263
|
+
lastY = nextY;
|
|
1264
|
+
lastCursorTime = currentTime;
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
const deltaX = nextX - lastX;
|
|
1268
|
+
const deltaY = nextY - lastY;
|
|
1269
|
+
const distanceSquared = deltaX * deltaX + deltaY * deltaY;
|
|
1270
|
+
const thresholdSquared = elapsedTime * elapsedTime * CURSOR_SPEED_THRESHOLD_SQUARED;
|
|
1271
|
+
lastX = nextX;
|
|
1272
|
+
lastY = nextY;
|
|
1273
|
+
lastCursorTime = currentTime;
|
|
1274
|
+
return distanceSquared < thresholdSquared;
|
|
1275
|
+
}
|
|
1276
|
+
function closeIfNoOpenChild() {
|
|
1277
|
+
if (!hasOpenChild()) {
|
|
1278
|
+
onClose();
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
function onMouseMove(event) {
|
|
1282
|
+
cancelClose();
|
|
1283
|
+
clearTimeout(restTimer);
|
|
1284
|
+
const { clientX, clientY } = event;
|
|
1285
|
+
const target = getTarget(event);
|
|
1286
|
+
const isOverFloatingEl = !!target && floating.contains(target);
|
|
1287
|
+
const isOverReferenceEl = !!target && reference.contains(target);
|
|
1288
|
+
// This handler is bound only to document `mousemove` (never `mouseleave`), so Floating UI's
|
|
1289
|
+
// leave-specific paths — the overlapping-element `relatedTarget` guard and the `!isLeave`
|
|
1290
|
+
// guards — are omitted: they could never run here.
|
|
1291
|
+
if (isOverFloatingEl) {
|
|
1292
|
+
// "Landed" tracks reaching the popup only — not the trigger. Setting it on the trigger
|
|
1293
|
+
// would close as soon as the cursor enters the gap (no leave event resets it).
|
|
1294
|
+
if (!hasLanded) {
|
|
1295
|
+
hasLanded = true;
|
|
1296
|
+
onLanded?.();
|
|
1297
|
+
}
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
if (isOverReferenceEl) {
|
|
1301
|
+
// Over the trigger — stay open; the polygon/trough logic resumes once in the gap.
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
// If any nested child submenu is open, never close the parent.
|
|
1305
|
+
if (hasOpenChild())
|
|
1306
|
+
return;
|
|
1307
|
+
// Read the placed side live — it may have been unresolved at open time, or flipped since.
|
|
1308
|
+
const side = sideOf();
|
|
1309
|
+
const refRect = reference.getBoundingClientRect();
|
|
1310
|
+
const rect = floating.getBoundingClientRect();
|
|
1311
|
+
const cursorLeaveFromRight = x > rect.right - rect.width / 2;
|
|
1312
|
+
const cursorLeaveFromBottom = y > rect.bottom - rect.height / 2;
|
|
1313
|
+
const isFloatingWider = rect.width > refRect.width;
|
|
1314
|
+
const isFloatingTaller = rect.height > refRect.height;
|
|
1315
|
+
const left = (isFloatingWider ? refRect : rect).left;
|
|
1316
|
+
const right = (isFloatingWider ? refRect : rect).right;
|
|
1317
|
+
const top = (isFloatingTaller ? refRect : rect).top;
|
|
1318
|
+
const bottom = (isFloatingTaller ? refRect : rect).bottom;
|
|
1319
|
+
// Leaving from the opposite side: the buffer logic would otherwise keep it open — close.
|
|
1320
|
+
if ((side === 'top' && y >= refRect.bottom - 1) ||
|
|
1321
|
+
(side === 'bottom' && y <= refRect.top + 1) ||
|
|
1322
|
+
(side === 'left' && x >= refRect.right - 1) ||
|
|
1323
|
+
(side === 'right' && x <= refRect.left + 1)) {
|
|
1324
|
+
closeIfNoOpenChild();
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
// Stay open while the cursor is within the rectangular trough between the two elements.
|
|
1328
|
+
let isInsideTroughRect = false;
|
|
1329
|
+
switch (side) {
|
|
1330
|
+
case 'top':
|
|
1331
|
+
isInsideTroughRect = isInsideAxisAlignedRect(clientX, clientY, left, refRect.top + 1, right, rect.bottom - 1);
|
|
1332
|
+
break;
|
|
1333
|
+
case 'bottom':
|
|
1334
|
+
isInsideTroughRect = isInsideAxisAlignedRect(clientX, clientY, left, rect.top + 1, right, refRect.bottom - 1);
|
|
1335
|
+
break;
|
|
1336
|
+
case 'left':
|
|
1337
|
+
isInsideTroughRect = isInsideAxisAlignedRect(clientX, clientY, rect.right - 1, bottom, refRect.left + 1, top);
|
|
1338
|
+
break;
|
|
1339
|
+
case 'right':
|
|
1340
|
+
isInsideTroughRect = isInsideAxisAlignedRect(clientX, clientY, refRect.right - 1, bottom, rect.left + 1, top);
|
|
1341
|
+
break;
|
|
1342
|
+
}
|
|
1343
|
+
if (isInsideTroughRect)
|
|
1344
|
+
return;
|
|
1345
|
+
if (hasLanded && !isInsideRect(clientX, clientY, refRect)) {
|
|
1346
|
+
closeIfNoOpenChild();
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (isCursorMovingSlowly(clientX, clientY)) {
|
|
1350
|
+
closeIfNoOpenChild();
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
let isInsidePolygon = false;
|
|
1354
|
+
switch (side) {
|
|
1355
|
+
case 'top': {
|
|
1356
|
+
const cursorXOffset = isFloatingWider ? POLYGON_BUFFER / 2 : POLYGON_BUFFER * 4;
|
|
1357
|
+
const cursorPointOneX = isFloatingWider
|
|
1358
|
+
? x + cursorXOffset
|
|
1359
|
+
: cursorLeaveFromRight
|
|
1360
|
+
? x + cursorXOffset
|
|
1361
|
+
: x - cursorXOffset;
|
|
1362
|
+
const cursorPointTwoX = isFloatingWider
|
|
1363
|
+
? x - cursorXOffset
|
|
1364
|
+
: cursorLeaveFromRight
|
|
1365
|
+
? x + cursorXOffset
|
|
1366
|
+
: x - cursorXOffset;
|
|
1367
|
+
const cursorPointY = y + POLYGON_BUFFER + 1;
|
|
1368
|
+
const commonYLeft = cursorLeaveFromRight
|
|
1369
|
+
? rect.bottom - POLYGON_BUFFER
|
|
1370
|
+
: isFloatingWider
|
|
1371
|
+
? rect.bottom - POLYGON_BUFFER
|
|
1372
|
+
: rect.top;
|
|
1373
|
+
const commonYRight = cursorLeaveFromRight
|
|
1374
|
+
? isFloatingWider
|
|
1375
|
+
? rect.bottom - POLYGON_BUFFER
|
|
1376
|
+
: rect.top
|
|
1377
|
+
: rect.bottom - POLYGON_BUFFER;
|
|
1378
|
+
isInsidePolygon = isPointInQuadrilateral(clientX, clientY, cursorPointOneX, cursorPointY, cursorPointTwoX, cursorPointY, rect.left, commonYLeft, rect.right, commonYRight);
|
|
1379
|
+
break;
|
|
1380
|
+
}
|
|
1381
|
+
case 'bottom': {
|
|
1382
|
+
const cursorXOffset = isFloatingWider ? POLYGON_BUFFER / 2 : POLYGON_BUFFER * 4;
|
|
1383
|
+
const cursorPointOneX = isFloatingWider
|
|
1384
|
+
? x + cursorXOffset
|
|
1385
|
+
: cursorLeaveFromRight
|
|
1386
|
+
? x + cursorXOffset
|
|
1387
|
+
: x - cursorXOffset;
|
|
1388
|
+
const cursorPointTwoX = isFloatingWider
|
|
1389
|
+
? x - cursorXOffset
|
|
1390
|
+
: cursorLeaveFromRight
|
|
1391
|
+
? x + cursorXOffset
|
|
1392
|
+
: x - cursorXOffset;
|
|
1393
|
+
const cursorPointY = y - POLYGON_BUFFER;
|
|
1394
|
+
const commonYLeft = cursorLeaveFromRight
|
|
1395
|
+
? rect.top + POLYGON_BUFFER
|
|
1396
|
+
: isFloatingWider
|
|
1397
|
+
? rect.top + POLYGON_BUFFER
|
|
1398
|
+
: rect.bottom;
|
|
1399
|
+
const commonYRight = cursorLeaveFromRight
|
|
1400
|
+
? isFloatingWider
|
|
1401
|
+
? rect.top + POLYGON_BUFFER
|
|
1402
|
+
: rect.bottom
|
|
1403
|
+
: rect.top + POLYGON_BUFFER;
|
|
1404
|
+
isInsidePolygon = isPointInQuadrilateral(clientX, clientY, cursorPointOneX, cursorPointY, cursorPointTwoX, cursorPointY, rect.left, commonYLeft, rect.right, commonYRight);
|
|
1405
|
+
break;
|
|
1406
|
+
}
|
|
1407
|
+
case 'left': {
|
|
1408
|
+
const cursorYOffset = isFloatingTaller ? POLYGON_BUFFER / 2 : POLYGON_BUFFER * 4;
|
|
1409
|
+
const cursorPointOneY = isFloatingTaller
|
|
1410
|
+
? y + cursorYOffset
|
|
1411
|
+
: cursorLeaveFromBottom
|
|
1412
|
+
? y + cursorYOffset
|
|
1413
|
+
: y - cursorYOffset;
|
|
1414
|
+
const cursorPointTwoY = isFloatingTaller
|
|
1415
|
+
? y - cursorYOffset
|
|
1416
|
+
: cursorLeaveFromBottom
|
|
1417
|
+
? y + cursorYOffset
|
|
1418
|
+
: y - cursorYOffset;
|
|
1419
|
+
const cursorPointX = x + POLYGON_BUFFER + 1;
|
|
1420
|
+
const commonXTop = cursorLeaveFromBottom
|
|
1421
|
+
? rect.right - POLYGON_BUFFER
|
|
1422
|
+
: isFloatingTaller
|
|
1423
|
+
? rect.right - POLYGON_BUFFER
|
|
1424
|
+
: rect.left;
|
|
1425
|
+
const commonXBottom = cursorLeaveFromBottom
|
|
1426
|
+
? isFloatingTaller
|
|
1427
|
+
? rect.right - POLYGON_BUFFER
|
|
1428
|
+
: rect.left
|
|
1429
|
+
: rect.right - POLYGON_BUFFER;
|
|
1430
|
+
isInsidePolygon = isPointInQuadrilateral(clientX, clientY, commonXTop, rect.top, commonXBottom, rect.bottom, cursorPointX, cursorPointOneY, cursorPointX, cursorPointTwoY);
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
case 'right': {
|
|
1434
|
+
const cursorYOffset = isFloatingTaller ? POLYGON_BUFFER / 2 : POLYGON_BUFFER * 4;
|
|
1435
|
+
const cursorPointOneY = isFloatingTaller
|
|
1436
|
+
? y + cursorYOffset
|
|
1437
|
+
: cursorLeaveFromBottom
|
|
1438
|
+
? y + cursorYOffset
|
|
1439
|
+
: y - cursorYOffset;
|
|
1440
|
+
const cursorPointTwoY = isFloatingTaller
|
|
1441
|
+
? y - cursorYOffset
|
|
1442
|
+
: cursorLeaveFromBottom
|
|
1443
|
+
? y + cursorYOffset
|
|
1444
|
+
: y - cursorYOffset;
|
|
1445
|
+
const cursorPointX = x - POLYGON_BUFFER;
|
|
1446
|
+
const commonXTop = cursorLeaveFromBottom
|
|
1447
|
+
? rect.left + POLYGON_BUFFER
|
|
1448
|
+
: isFloatingTaller
|
|
1449
|
+
? rect.left + POLYGON_BUFFER
|
|
1450
|
+
: rect.right;
|
|
1451
|
+
const commonXBottom = cursorLeaveFromBottom
|
|
1452
|
+
? isFloatingTaller
|
|
1453
|
+
? rect.left + POLYGON_BUFFER
|
|
1454
|
+
: rect.right
|
|
1455
|
+
: rect.left + POLYGON_BUFFER;
|
|
1456
|
+
isInsidePolygon = isPointInQuadrilateral(clientX, clientY, cursorPointX, cursorPointOneY, cursorPointX, cursorPointTwoY, commonXTop, rect.top, commonXBottom, rect.bottom);
|
|
1457
|
+
break;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (!isInsidePolygon) {
|
|
1461
|
+
closeIfNoOpenChild();
|
|
1462
|
+
}
|
|
1463
|
+
else if (!hasLanded) {
|
|
1464
|
+
// Inside the polygon but not landed: if the cursor halts here (no further moves),
|
|
1465
|
+
// close shortly after so a stationary cursor in the gap doesn't keep it open.
|
|
1466
|
+
restTimer = setTimeout(closeIfNoOpenChild, REST_CHECK_MS);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return { handler: onMouseMove, dispose: () => clearTimeout(restTimer) };
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Pointer-events "tunnel" used while a submenu opened by hover is being traversed. Disables pointer
|
|
1473
|
+
* events on `scope` (the parent popup or `document.body`) while keeping the `reference` and
|
|
1474
|
+
* `floating` interactive, so sibling items cannot react until the cursor lands or the submenu
|
|
1475
|
+
* closes. Returns a cleanup that restores the exact previous inline values.
|
|
1476
|
+
*/
|
|
1477
|
+
function applyPointerTunnel(scope, reference, floating) {
|
|
1478
|
+
const saved = [
|
|
1479
|
+
[scope, scope.style.pointerEvents],
|
|
1480
|
+
[reference, reference.style.pointerEvents],
|
|
1481
|
+
[floating, floating.style.pointerEvents]
|
|
1482
|
+
];
|
|
1483
|
+
scope.style.pointerEvents = 'none';
|
|
1484
|
+
reference.style.pointerEvents = 'auto';
|
|
1485
|
+
floating.style.pointerEvents = 'auto';
|
|
1486
|
+
let cleaned = false;
|
|
1487
|
+
return () => {
|
|
1488
|
+
if (cleaned)
|
|
1489
|
+
return;
|
|
1490
|
+
cleaned = true;
|
|
1491
|
+
saved.forEach(([el, prev]) => (el.style.pointerEvents = prev));
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
/** Registry of submenus currently open, keyed by their trigger element. */
|
|
1495
|
+
const openSubmenus = new Map();
|
|
1496
|
+
/** Marks `trigger`'s submenu (with popup `popup`) as open. Returns a cleanup to unmark it. */
|
|
1497
|
+
function registerOpenSubmenu(trigger, popup) {
|
|
1498
|
+
openSubmenus.set(trigger, popup);
|
|
1499
|
+
return () => {
|
|
1500
|
+
if (openSubmenus.get(trigger) === popup) {
|
|
1501
|
+
openSubmenus.delete(trigger);
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Whether any *other* open submenu is a descendant of `floating` (a nested child is open).
|
|
1507
|
+
*
|
|
1508
|
+
* Portal-safe: `RdxMenuPortal` only teleports a submenu's positioner/popup template, never the
|
|
1509
|
+
* sub-trigger (which stays in its parent popup), and a portaled popup carries its descendant
|
|
1510
|
+
* triggers with it — so `floating.contains(childTrigger)` holds whether or not portals are used.
|
|
1511
|
+
*/
|
|
1512
|
+
function hasOpenChildSubmenu(reference, floating) {
|
|
1513
|
+
for (const trigger of openSubmenus.keys()) {
|
|
1514
|
+
if (trigger !== reference && floating.contains(trigger)) {
|
|
1515
|
+
return true;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
return false;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1177
1521
|
const numberOrUndefined$1 = (value) => (value == null ? undefined : numberAttribute(value));
|
|
1178
1522
|
const submenuRootsByTrigger = new WeakMap();
|
|
1179
1523
|
/**
|
|
@@ -1190,7 +1534,12 @@ class RdxMenuSubTrigger {
|
|
|
1190
1534
|
this.submenuRoot = inject(RdxMenuRoot);
|
|
1191
1535
|
this.elementRef = inject(ElementRef);
|
|
1192
1536
|
this.destroyRef = inject(DestroyRef);
|
|
1537
|
+
this.isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
1193
1538
|
this.isFocused = signal(false, ...(ngDevMode ? [{ debugName: "isFocused" }] : /* istanbul ignore next */ []));
|
|
1539
|
+
/** Cursor position from the last pointer move over the trigger (safe-polygon apex). */
|
|
1540
|
+
this.lastPointer = null;
|
|
1541
|
+
/** Whether the current open was initiated by hover (vs keyboard / click). */
|
|
1542
|
+
this.openedByHover = false;
|
|
1194
1543
|
/** Whether this trigger (and therefore the submenu) is disabled. */
|
|
1195
1544
|
this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1196
1545
|
/** Whether this trigger should be treated as a native button. Auto-detected for `<button>`. */
|
|
@@ -1216,10 +1565,65 @@ class RdxMenuSubTrigger {
|
|
|
1216
1565
|
submenuRootsByTrigger.delete(el);
|
|
1217
1566
|
});
|
|
1218
1567
|
});
|
|
1568
|
+
// While this submenu is open by hover, it owns the decision to close itself: a document
|
|
1569
|
+
// `mousemove` handler keeps it open while the cursor traverses the safe polygon toward the
|
|
1570
|
+
// popup, and a pointer-events tunnel stops siblings from stealing it mid-traversal.
|
|
1571
|
+
effect((onCleanup) => {
|
|
1572
|
+
const open = this.submenuContext.isOpen();
|
|
1573
|
+
const popup = this.submenuContext.popupElement();
|
|
1574
|
+
// Once closed, forget how this open started so the next (possibly keyboard / programmatic)
|
|
1575
|
+
// open doesn't re-arm the hover tunnel from a stale flag.
|
|
1576
|
+
if (!open) {
|
|
1577
|
+
this.openedByHover = false;
|
|
1578
|
+
this.lastPointer = null;
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
if (!popup || !this.openedByHover || !this.lastPointer || !this.isBrowser) {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
const reference = this.elementRef.nativeElement;
|
|
1585
|
+
const scope = reference.closest('[rdxMenuPopup]') ?? document.body;
|
|
1586
|
+
const unregisterOpen = registerOpenSubmenu(reference, popup);
|
|
1587
|
+
let removeTunnel = applyPointerTunnel(scope, reference, popup);
|
|
1588
|
+
const { handler, dispose } = createSafePolygonHandler({
|
|
1589
|
+
reference,
|
|
1590
|
+
floating: popup,
|
|
1591
|
+
// Live getter: `data-side` may be unresolved at open time and can flip on collision.
|
|
1592
|
+
side: () => popup.getAttribute('data-side') ?? 'right',
|
|
1593
|
+
x: this.lastPointer.x,
|
|
1594
|
+
y: this.lastPointer.y,
|
|
1595
|
+
onClose: () => this.scheduleClose(),
|
|
1596
|
+
cancelClose: () => clearTimeout(this.closeTimer),
|
|
1597
|
+
hasOpenChild: () => hasOpenChildSubmenu(reference, popup),
|
|
1598
|
+
onLanded: () => {
|
|
1599
|
+
removeTunnel?.();
|
|
1600
|
+
removeTunnel = undefined;
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
document.addEventListener('mousemove', handler);
|
|
1604
|
+
onCleanup(() => {
|
|
1605
|
+
document.removeEventListener('mousemove', handler);
|
|
1606
|
+
dispose();
|
|
1607
|
+
removeTunnel?.();
|
|
1608
|
+
unregisterOpen();
|
|
1609
|
+
clearTimeout(this.closeTimer);
|
|
1610
|
+
});
|
|
1611
|
+
});
|
|
1219
1612
|
this.destroyRef.onDestroy(() => {
|
|
1220
1613
|
clearTimeout(this.openTimer);
|
|
1614
|
+
clearTimeout(this.closeTimer);
|
|
1221
1615
|
});
|
|
1222
1616
|
}
|
|
1617
|
+
scheduleClose() {
|
|
1618
|
+
clearTimeout(this.closeTimer);
|
|
1619
|
+
const delay = this.closeDelay() ?? 0;
|
|
1620
|
+
if (delay <= 0) {
|
|
1621
|
+
this.submenuContext.close();
|
|
1622
|
+
}
|
|
1623
|
+
else {
|
|
1624
|
+
this.closeTimer = setTimeout(() => this.submenuContext.close(), delay);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1223
1627
|
onFocus() {
|
|
1224
1628
|
if (!this.disabled()) {
|
|
1225
1629
|
this.clearSiblingHighlights();
|
|
@@ -1232,6 +1636,7 @@ class RdxMenuSubTrigger {
|
|
|
1232
1636
|
onClick() {
|
|
1233
1637
|
if (this.disabled())
|
|
1234
1638
|
return;
|
|
1639
|
+
this.openedByHover = false;
|
|
1235
1640
|
this.clearSiblingHighlights();
|
|
1236
1641
|
if (!this.submenuContext.isOpen()) {
|
|
1237
1642
|
this.closeSiblingSubmenus();
|
|
@@ -1243,6 +1648,7 @@ class RdxMenuSubTrigger {
|
|
|
1243
1648
|
return;
|
|
1244
1649
|
event.preventDefault();
|
|
1245
1650
|
event.stopPropagation();
|
|
1651
|
+
this.openedByHover = false;
|
|
1246
1652
|
this.clearSiblingHighlights();
|
|
1247
1653
|
if (!this.submenuContext.isOpen()) {
|
|
1248
1654
|
this.closeSiblingSubmenus();
|
|
@@ -1252,6 +1658,7 @@ class RdxMenuSubTrigger {
|
|
|
1252
1658
|
onPointerMove(event) {
|
|
1253
1659
|
if (event.pointerType !== 'mouse' || this.disabled() || !this.openOnHover())
|
|
1254
1660
|
return;
|
|
1661
|
+
this.lastPointer = { x: event.clientX, y: event.clientY };
|
|
1255
1662
|
this.clearSiblingHighlights();
|
|
1256
1663
|
if (this.submenuContext.highlightItemOnHover() && document.activeElement !== this.elementRef.nativeElement) {
|
|
1257
1664
|
this.elementRef.nativeElement.focus({ preventScroll: true });
|
|
@@ -1260,6 +1667,7 @@ class RdxMenuSubTrigger {
|
|
|
1260
1667
|
clearTimeout(this.openTimer);
|
|
1261
1668
|
this.closeSiblingSubmenus();
|
|
1262
1669
|
this.openTimer = setTimeout(() => {
|
|
1670
|
+
this.openedByHover = true;
|
|
1263
1671
|
this.submenuContext.show(false);
|
|
1264
1672
|
}, this.delay() ?? 100);
|
|
1265
1673
|
}
|