@radix-ng/primitives 1.0.0-beta.2 → 1.0.0-beta.4
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/LICENSE +1 -1
- package/README.md +76 -6
- package/fesm2022/radix-ng-primitives-accordion.mjs +5 -3
- package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-alert-dialog.mjs +31 -24
- package/fesm2022/radix-ng-primitives-alert-dialog.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-autocomplete.mjs +1744 -0
- package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -0
- package/fesm2022/radix-ng-primitives-calendar.mjs +5 -3
- package/fesm2022/radix-ng-primitives-calendar.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-combobox.mjs +1399 -606
- package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-config.mjs +13 -4
- package/fesm2022/radix-ng-primitives-config.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-context-menu.mjs +51 -10
- package/fesm2022/radix-ng-primitives-context-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-core.mjs +1345 -64
- package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-date-field.mjs +5 -3
- package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-dialog.mjs +271 -145
- package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-direction-provider.mjs +70 -0
- package/fesm2022/radix-ng-primitives-direction-provider.mjs.map +1 -0
- package/fesm2022/radix-ng-primitives-dismissable-layer.mjs +519 -184
- package/fesm2022/radix-ng-primitives-dismissable-layer.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-drawer.mjs +154 -64
- package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-field.mjs +3 -2
- package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs +517 -0
- package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs.map +1 -0
- package/fesm2022/radix-ng-primitives-focus-scope.mjs +296 -70
- package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-menu.mjs +894 -299
- package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-menubar.mjs +32 -4
- package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs +176 -207
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-popover.mjs +250 -250
- package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-popper.mjs +94 -45
- package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-portal.mjs +107 -17
- package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-presence.mjs +262 -79
- package/fesm2022/radix-ng-primitives-presence.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-preview-card.mjs +172 -218
- package/fesm2022/radix-ng-primitives-preview-card.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-roving-focus.mjs +4 -2
- package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-scroll-area.mjs +5 -4
- package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-select.mjs +303 -234
- package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-slider.mjs +5 -3
- package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-stepper.mjs +5 -3
- package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-time-field.mjs +5 -3
- package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-toast.mjs +15 -36
- package/fesm2022/radix-ng-primitives-toast.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-toggle-group.mjs +5 -3
- package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-toolbar.mjs +5 -3
- package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-tooltip.mjs +105 -145
- package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
- package/package.json +14 -1
- package/types/radix-ng-primitives-accordion.d.ts +4 -3
- package/types/radix-ng-primitives-alert-dialog.d.ts +17 -11
- package/types/radix-ng-primitives-autocomplete.d.ts +661 -0
- package/types/radix-ng-primitives-calendar.d.ts +5 -3
- package/types/radix-ng-primitives-combobox.d.ts +727 -293
- package/types/radix-ng-primitives-config.d.ts +1 -1
- package/types/radix-ng-primitives-context-menu.d.ts +15 -5
- package/types/radix-ng-primitives-core.d.ts +762 -14
- package/types/radix-ng-primitives-date-field.d.ts +3 -2
- package/types/radix-ng-primitives-dialog.d.ts +107 -55
- package/types/radix-ng-primitives-direction-provider.d.ts +41 -0
- package/types/radix-ng-primitives-dismissable-layer.d.ts +147 -99
- package/types/radix-ng-primitives-drawer.d.ts +49 -22
- package/types/radix-ng-primitives-field.d.ts +1 -0
- package/types/radix-ng-primitives-floating-focus-manager.d.ts +175 -0
- package/types/radix-ng-primitives-focus-scope.d.ts +132 -1
- package/types/radix-ng-primitives-menu.d.ts +204 -112
- package/types/radix-ng-primitives-navigation-menu.d.ts +61 -101
- package/types/radix-ng-primitives-popover.d.ts +82 -115
- package/types/radix-ng-primitives-popper.d.ts +46 -10
- package/types/radix-ng-primitives-portal.d.ts +53 -8
- package/types/radix-ng-primitives-presence.d.ts +98 -17
- package/types/radix-ng-primitives-preview-card.d.ts +63 -95
- package/types/radix-ng-primitives-roving-focus.d.ts +7 -6
- package/types/radix-ng-primitives-scroll-area.d.ts +2 -2
- package/types/radix-ng-primitives-select.d.ts +192 -158
- package/types/radix-ng-primitives-slider.d.ts +5 -4
- package/types/radix-ng-primitives-stepper.d.ts +4 -3
- package/types/radix-ng-primitives-time-field.d.ts +3 -2
- package/types/radix-ng-primitives-toast.d.ts +7 -7
- package/types/radix-ng-primitives-toggle-group.d.ts +5 -4
- package/types/radix-ng-primitives-toolbar.d.ts +3 -2
- package/types/radix-ng-primitives-tooltip.d.ts +48 -84
|
@@ -1,26 +1,527 @@
|
|
|
1
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
1
2
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
+
import { inject, DestroyRef, PLATFORM_ID, ElementRef, Directive, input, booleanAttribute, linkedSignal, output, signal, effect, afterNextRender } from '@angular/core';
|
|
4
|
+
import { RDX_FLOATING_MARKER } from '@radix-ng/primitives/floating-focus-manager';
|
|
5
|
+
import { RDX_FLOATING_ROOT_CONTEXT } from '@radix-ng/primitives/core';
|
|
3
6
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
const alwaysTrue = () => true;
|
|
8
|
+
const alwaysFalse = () => false;
|
|
9
|
+
const sloppy = () => 'sloppy';
|
|
10
|
+
/** Duck-types an `EventTarget` to `Node` (cross-realm-safe; `Node.contains` throws on a non-Node). */
|
|
11
|
+
function isNode(target) {
|
|
12
|
+
return target !== null && typeof target.nodeType === 'number';
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Owner-document-safe `HTMLElement` check. A raw `target instanceof HTMLElement` is realm-sensitive — it
|
|
16
|
+
* returns `false` for an element from another document (iframe / popup window) because that realm has its
|
|
17
|
+
* own `HTMLElement` constructor. Resolve the constructor from the node's own `defaultView` (Base UI
|
|
18
|
+
* `isHTMLElement`).
|
|
19
|
+
*/
|
|
20
|
+
function isHTMLElement(target) {
|
|
21
|
+
if (!isNode(target)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const view = target.ownerDocument?.defaultView;
|
|
25
|
+
return view ? target instanceof view.HTMLElement : target instanceof HTMLElement;
|
|
26
|
+
}
|
|
27
|
+
/** Owner-document-safe `Element` check for SVG/custom-element outside targets. */
|
|
28
|
+
function isElement(target) {
|
|
29
|
+
if (!isNode(target)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const view = target.ownerDocument?.defaultView;
|
|
33
|
+
return view ? target instanceof view.Element : target instanceof Element;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether `window` is a WebKit (Safari / any iOS browser) engine — its IME `compositionend`/`keydown`
|
|
37
|
+
* ordering needs a longer guard. Requires the `Safari` token and excludes desktop Blink (Chrome /
|
|
38
|
+
* Edge / Android), so jsdom (`AppleWebKit/537.36 … jsdom`, no `Safari`) is correctly **not** WebKit and
|
|
39
|
+
* the unit timing stays 0ms.
|
|
40
|
+
*/
|
|
41
|
+
function isWebKit(window) {
|
|
42
|
+
const ua = window.navigator.userAgent;
|
|
43
|
+
return /AppleWebKit/i.test(ua) && /Safari/i.test(ua) && !/Chrome|Chromium|Edg|Android/i.test(ua);
|
|
44
|
+
}
|
|
45
|
+
/** Only a primary (left / default) press dismisses — a non-primary mouse button is ignored. */
|
|
46
|
+
function isPrimaryButton(event) {
|
|
47
|
+
return !('button' in event) || event.button === 0;
|
|
48
|
+
}
|
|
49
|
+
function isRootElement(element) {
|
|
50
|
+
const document = element.ownerDocument;
|
|
51
|
+
return element === document.documentElement || element === document.body;
|
|
52
|
+
}
|
|
53
|
+
function isShadowRoot(value) {
|
|
54
|
+
return value !== null && value.nodeType === 11 && 'host' in value;
|
|
55
|
+
}
|
|
56
|
+
function getParentNode(element) {
|
|
57
|
+
const parent = element.parentNode;
|
|
58
|
+
if (parent) {
|
|
59
|
+
return parent;
|
|
60
|
+
}
|
|
61
|
+
const root = element.getRootNode();
|
|
62
|
+
return isShadowRoot(root) ? root.host : null;
|
|
63
|
+
}
|
|
64
|
+
function isLastTraversableNode(node) {
|
|
65
|
+
return (node === null ||
|
|
66
|
+
node.nodeType === Node.DOCUMENT_NODE ||
|
|
67
|
+
isShadowRoot(node) ||
|
|
68
|
+
(isElement(node) && isRootElement(node)));
|
|
69
|
+
}
|
|
70
|
+
function getTargetRootAncestor(target) {
|
|
71
|
+
let ancestor = target;
|
|
72
|
+
while (!isLastTraversableNode(ancestor)) {
|
|
73
|
+
const parent = getParentNode(ancestor);
|
|
74
|
+
if (isLastTraversableNode(parent) || !isElement(parent)) {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
ancestor = parent;
|
|
78
|
+
}
|
|
79
|
+
return ancestor;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Base UI third-party guard: ignore elements injected after `markOthers` ran. A normal outside target sits
|
|
83
|
+
* in a root subtree that contains at least one marker; an injected root does not.
|
|
84
|
+
*/
|
|
85
|
+
function isInjectedAfterMark(target, context) {
|
|
86
|
+
if (!isElement(target) || isRootElement(target)) {
|
|
87
|
+
return false;
|
|
12
88
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
89
|
+
if (target.closest('[data-rdx-internal-backdrop], [data-rdx-menu-internal-backdrop], [data-rdx-dialog-internal-backdrop]')) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const targetRoot = target.getRootNode();
|
|
93
|
+
const queryRoot = isShadowRoot(targetRoot) ? targetRoot : context.ownerDocument;
|
|
94
|
+
const markers = Array.from(queryRoot.querySelectorAll(`[${RDX_FLOATING_MARKER}]`));
|
|
95
|
+
if (markers.length === 0) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const floating = context.floatingElement;
|
|
99
|
+
if (floating && target.contains(floating)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const targetRootAncestor = getTargetRootAncestor(target);
|
|
103
|
+
return markers.every((marker) => !targetRootAncestor.contains(marker));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Whether the press landed on `target`'s scrollbar (a scrollbar drag must not dismiss). Mirrors Base
|
|
107
|
+
* UI's geometry check (`useDismiss.ts`): skipped for touch (scrollbars get no touch events) and resolved
|
|
108
|
+
* from the element's scroll metrics + the press offset.
|
|
109
|
+
*
|
|
110
|
+
* @remarks Layout-dependent — verified by Playwright, not jsdom (which reports zero box metrics, so this
|
|
111
|
+
* correctly returns `false` there). See `apps/visual-regression`.
|
|
112
|
+
*/
|
|
113
|
+
function isScrollbarPress(event) {
|
|
114
|
+
const target = event.target;
|
|
115
|
+
if (!isHTMLElement(target) || 'touches' in event || typeof event.offsetX !== 'number') {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
const view = target.ownerDocument.defaultView;
|
|
119
|
+
if (!view) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const press = event;
|
|
123
|
+
const style = view.getComputedStyle(target);
|
|
124
|
+
const scrollable = /auto|scroll/;
|
|
125
|
+
const lastTraversableNode = isLastTraversableNode(target);
|
|
126
|
+
const isScrollableX = lastTraversableNode || scrollable.test(style.overflowX);
|
|
127
|
+
const isScrollableY = lastTraversableNode || scrollable.test(style.overflowY);
|
|
128
|
+
const canScrollX = isScrollableX && target.clientWidth > 0 && target.scrollWidth > target.clientWidth;
|
|
129
|
+
const canScrollY = isScrollableY && target.clientHeight > 0 && target.scrollHeight > target.clientHeight;
|
|
130
|
+
const isRtl = style.direction === 'rtl';
|
|
131
|
+
const onVerticalScrollbar = canScrollY &&
|
|
132
|
+
(isRtl ? press.offsetX <= target.offsetWidth - target.clientWidth : press.offsetX > target.clientWidth);
|
|
133
|
+
const onHorizontalScrollbar = canScrollX && press.offsetY > target.clientHeight;
|
|
134
|
+
return onVerticalScrollbar || onHorizontalScrollbar;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Each capability publishes its bubbles policy here, keyed by its root context, so an **ancestor**'s
|
|
138
|
+
* `hasBlockingChild` can read a child's flags — the Angular counterpart of Base UI storing
|
|
139
|
+
* `__escapeKeyBubbles` / `__outsidePressBubbles` on the context's `dataRef` (`useDismiss.ts`). Kept in a
|
|
140
|
+
* dismissal-private `WeakMap` (not on the neutral context) so the context stays capability-agnostic.
|
|
141
|
+
*/
|
|
142
|
+
const dismissBubblesByContext = new WeakMap();
|
|
143
|
+
/**
|
|
144
|
+
* Whether a child `context` lets the given event bubble past it to its ancestors. Defaults match Base
|
|
145
|
+
* UI for a child that has no dismissal policy (e.g. focus-only): Escape does **not** bubble (so it
|
|
146
|
+
* blocks), an outside press **does** (so it does not block).
|
|
147
|
+
*/
|
|
148
|
+
function childBubbles(context, reason) {
|
|
149
|
+
const bubbles = dismissBubblesByContext.get(context);
|
|
150
|
+
if (reason === 'escape-key') {
|
|
151
|
+
return bubbles ? bubbles.escapeKey() : false;
|
|
152
|
+
}
|
|
153
|
+
return bubbles ? bubbles.outsidePress() : true;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The **dismiss** mechanism (ADR 0015) — the Angular counterpart of Base UI's `useDismiss`. It
|
|
157
|
+
* **references** a {@link RdxFloatingRootContext} (mandatory: `open` / `triggers` / elements live there)
|
|
158
|
+
* and a {@link RdxFloatingNode} (**optional**: a node-optional / Navigation-Menu state has `node ===
|
|
159
|
+
* null`); it never creates them. It listens for Escape / outside-press / focus-out and **requests** a
|
|
160
|
+
* dismissal via `onDismiss` when an interaction lands outside the logical layer and this layer owns the
|
|
161
|
+
* event.
|
|
162
|
+
*
|
|
163
|
+
* **Logical, not DOM-order, containment.** "Inside" is resolved through the shared tree — this popup's
|
|
164
|
+
* floating element + its registered triggers, **plus** the same for every open descendant node, **plus**
|
|
165
|
+
* this layer's own {@link branches}. So a portal-relocated child still counts as inside its parent — which
|
|
166
|
+
* a DOM-order containment check cannot do.
|
|
167
|
+
*
|
|
168
|
+
* **Ownership & propagation (`hasBlockingChild`).** Each layer publishes per-event `bubbles` flags
|
|
169
|
+
* (`escapeKeyBubbles` default `false`, `outsidePressBubbles` default `true`); an ancestor reads its open
|
|
170
|
+
* children's flags via {@link childBubbles}. A non-bubbling layer **yields** to a non-bubbling open child,
|
|
171
|
+
* so by default Escape closes only the **deepest** layer while an outside press closes the **whole stack**.
|
|
172
|
+
* A layer with `escapeKeyBubbles = true` re-emits to its parent (Menu's `closeParentOnEsc`). The owning
|
|
173
|
+
* non-bubbling Escape layer also `stopPropagation()`s so the key does not reach app handlers.
|
|
174
|
+
*
|
|
175
|
+
* **Press / IME hardening.** Outside-press honors `outsidePressEvent` (`'sloppy'` → `pointerdown`,
|
|
176
|
+
* `'intentional'` → `click` with press-start-inside drag-out suppression), ignores non-primary mouse
|
|
177
|
+
* buttons and scrollbar presses, resets on `pointercancel`, and ignores Escape while an IME composition is
|
|
178
|
+
* active. **Touch** sloppy-mode gesture hardening (long-press / drag-distance thresholds) is ported here
|
|
179
|
+
* but only unit-exercisable via synthetic events — real on-device behavior is covered by
|
|
180
|
+
* `apps/visual-regression` (Playwright).
|
|
181
|
+
*
|
|
182
|
+
* **Scope.** Must be constructed in an injection context (`DestroyRef` / `PLATFORM_ID`). No-op on the
|
|
183
|
+
* server.
|
|
184
|
+
*/
|
|
185
|
+
class RdxDismiss {
|
|
186
|
+
constructor(context, node, config = {}) {
|
|
187
|
+
this.context = context;
|
|
188
|
+
this.node = node;
|
|
189
|
+
/** This layer's own inside-content set (branches that live outside the popup DOM). */
|
|
190
|
+
this.branches = new Set();
|
|
191
|
+
const enabled = config.enabled ?? alwaysTrue;
|
|
192
|
+
const escapeKey = config.escapeKey ?? alwaysTrue;
|
|
193
|
+
const escapeKeyBubbles = config.escapeKeyBubbles ?? alwaysFalse;
|
|
194
|
+
const outsidePress = config.outsidePress ?? alwaysTrue;
|
|
195
|
+
const outsidePressBubbles = config.outsidePressBubbles ?? alwaysTrue;
|
|
196
|
+
const outsidePressEvent = config.outsidePressEvent ?? sloppy;
|
|
197
|
+
const focusOutside = config.focusOutside ?? alwaysTrue;
|
|
198
|
+
// Plain function (not `computed`) so it re-reads `open()` per event even when `open` is not a
|
|
199
|
+
// signal — while still tracking signals when read inside a reactive context.
|
|
200
|
+
this.active = () => this.context.open() && enabled();
|
|
201
|
+
// Publish this layer's bubbles policy so an ANCESTOR's hasBlockingChild can read it. Registered
|
|
202
|
+
// (and cleaned up) regardless of platform so SSR leaves no stale entry.
|
|
203
|
+
dismissBubblesByContext.set(this.context, { escapeKey: escapeKeyBubbles, outsidePress: outsidePressBubbles });
|
|
204
|
+
const destroyRef = inject(DestroyRef);
|
|
205
|
+
destroyRef.onDestroy(() => dismissBubblesByContext.delete(this.context));
|
|
206
|
+
if (!isPlatformBrowser(inject(PLATFORM_ID))) {
|
|
207
|
+
return; // SSR: no listeners, no DOM access.
|
|
208
|
+
}
|
|
209
|
+
const ownerDocument = this.context.ownerDocument;
|
|
210
|
+
const ownerWindow = ownerDocument.defaultView ?? globalThis;
|
|
211
|
+
const dismiss = (reason, event) => {
|
|
212
|
+
config.onDismiss?.(reason, event);
|
|
213
|
+
};
|
|
214
|
+
// IME: a press of Escape while composing should close the compose menu, not the popup.
|
|
215
|
+
let isComposing = false;
|
|
216
|
+
// Press-start-inside tracking (drag-out suppression for `intentional` mode).
|
|
217
|
+
let pressStartedInside = false;
|
|
218
|
+
// Pointer type of the active press, used to resolve a per-pointer-type `outsidePressEvent` map.
|
|
219
|
+
let currentPointerType = '';
|
|
220
|
+
// Touch outside-press hardening (Base UI `useDismiss`). For touch, a `sloppy` outside-press is
|
|
221
|
+
// NOT decided on the initial `pointerdown` (that would dismiss the moment a finger lands, even on
|
|
222
|
+
// the start of a scroll): a small drag (>5px) arms a close on `touchend`, a larger one (>10px)
|
|
223
|
+
// closes immediately (the user is scrolling away), and a plain tap closes via the synthetic
|
|
224
|
+
// `mousedown` that follows — unless the finger is held past a 1s grace window (a long-press).
|
|
225
|
+
let touchState = null;
|
|
226
|
+
let touchGraceTimer;
|
|
227
|
+
let touchClearTimer;
|
|
228
|
+
const clearTouchTimers = () => {
|
|
229
|
+
ownerWindow.clearTimeout(touchGraceTimer);
|
|
230
|
+
ownerWindow.clearTimeout(touchClearTimer);
|
|
18
231
|
};
|
|
232
|
+
// Resolve `outsidePressEvent` to a concrete mode for the active pointer type (pen / unknown → mouse).
|
|
233
|
+
const resolveOutsidePressEvent = () => {
|
|
234
|
+
const value = outsidePressEvent();
|
|
235
|
+
if (typeof value === 'string') {
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
const type = currentPointerType === 'pen' || currentPointerType === '' ? 'mouse' : currentPointerType;
|
|
239
|
+
return value[type] ?? 'sloppy';
|
|
240
|
+
};
|
|
241
|
+
const handleKeyDown = (event) => {
|
|
242
|
+
if (event.key !== 'Escape' || !this.active() || !escapeKey() || isComposing) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// A non-bubbling layer yields to a deeper non-bubbling open layer (Base UI:
|
|
246
|
+
// `!escapeKeyBubbles && hasBlockingChild`). A bubbling layer never yields.
|
|
247
|
+
if (!escapeKeyBubbles() && this.hasBlockingChild('escape-key')) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
config.onEscapeKeyDown?.(event);
|
|
251
|
+
if (!event.defaultPrevented) {
|
|
252
|
+
dismiss('escape-key', event);
|
|
253
|
+
}
|
|
254
|
+
// Propagation control: the owning non-bubbling layer consumes the Escape so it does not
|
|
255
|
+
// reach ancestor layers / app-level handlers (Base UI `event.stopPropagation()`).
|
|
256
|
+
if (!escapeKeyBubbles()) {
|
|
257
|
+
event.stopPropagation();
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const handleCompositionStart = () => {
|
|
261
|
+
isComposing = true;
|
|
262
|
+
};
|
|
263
|
+
const handleCompositionEnd = () => {
|
|
264
|
+
// Safari fires `compositionend` before `keydown`, so clear on a later tick. 0ms/1ms are
|
|
265
|
+
// unreliable in Safari — WebKit needs ~5ms; other engines stay at 0ms (Base UI `useDismiss`).
|
|
266
|
+
ownerWindow.setTimeout(() => {
|
|
267
|
+
isComposing = false;
|
|
268
|
+
}, isWebKit(ownerWindow) ? 5 : 0);
|
|
269
|
+
};
|
|
270
|
+
const tryOutsidePress = (event) => {
|
|
271
|
+
if (!this.active() || !isPrimaryButton(event) || this.isInside(event.target) || !outsidePress(event)) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (isInjectedAfterMark(event.target, this.context)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (isScrollbarPress(event)) {
|
|
278
|
+
return; // a scrollbar drag is not an outside press
|
|
279
|
+
}
|
|
280
|
+
// By default outside-press bubbles (closes the whole stack); only a non-bubbling layer with
|
|
281
|
+
// a non-bubbling open child yields to it.
|
|
282
|
+
if (!outsidePressBubbles() && this.hasBlockingChild('outside-press')) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
config.onPointerDownOutside?.(event);
|
|
286
|
+
if (!event.defaultPrevented) {
|
|
287
|
+
dismiss('outside-press', event);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
// Capture-phase, so it records where the press began (and its pointer type) before any dismiss
|
|
291
|
+
// handler runs.
|
|
292
|
+
const handlePressStart = (event) => {
|
|
293
|
+
pressStartedInside = this.isInside(event.target);
|
|
294
|
+
currentPointerType = event.pointerType || '';
|
|
295
|
+
};
|
|
296
|
+
const handlePointerCancel = () => {
|
|
297
|
+
pressStartedInside = false;
|
|
298
|
+
};
|
|
299
|
+
const handlePointerDown = (event) => {
|
|
300
|
+
if (resolveOutsidePressEvent() !== 'sloppy') {
|
|
301
|
+
return; // `intentional` dismisses on click, not pointerdown
|
|
302
|
+
}
|
|
303
|
+
if (event.pointerType === 'touch') {
|
|
304
|
+
return; // touch is decided by the touchstart/move/end + synthetic-mousedown machine below
|
|
305
|
+
}
|
|
306
|
+
tryOutsidePress(event);
|
|
307
|
+
};
|
|
308
|
+
const handleClick = (event) => {
|
|
309
|
+
if (resolveOutsidePressEvent() !== 'intentional') {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// A press that started inside (text selection dragged out) consumes its one outside click.
|
|
313
|
+
if (pressStartedInside) {
|
|
314
|
+
pressStartedInside = false;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
tryOutsidePress(event);
|
|
318
|
+
};
|
|
319
|
+
// ─── Touch sloppy-mode hardening (Base UI `useDismiss`) ───────────────────
|
|
320
|
+
const handleTouchStart = (event) => {
|
|
321
|
+
currentPointerType = 'touch'; // a touch's pointer type, independent of pointerdown ordering
|
|
322
|
+
if (resolveOutsidePressEvent() !== 'sloppy' || !this.active() || this.isInside(event.target)) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const touch = event.touches?.[0];
|
|
326
|
+
if (!touch) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
clearTouchTimers();
|
|
330
|
+
touchState = {
|
|
331
|
+
startX: touch.clientX,
|
|
332
|
+
startY: touch.clientY,
|
|
333
|
+
dismissOnTouchEnd: false,
|
|
334
|
+
dismissOnMouseDown: true
|
|
335
|
+
};
|
|
336
|
+
// After 1s the press is a long-press, not a dismissal — disarm both the touchend and the
|
|
337
|
+
// synthetic-mousedown close.
|
|
338
|
+
touchGraceTimer = ownerWindow.setTimeout(() => {
|
|
339
|
+
if (touchState) {
|
|
340
|
+
touchState.dismissOnTouchEnd = false;
|
|
341
|
+
touchState.dismissOnMouseDown = false;
|
|
342
|
+
}
|
|
343
|
+
}, 1000);
|
|
344
|
+
};
|
|
345
|
+
const handleTouchMove = (event) => {
|
|
346
|
+
if (!touchState || resolveOutsidePressEvent() !== 'sloppy' || this.isInside(event.target)) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const touch = event.touches?.[0];
|
|
350
|
+
if (!touch) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const dx = touch.clientX - touchState.startX;
|
|
354
|
+
const dy = touch.clientY - touchState.startY;
|
|
355
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
356
|
+
if (distance > 5) {
|
|
357
|
+
touchState.dismissOnTouchEnd = true; // a deliberate drag — close when the finger lifts
|
|
358
|
+
}
|
|
359
|
+
if (distance > 10) {
|
|
360
|
+
tryOutsidePress(event); // scrolling away — close now
|
|
361
|
+
clearTouchTimers();
|
|
362
|
+
touchState = null;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const handleTouchEnd = (event) => {
|
|
366
|
+
if (!touchState || resolveOutsidePressEvent() !== 'sloppy' || this.isInside(event.target)) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (touchState.dismissOnTouchEnd) {
|
|
370
|
+
tryOutsidePress(event);
|
|
371
|
+
clearTouchTimers();
|
|
372
|
+
touchState = null;
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
// A plain tap: defer to the synthetic `mousedown` (which also lets us absorb the click-through),
|
|
376
|
+
// but drop the state shortly after in case no mouse event follows on this platform.
|
|
377
|
+
ownerWindow.clearTimeout(touchClearTimer);
|
|
378
|
+
touchClearTimer = ownerWindow.setTimeout(() => {
|
|
379
|
+
touchState = null;
|
|
380
|
+
}, 400);
|
|
381
|
+
};
|
|
382
|
+
const handleMouseDown = (event) => {
|
|
383
|
+
// Only the synthetic `mousedown` that follows a touch is handled here — a real mouse leaves
|
|
384
|
+
// `touchState` null and is dismissed by `handlePointerDown` instead (no double-close).
|
|
385
|
+
if (!touchState || resolveOutsidePressEvent() !== 'sloppy') {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (!touchState.dismissOnMouseDown) {
|
|
389
|
+
touchState = null; // grace expired (long-press) — do not dismiss
|
|
390
|
+
clearTouchTimers();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
tryOutsidePress(event);
|
|
394
|
+
clearTouchTimers();
|
|
395
|
+
touchState = null;
|
|
396
|
+
};
|
|
397
|
+
const handleFocusIn = (event) => {
|
|
398
|
+
// Defer two microtasks so focus settles before reading containment (matches the standalone
|
|
399
|
+
// RdxFocusOutside; the `on*` hook still runs synchronously, so `preventDefault` is honored).
|
|
400
|
+
void Promise.resolve()
|
|
401
|
+
.then(() => Promise.resolve())
|
|
402
|
+
.then(() => {
|
|
403
|
+
if (!this.active() || !focusOutside() || this.isInside(event.target)) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
config.onFocusOutside?.(event);
|
|
407
|
+
if (!event.defaultPrevented) {
|
|
408
|
+
dismiss('focus-outside', event);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
};
|
|
412
|
+
ownerDocument.addEventListener('keydown', handleKeyDown, { capture: true });
|
|
413
|
+
ownerDocument.addEventListener('compositionstart', handleCompositionStart, { capture: true });
|
|
414
|
+
ownerDocument.addEventListener('compositionend', handleCompositionEnd, { capture: true });
|
|
415
|
+
ownerDocument.addEventListener('focusin', handleFocusIn);
|
|
416
|
+
ownerDocument.addEventListener('pointerdown', handlePressStart, { capture: true });
|
|
417
|
+
ownerDocument.addEventListener('pointercancel', handlePointerCancel, { capture: true });
|
|
418
|
+
// Defer attaching the dismiss listeners past the current event loop so the very interaction that
|
|
419
|
+
// opened this layer doesn't immediately dismiss it (the opening press is still propagating).
|
|
420
|
+
const pointerTimer = ownerWindow.setTimeout(() => {
|
|
421
|
+
ownerDocument.addEventListener('pointerdown', handlePointerDown);
|
|
422
|
+
ownerDocument.addEventListener('click', handleClick);
|
|
423
|
+
ownerDocument.addEventListener('touchstart', handleTouchStart, { capture: true, passive: true });
|
|
424
|
+
ownerDocument.addEventListener('touchmove', handleTouchMove, { capture: true, passive: true });
|
|
425
|
+
ownerDocument.addEventListener('touchend', handleTouchEnd, { capture: true, passive: true });
|
|
426
|
+
ownerDocument.addEventListener('mousedown', handleMouseDown, { capture: true });
|
|
427
|
+
}, 0);
|
|
428
|
+
destroyRef.onDestroy(() => {
|
|
429
|
+
ownerWindow.clearTimeout(pointerTimer);
|
|
430
|
+
ownerDocument.removeEventListener('keydown', handleKeyDown, { capture: true });
|
|
431
|
+
ownerDocument.removeEventListener('compositionstart', handleCompositionStart, { capture: true });
|
|
432
|
+
ownerDocument.removeEventListener('compositionend', handleCompositionEnd, { capture: true });
|
|
433
|
+
ownerDocument.removeEventListener('focusin', handleFocusIn);
|
|
434
|
+
ownerDocument.removeEventListener('pointerdown', handlePressStart, { capture: true });
|
|
435
|
+
ownerDocument.removeEventListener('pointercancel', handlePointerCancel, { capture: true });
|
|
436
|
+
ownerDocument.removeEventListener('pointerdown', handlePointerDown);
|
|
437
|
+
ownerDocument.removeEventListener('click', handleClick);
|
|
438
|
+
ownerDocument.removeEventListener('touchstart', handleTouchStart, { capture: true });
|
|
439
|
+
ownerDocument.removeEventListener('touchmove', handleTouchMove, { capture: true });
|
|
440
|
+
ownerDocument.removeEventListener('touchend', handleTouchEnd, { capture: true });
|
|
441
|
+
ownerDocument.removeEventListener('mousedown', handleMouseDown, { capture: true });
|
|
442
|
+
clearTouchTimers();
|
|
443
|
+
});
|
|
19
444
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
445
|
+
/**
|
|
446
|
+
* Whether `target` is logically inside this layer: within this popup's floating element or one of its
|
|
447
|
+
* registered triggers, within any **open descendant** node's floating element / triggers, or within a
|
|
448
|
+
* registered branch.
|
|
449
|
+
*/
|
|
450
|
+
isInside(target) {
|
|
451
|
+
if (this.contextContains(this.context, target)) {
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
const node = this.node();
|
|
455
|
+
if (node) {
|
|
456
|
+
for (const child of node.tree.children(node, { onlyOpen: true })) {
|
|
457
|
+
if (child.context && this.contextContains(child.context, target)) {
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (isNode(target)) {
|
|
463
|
+
for (const branch of this.branches) {
|
|
464
|
+
if (branch.contains(target)) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
/** `target` is inside a context's floating element or one of its registered triggers. */
|
|
472
|
+
contextContains(context, target) {
|
|
473
|
+
const floating = context.floatingElement;
|
|
474
|
+
if (floating && isNode(target) && floating.contains(target)) {
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
return context.triggers.contains(target);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Whether an open descendant blocks this layer for `reason` (so it should yield). A child blocks when
|
|
481
|
+
* it does **not** let the event bubble past it ({@link childBubbles} is `false`). Base UI's
|
|
482
|
+
* `hasBlockingChild` (`useDismiss.ts:170`) is a capability-local function — **not** a tree method — so
|
|
483
|
+
* the tree stays neutral.
|
|
484
|
+
*/
|
|
485
|
+
hasBlockingChild(reason) {
|
|
486
|
+
const node = this.node();
|
|
487
|
+
if (!node) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
return node.tree
|
|
491
|
+
.children(node, { onlyOpen: true })
|
|
492
|
+
.some((child) => child.context !== null && !childBubbles(child.context, reason));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Marks its host as **inside** the enclosing floating layer (ADR 0015): a pointer / focus interaction on
|
|
498
|
+
* it does not count as "outside", so it never dismisses the popup. It registers the host into the
|
|
499
|
+
* floating root context's trigger registry — {@link RdxDismiss}'s `isInside` check reads it.
|
|
500
|
+
*
|
|
501
|
+
* Use it for an element that lives **outside** the popup DOM but logically belongs to the layer — e.g. a
|
|
502
|
+
* Combobox input / trigger / chips / clear button that keeps focus (or is clicked) while the listbox is
|
|
503
|
+
* open.
|
|
504
|
+
*/
|
|
505
|
+
class RdxFloatingInsideElement {
|
|
506
|
+
constructor() {
|
|
507
|
+
const context = inject(RDX_FLOATING_ROOT_CONTEXT, { optional: true });
|
|
508
|
+
if (!context) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const host = inject(ElementRef).nativeElement;
|
|
512
|
+
context.triggers.add(host);
|
|
513
|
+
inject(DestroyRef).onDestroy(() => context.triggers.delete(host));
|
|
514
|
+
}
|
|
515
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxFloatingInsideElement, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
516
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxFloatingInsideElement, isStandalone: true, selector: "[rdxFloatingInside]", exportAs: ["rdxFloatingInside"], ngImport: i0 }); }
|
|
23
517
|
}
|
|
518
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxFloatingInsideElement, decorators: [{
|
|
519
|
+
type: Directive,
|
|
520
|
+
args: [{
|
|
521
|
+
selector: '[rdxFloatingInside]',
|
|
522
|
+
exportAs: 'rdxFloatingInside'
|
|
523
|
+
}]
|
|
524
|
+
}], ctorParameters: () => [] });
|
|
24
525
|
|
|
25
526
|
function isLayerExist(layerElement, targetElement) {
|
|
26
527
|
const targetLayer = targetElement.closest('[data-dismissable-layer]');
|
|
@@ -266,175 +767,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
266
767
|
}]
|
|
267
768
|
}], ctorParameters: () => [], propDecorators: { escapeKeyDown: [{ type: i0.Output, args: ["escapeKeyDown"] }] } });
|
|
268
769
|
|
|
269
|
-
/**
|
|
270
|
-
* Shared across all layers. Holds the body's original `pointer-events` value while at least one
|
|
271
|
-
* layer disables outside pointer events. `null` means the body is not currently overridden — it
|
|
272
|
-
* doubles as the "ownership" flag so stacked layers don't overwrite the saved original with
|
|
273
|
-
* `none`.
|
|
274
|
-
*/
|
|
275
|
-
let originalBodyPointerEvents = null;
|
|
276
|
-
class RdxDismissableLayer {
|
|
277
|
-
constructor() {
|
|
278
|
-
this.elementRef = inject(ElementRef);
|
|
279
|
-
this.injector = inject(Injector);
|
|
280
|
-
this.destroyRef = inject(DestroyRef);
|
|
281
|
-
this.context = inject(RdxDismissableLayersContextToken);
|
|
282
|
-
this.config = inject(RdxDismissableLayerConfigToken);
|
|
283
|
-
this.rdxPointerDownOutside = inject(RdxPointerDownOutside);
|
|
284
|
-
this.rdxFocusOutside = inject(RdxFocusOutside);
|
|
285
|
-
this.rdxEscapeKeyDown = inject(RdxEscapeKeyDown);
|
|
286
|
-
/**
|
|
287
|
-
* Event handler called when the escape key is down.
|
|
288
|
-
* Can be prevented.
|
|
289
|
-
*/
|
|
290
|
-
this.escapeKeyDown = output();
|
|
291
|
-
/**
|
|
292
|
-
* Event handler called when a `pointerdown` event happens outside of the `DismissableLayer`.
|
|
293
|
-
* Can be prevented.
|
|
294
|
-
*/
|
|
295
|
-
this.pointerDownOutside = output();
|
|
296
|
-
/**
|
|
297
|
-
* Event handler called when the focus moves outside of the `DismissableLayer`.
|
|
298
|
-
* Can be prevented.
|
|
299
|
-
*/
|
|
300
|
-
this.focusOutside = output();
|
|
301
|
-
/**
|
|
302
|
-
* Event handler called when an interaction happens outside the `DismissableLayer`.
|
|
303
|
-
* Specifically, when a `pointerdown` event happens outside or focus moves outside of it.
|
|
304
|
-
* Can be prevented.
|
|
305
|
-
*/
|
|
306
|
-
this.interactOutside = output();
|
|
307
|
-
/**
|
|
308
|
-
* Handler called when the `DismissableLayer` should be dismissed
|
|
309
|
-
*/
|
|
310
|
-
this.dismiss = output();
|
|
311
|
-
/**
|
|
312
|
-
* When `true`, hover/focus/click interactions will be disabled on elements outside
|
|
313
|
-
* the `DismissableLayer`. Users will need to click twice on outside elements to
|
|
314
|
-
* interact with them: once to close the `DismissableLayer`, and again to trigger the element.
|
|
315
|
-
*/
|
|
316
|
-
this.disableOutsidePointerEvents = input(undefined, { ...(ngDevMode ? { debugName: "disableOutsidePointerEvents" } : /* istanbul ignore next */ {}), transform: (value) => (value === undefined ? undefined : booleanAttribute(value)) });
|
|
317
|
-
this.isOutsidePointerEventsDisabled = computed(() => this.disableOutsidePointerEvents() ?? this.config.disableOutsidePointerEvents(), ...(ngDevMode ? [{ debugName: "isOutsidePointerEventsDisabled" }] : /* istanbul ignore next */ []));
|
|
318
|
-
this.isBodyPointerEventsDisabled = computed(() => this.context.layersWithOutsidePointerEventsDisabled().length > 0, ...(ngDevMode ? [{ debugName: "isBodyPointerEventsDisabled" }] : /* istanbul ignore next */ []));
|
|
319
|
-
this.isPointerEventsEnabled = computed(() => {
|
|
320
|
-
const layers = this.context.layersRoot();
|
|
321
|
-
const disabledLayers = this.context.layersWithOutsidePointerEventsDisabled();
|
|
322
|
-
const highestDisabledLayer = disabledLayers[disabledLayers.length - 1];
|
|
323
|
-
return this.index() >= layers.indexOf(highestDisabledLayer);
|
|
324
|
-
}, ...(ngDevMode ? [{ debugName: "isPointerEventsEnabled" }] : /* istanbul ignore next */ []));
|
|
325
|
-
this.index = computed(() => this.context.layersRoot().indexOf(this), ...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
|
|
326
|
-
/** The topmost layer in the stack — the only one that should react to the Escape key. */
|
|
327
|
-
this.isHighestLayer = computed(() => {
|
|
328
|
-
const layers = this.context.layersRoot();
|
|
329
|
-
return layers.indexOf(this) === layers.length - 1;
|
|
330
|
-
}, ...(ngDevMode ? [{ debugName: "isHighestLayer" }] : /* istanbul ignore next */ []));
|
|
331
|
-
this.context.layersRoot.update((v) => [...v, this]);
|
|
332
|
-
this.destroyRef.onDestroy(() => {
|
|
333
|
-
this.context.layersRoot.update((v) => v.filter((i) => i !== this));
|
|
334
|
-
});
|
|
335
|
-
this.setupBodyPointerEvents();
|
|
336
|
-
this.rdxPointerDownOutside.pointerDownOutside.subscribe((event) => {
|
|
337
|
-
const isPointerDownOnBranch = this.context
|
|
338
|
-
.branches()
|
|
339
|
-
.some((branch) => branch.contains(event.target));
|
|
340
|
-
if (!this.isPointerEventsEnabled() || isPointerDownOnBranch) {
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
this.pointerDownOutside.emit(event);
|
|
344
|
-
this.interactOutside.emit(event);
|
|
345
|
-
if (!event.defaultPrevented) {
|
|
346
|
-
this.dismiss.emit();
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
this.rdxFocusOutside.focusOutside.subscribe((event) => {
|
|
350
|
-
const isFocusInBranch = this.context
|
|
351
|
-
.branches()
|
|
352
|
-
.some((branch) => branch.contains(event.target));
|
|
353
|
-
if (isFocusInBranch) {
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
this.focusOutside.emit(event);
|
|
357
|
-
this.interactOutside.emit(event);
|
|
358
|
-
if (!event.defaultPrevented) {
|
|
359
|
-
this.dismiss.emit();
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
this.rdxEscapeKeyDown.escapeKeyDown.subscribe((event) => {
|
|
363
|
-
// Only the topmost layer is dismissed by Escape; stacked layers close one at a time.
|
|
364
|
-
if (!this.isHighestLayer()) {
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
this.escapeKeyDown.emit(event);
|
|
368
|
-
if (!event.defaultPrevented) {
|
|
369
|
-
event.preventDefault();
|
|
370
|
-
this.dismiss.emit();
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Toggles `pointer-events: none` on the document body while any layer has
|
|
376
|
-
* `disableOutsidePointerEvents`. Ownership is shared across all layers via
|
|
377
|
-
* {@link originalBodyPointerEvents}: the original value is saved only on the global
|
|
378
|
-
* `0 -> >0` transition and restored only when the count returns to `0`.
|
|
379
|
-
*/
|
|
380
|
-
setupBodyPointerEvents() {
|
|
381
|
-
afterNextRender(() => {
|
|
382
|
-
const ownerDocument = this.elementRef.nativeElement.ownerDocument ?? globalThis.document;
|
|
383
|
-
effect((onCleanup) => {
|
|
384
|
-
const disabledCount = this.context.layersWithOutsidePointerEventsDisabled().length;
|
|
385
|
-
if (disabledCount > 0 && originalBodyPointerEvents === null) {
|
|
386
|
-
originalBodyPointerEvents = ownerDocument.body.style.pointerEvents;
|
|
387
|
-
ownerDocument.body.style.pointerEvents = 'none';
|
|
388
|
-
}
|
|
389
|
-
onCleanup(() => {
|
|
390
|
-
const remaining = untracked(() => this.context.layersWithOutsidePointerEventsDisabled().length);
|
|
391
|
-
if (remaining === 0 && originalBodyPointerEvents !== null) {
|
|
392
|
-
ownerDocument.body.style.pointerEvents = originalBodyPointerEvents;
|
|
393
|
-
originalBodyPointerEvents = null;
|
|
394
|
-
}
|
|
395
|
-
});
|
|
396
|
-
}, { injector: this.injector });
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDismissableLayer, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
400
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxDismissableLayer, isStandalone: true, selector: "[rdxDismissableLayer]", inputs: { disableOutsidePointerEvents: { classPropertyName: "disableOutsidePointerEvents", publicName: "disableOutsidePointerEvents", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { escapeKeyDown: "escapeKeyDown", pointerDownOutside: "pointerDownOutside", focusOutside: "focusOutside", interactOutside: "interactOutside", dismiss: "dismiss" }, host: { attributes: { "data-dismissable-layer": "" }, properties: { "style": "{\n pointerEvents: isBodyPointerEventsDisabled() ? (isPointerEventsEnabled() ? 'auto' : 'none') : undefined\n }" } }, exportAs: ["rdxDismissableLayer"], hostDirectives: [{ directive: RdxPointerDownOutside }, { directive: RdxFocusOutside }, { directive: RdxEscapeKeyDown }], ngImport: i0 }); }
|
|
401
|
-
}
|
|
402
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDismissableLayer, decorators: [{
|
|
403
|
-
type: Directive,
|
|
404
|
-
args: [{
|
|
405
|
-
selector: '[rdxDismissableLayer]',
|
|
406
|
-
exportAs: 'rdxDismissableLayer',
|
|
407
|
-
hostDirectives: [RdxPointerDownOutside, RdxFocusOutside, RdxEscapeKeyDown],
|
|
408
|
-
host: {
|
|
409
|
-
'data-dismissable-layer': '',
|
|
410
|
-
'[style]': `{
|
|
411
|
-
pointerEvents: isBodyPointerEventsDisabled() ? (isPointerEventsEnabled() ? 'auto' : 'none') : undefined
|
|
412
|
-
}`
|
|
413
|
-
}
|
|
414
|
-
}]
|
|
415
|
-
}], ctorParameters: () => [], propDecorators: { escapeKeyDown: [{ type: i0.Output, args: ["escapeKeyDown"] }], pointerDownOutside: [{ type: i0.Output, args: ["pointerDownOutside"] }], focusOutside: [{ type: i0.Output, args: ["focusOutside"] }], interactOutside: [{ type: i0.Output, args: ["interactOutside"] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }], disableOutsidePointerEvents: [{ type: i0.Input, args: [{ isSignal: true, alias: "disableOutsidePointerEvents", required: false }] }] } });
|
|
416
|
-
|
|
417
|
-
class RdxDismissableLayerBranch {
|
|
418
|
-
constructor() {
|
|
419
|
-
this.elementRef = inject(ElementRef);
|
|
420
|
-
this.destroyRef = inject(DestroyRef);
|
|
421
|
-
this.dismissableLayersContext = inject(RdxDismissableLayersContextToken);
|
|
422
|
-
this.dismissableLayersContext.branches.update((elements) => [...elements, this.elementRef.nativeElement]);
|
|
423
|
-
this.destroyRef.onDestroy(() => this.dismissableLayersContext.branches.update((elements) => elements.filter((el) => el !== this.elementRef.nativeElement)));
|
|
424
|
-
}
|
|
425
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDismissableLayerBranch, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
426
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxDismissableLayerBranch, isStandalone: true, selector: "[rdxDismissableLayerBranch]", ngImport: i0 }); }
|
|
427
|
-
}
|
|
428
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDismissableLayerBranch, decorators: [{
|
|
429
|
-
type: Directive,
|
|
430
|
-
args: [{
|
|
431
|
-
selector: '[rdxDismissableLayerBranch]'
|
|
432
|
-
}]
|
|
433
|
-
}], ctorParameters: () => [] });
|
|
434
|
-
|
|
435
770
|
/**
|
|
436
771
|
* Generated bundle index. Do not edit.
|
|
437
772
|
*/
|
|
438
773
|
|
|
439
|
-
export {
|
|
774
|
+
export { RdxDismiss, RdxEscapeKeyDown, RdxFloatingInsideElement, RdxFocusOutside, RdxPointerDownOutside };
|
|
440
775
|
//# sourceMappingURL=radix-ng-primitives-dismissable-layer.mjs.map
|