forty-cdk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/accordion/README.md +122 -0
- package/aspect-ratio/README.md +76 -0
- package/avatar/README.md +100 -0
- package/breadcrumbs/README.md +49 -0
- package/breakpoints/README.md +81 -0
- package/button/README.md +49 -0
- package/calendar/README.md +458 -0
- package/carousel/README.md +358 -0
- package/checkbox/README.md +146 -0
- package/combobox/README.md +535 -0
- package/context-menu/README.md +139 -0
- package/date-field/README.md +184 -0
- package/date-picker/README.md +338 -0
- package/dialog/README.md +388 -0
- package/disclosure/README.md +114 -0
- package/drag-drop/README.md +359 -0
- package/drawer/README.md +560 -0
- package/dropdown-menu/README.md +176 -0
- package/fesm2022/forty-cdk-accordion.mjs +348 -0
- package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
- package/fesm2022/forty-cdk-avatar.mjs +308 -0
- package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
- package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
- package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
- package/fesm2022/forty-cdk-button.mjs +134 -0
- package/fesm2022/forty-cdk-button.mjs.map +1 -0
- package/fesm2022/forty-cdk-calendar.mjs +2034 -0
- package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
- package/fesm2022/forty-cdk-carousel.mjs +968 -0
- package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
- package/fesm2022/forty-cdk-checkbox.mjs +226 -0
- package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-combobox.mjs +2596 -0
- package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
- package/fesm2022/forty-cdk-context-menu.mjs +413 -0
- package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-core.mjs +9022 -0
- package/fesm2022/forty-cdk-core.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-field.mjs +744 -0
- package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
- package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-dialog.mjs +707 -0
- package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
- package/fesm2022/forty-cdk-disclosure.mjs +190 -0
- package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
- package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
- package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
- package/fesm2022/forty-cdk-drawer.mjs +1641 -0
- package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-field.mjs +425 -0
- package/fesm2022/forty-cdk-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-fieldset.mjs +164 -0
- package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
- package/fesm2022/forty-cdk-file-upload.mjs +221 -0
- package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
- package/fesm2022/forty-cdk-hover-card.mjs +496 -0
- package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
- package/fesm2022/forty-cdk-input.mjs +274 -0
- package/fesm2022/forty-cdk-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
- package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
- package/fesm2022/forty-cdk-listbox.mjs +1279 -0
- package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-menu.mjs +1439 -0
- package/fesm2022/forty-cdk-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-menubar.mjs +787 -0
- package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
- package/fesm2022/forty-cdk-meter.mjs +211 -0
- package/fesm2022/forty-cdk-meter.mjs.map +1 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-number-input.mjs +559 -0
- package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-otp-input.mjs +527 -0
- package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-pagination.mjs +323 -0
- package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
- package/fesm2022/forty-cdk-popover.mjs +698 -0
- package/fesm2022/forty-cdk-popover.mjs.map +1 -0
- package/fesm2022/forty-cdk-progress.mjs +226 -0
- package/fesm2022/forty-cdk-progress.mjs.map +1 -0
- package/fesm2022/forty-cdk-radio-group.mjs +378 -0
- package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
- package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
- package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
- package/fesm2022/forty-cdk-search.mjs +205 -0
- package/fesm2022/forty-cdk-search.mjs.map +1 -0
- package/fesm2022/forty-cdk-select.mjs +1661 -0
- package/fesm2022/forty-cdk-select.mjs.map +1 -0
- package/fesm2022/forty-cdk-separator.mjs +82 -0
- package/fesm2022/forty-cdk-separator.mjs.map +1 -0
- package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
- package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
- package/fesm2022/forty-cdk-slider.mjs +803 -0
- package/fesm2022/forty-cdk-slider.mjs.map +1 -0
- package/fesm2022/forty-cdk-stepper.mjs +886 -0
- package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
- package/fesm2022/forty-cdk-switch.mjs +137 -0
- package/fesm2022/forty-cdk-switch.mjs.map +1 -0
- package/fesm2022/forty-cdk-table.mjs +1518 -0
- package/fesm2022/forty-cdk-table.mjs.map +1 -0
- package/fesm2022/forty-cdk-tabs.mjs +400 -0
- package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-field.mjs +593 -0
- package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
- package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-toast.mjs +1153 -0
- package/fesm2022/forty-cdk-toast.mjs.map +1 -0
- package/fesm2022/forty-cdk-toggle.mjs +516 -0
- package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
- package/fesm2022/forty-cdk-toolbar.mjs +374 -0
- package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
- package/fesm2022/forty-cdk-tooltip.mjs +672 -0
- package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
- package/fesm2022/forty-cdk-tree.mjs +2007 -0
- package/fesm2022/forty-cdk-tree.mjs.map +1 -0
- package/fesm2022/forty-cdk-virtualization.mjs +1 -1
- package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
- package/fesm2022/forty-cdk.mjs +0 -43310
- package/fesm2022/forty-cdk.mjs.map +1 -1
- package/field/README.md +97 -0
- package/fieldset/README.md +86 -0
- package/file-upload/README.md +73 -0
- package/hover-card/README.md +171 -0
- package/input/README.md +156 -0
- package/listbox/README.md +424 -0
- package/menu/README.md +181 -0
- package/menubar/README.md +140 -0
- package/meter/README.md +128 -0
- package/navigation-menu/README.md +253 -0
- package/number-input/README.md +171 -0
- package/otp-input/README.md +198 -0
- package/package.json +213 -1
- package/pagination/README.md +61 -0
- package/pane-resizer/README.md +136 -0
- package/popover/README.md +262 -0
- package/progress/README.md +115 -0
- package/radio-group/README.md +129 -0
- package/scroll-area/README.md +184 -0
- package/search/README.md +42 -0
- package/select/README.md +488 -0
- package/separator/README.md +84 -0
- package/signal-forms/README.md +72 -0
- package/slider/README.md +152 -0
- package/stepper/README.md +292 -0
- package/switch/README.md +116 -0
- package/table/README.md +769 -0
- package/tabs/README.md +130 -0
- package/time-field/README.md +157 -0
- package/time-picker/README.md +172 -0
- package/toast/README.md +398 -0
- package/toggle/README.md +224 -0
- package/toolbar/README.md +109 -0
- package/tooltip/README.md +274 -0
- package/tree/README.md +708 -0
- package/types/forty-cdk-accordion.d.ts +242 -0
- package/types/forty-cdk-aspect-ratio.d.ts +59 -0
- package/types/forty-cdk-avatar.d.ts +133 -0
- package/types/forty-cdk-breadcrumbs.d.ts +92 -0
- package/types/forty-cdk-breakpoints.d.ts +141 -0
- package/types/forty-cdk-button.d.ts +80 -0
- package/types/forty-cdk-calendar.d.ts +914 -0
- package/types/forty-cdk-carousel.d.ts +530 -0
- package/types/forty-cdk-checkbox.d.ts +141 -0
- package/types/forty-cdk-combobox.d.ts +1259 -0
- package/types/forty-cdk-context-menu.d.ts +313 -0
- package/types/forty-cdk-core.d.ts +5774 -0
- package/types/forty-cdk-date-field.d.ts +307 -0
- package/types/forty-cdk-date-picker.d.ts +622 -0
- package/types/forty-cdk-dialog.d.ts +546 -0
- package/types/forty-cdk-disclosure.d.ts +127 -0
- package/types/forty-cdk-drag-drop.d.ts +456 -0
- package/types/forty-cdk-drawer.d.ts +871 -0
- package/types/forty-cdk-dropdown-menu.d.ts +242 -0
- package/types/forty-cdk-field.d.ts +236 -0
- package/types/forty-cdk-fieldset.d.ts +119 -0
- package/types/forty-cdk-file-upload.d.ts +124 -0
- package/types/forty-cdk-hover-card.d.ts +320 -0
- package/types/forty-cdk-input.d.ts +169 -0
- package/types/forty-cdk-internationalized-date.d.ts +1 -1
- package/types/forty-cdk-listbox.d.ts +513 -0
- package/types/forty-cdk-menu.d.ts +629 -0
- package/types/forty-cdk-menubar.d.ts +451 -0
- package/types/forty-cdk-meter.d.ts +122 -0
- package/types/forty-cdk-navigation-menu.d.ts +514 -0
- package/types/forty-cdk-number-input.d.ts +319 -0
- package/types/forty-cdk-otp-input.d.ts +248 -0
- package/types/forty-cdk-pagination.d.ts +214 -0
- package/types/forty-cdk-pane-resizer.d.ts +145 -0
- package/types/forty-cdk-popover.d.ts +509 -0
- package/types/forty-cdk-progress.d.ts +143 -0
- package/types/forty-cdk-radio-group.d.ts +222 -0
- package/types/forty-cdk-scroll-area.d.ts +258 -0
- package/types/forty-cdk-search.d.ts +142 -0
- package/types/forty-cdk-select.d.ts +899 -0
- package/types/forty-cdk-separator.d.ts +59 -0
- package/types/forty-cdk-signal-forms.d.ts +58 -0
- package/types/forty-cdk-slider.d.ts +379 -0
- package/types/forty-cdk-stepper.d.ts +650 -0
- package/types/forty-cdk-switch.d.ts +87 -0
- package/types/forty-cdk-table.d.ts +723 -0
- package/types/forty-cdk-tabs.d.ts +235 -0
- package/types/forty-cdk-time-field.d.ts +307 -0
- package/types/forty-cdk-time-picker.d.ts +578 -0
- package/types/forty-cdk-toast.d.ts +598 -0
- package/types/forty-cdk-toggle.d.ts +310 -0
- package/types/forty-cdk-toolbar.d.ts +217 -0
- package/types/forty-cdk-tooltip.d.ts +436 -0
- package/types/forty-cdk-tree.d.ts +688 -0
- package/types/forty-cdk.d.ts +1 -19743
|
@@ -0,0 +1,1641 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, ElementRef, ErrorHandler, signal, computed, effect, DestroyRef, input, booleanAttribute, model, output, afterNextRender, Directive, ChangeDetectionStrategy, Component, createComponent, Injectable } from '@angular/core';
|
|
3
|
+
import { createDefaults, injectPrefersReducedMotion, isScrollableAtEdge, resolveSnapTarget, attachSwipeDismiss, ForDrawerScaleCoordinator, ForDrawerStack, injectModalShell, reflectDisabled, injectPortal, registerA11yName, registerA11yDescription, OverlayRef, OverlayContextInjector, OverlayManagerCore, resolveConfigClass } from 'forty-cdk/core';
|
|
4
|
+
import { NgComponentOutlet } from '@angular/common';
|
|
5
|
+
|
|
6
|
+
const FOR_DRAWER_CONTEXT = new InjectionToken('FOR_DRAWER_CONTEXT');
|
|
7
|
+
/**
|
|
8
|
+
* @internal Per-instance drawer id, provided by `ForDrawerManager` into the
|
|
9
|
+
* opened component's injector so the portaled `[forDrawerBackdrop]` can
|
|
10
|
+
* reflect `data-for-drawer-id` and be paired with its drawer for the
|
|
11
|
+
* manager's exit-animation orchestration. Absent in the declarative path,
|
|
12
|
+
* where the backdrop's own `animate.leave` runs normally.
|
|
13
|
+
*/
|
|
14
|
+
const FOR_DRAWER_INSTANCE_ID = new InjectionToken('FOR_DRAWER_INSTANCE_ID');
|
|
15
|
+
function injectDrawerContext(piece) {
|
|
16
|
+
const ctx = inject(FOR_DRAWER_CONTEXT, { optional: true });
|
|
17
|
+
if (!ctx) {
|
|
18
|
+
throw new Error(`[forty-cdk/drawer] ${piece} must be used inside a [forDrawer] element.`);
|
|
19
|
+
}
|
|
20
|
+
return ctx;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const FALLBACK = {
|
|
24
|
+
side: 'bottom',
|
|
25
|
+
modal: true,
|
|
26
|
+
dismissible: true,
|
|
27
|
+
swipeToDismiss: true,
|
|
28
|
+
closeThreshold: 0.25,
|
|
29
|
+
initialFocus: 'first',
|
|
30
|
+
returnFocus: true,
|
|
31
|
+
handleOnly: false,
|
|
32
|
+
scaleBackground: false,
|
|
33
|
+
setBackgroundColorOnScale: true,
|
|
34
|
+
scaleAmount: 0.95,
|
|
35
|
+
scaleTranslateYpx: 14,
|
|
36
|
+
scaleBorderRadiusPx: 8,
|
|
37
|
+
scaleBackgroundColor: 'black',
|
|
38
|
+
nestedScaleAmount: 0.93,
|
|
39
|
+
nestedTranslateYpx: 8,
|
|
40
|
+
};
|
|
41
|
+
const { token, provideDefaults } = createDefaults('FOR_DRAWER_DEFAULTS', FALLBACK);
|
|
42
|
+
/** Token holding the resolved drawer defaults for the current scope. */
|
|
43
|
+
const FOR_DRAWER_DEFAULTS = token;
|
|
44
|
+
/**
|
|
45
|
+
* Configures forty-cdk drawer defaults for this injector scope. Partial
|
|
46
|
+
* overrides inherit unspecified keys from the parent scope (or library
|
|
47
|
+
* defaults at the root).
|
|
48
|
+
*/
|
|
49
|
+
function provideForDrawerDefaults(defaults = {}) {
|
|
50
|
+
return provideDefaults(defaults);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const SNAP_POINT_PERCENT_RE = /^(-?\d+(?:\.\d+)?)%$/;
|
|
54
|
+
const SNAP_POINT_PX_RE = /^(-?\d+(?:\.\d+)?)px$/;
|
|
55
|
+
function classifySnapPoint(p) {
|
|
56
|
+
// Numbers and `NN%` strings both resolve as fractions of the dimension —
|
|
57
|
+
// their cross-comparison is dimension-independent. `NNpx` strings only
|
|
58
|
+
// become comparable once we know the dimension.
|
|
59
|
+
if (typeof p === 'number') {
|
|
60
|
+
return 'fraction';
|
|
61
|
+
}
|
|
62
|
+
return SNAP_POINT_PX_RE.test(p) ? 'px' : 'fraction';
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Convert a snap point to its fractional position along the dismissal axis.
|
|
66
|
+
* Returns `[0, 1]` (or beyond, for fractional values > 1, allowed
|
|
67
|
+
* as overshoot). Throws on malformed strings or values that aren't finite.
|
|
68
|
+
*/
|
|
69
|
+
function snapPointToFraction(p, dimension) {
|
|
70
|
+
if (typeof p === 'number') {
|
|
71
|
+
if (!Number.isFinite(p)) {
|
|
72
|
+
throw new Error(`[forty-cdk/drawer] Snap point must be a finite number, got ${p}.`);
|
|
73
|
+
}
|
|
74
|
+
return p;
|
|
75
|
+
}
|
|
76
|
+
const pctMatch = SNAP_POINT_PERCENT_RE.exec(p);
|
|
77
|
+
if (pctMatch) {
|
|
78
|
+
const n = Number.parseFloat(pctMatch[1]);
|
|
79
|
+
return n / 100;
|
|
80
|
+
}
|
|
81
|
+
const pxMatch = SNAP_POINT_PX_RE.exec(p);
|
|
82
|
+
if (pxMatch) {
|
|
83
|
+
const n = Number.parseFloat(pxMatch[1]);
|
|
84
|
+
return dimension === 0 ? 0 : n / dimension;
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`[forty-cdk/drawer] Snap point must be a number, "NN%", or "NNpx" string. Got: ${String(p)}.`);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Mount-time **shape** check. Validates each snap point in isolation
|
|
90
|
+
* (rejects `NaN`, malformed strings, non-numbers) and only enforces strict
|
|
91
|
+
* monotonicity when every point shares the same unit family — pure
|
|
92
|
+
* numbers/percent strings are dimension-independent and can be compared
|
|
93
|
+
* directly, as can pure-px arrays. Mixed `'NNpx'` + fraction inputs are
|
|
94
|
+
* deferred to {@link validateSnapPositions}, which runs once the live
|
|
95
|
+
* dimension is known.
|
|
96
|
+
*
|
|
97
|
+
* Throws an `Error` with a `[forty-cdk/drawer]` prefix on the first failure.
|
|
98
|
+
*/
|
|
99
|
+
function validateSnapPointsShape(snapPoints) {
|
|
100
|
+
// Sanity-check every entry; `snapPointToFraction` throws on NaN /
|
|
101
|
+
// malformed strings. Use `1` as a placeholder dimension — the px branch
|
|
102
|
+
// would otherwise return `0` which is a valid fraction. Discard the
|
|
103
|
+
// result; we only care about the throw.
|
|
104
|
+
for (const p of snapPoints) {
|
|
105
|
+
snapPointToFraction(p, 1);
|
|
106
|
+
}
|
|
107
|
+
// Monotonicity is dimension-independent only when every entry classifies
|
|
108
|
+
// the same way. Pure-fraction (numbers + percent strings) sorts directly;
|
|
109
|
+
// pure-px sorts by raw px value. A mixed array (e.g. `['200px', 0.5]`)
|
|
110
|
+
// cannot be checked here because the cross-comparison depends on the
|
|
111
|
+
// actual surface size — that runs in `validateSnapPositions` on first
|
|
112
|
+
// measurement.
|
|
113
|
+
const kind = classifySnapPoint(snapPoints[0]);
|
|
114
|
+
const homogeneous = snapPoints.every((p) => classifySnapPoint(p) === kind);
|
|
115
|
+
if (!homogeneous) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const positions = snapPoints.map((p) => {
|
|
119
|
+
if (kind === 'fraction') {
|
|
120
|
+
return snapPointToFraction(p, 1);
|
|
121
|
+
}
|
|
122
|
+
return Number.parseFloat(SNAP_POINT_PX_RE.exec(p)[1]);
|
|
123
|
+
});
|
|
124
|
+
for (let i = 1; i < positions.length; i++) {
|
|
125
|
+
if (positions[i] <= positions[i - 1]) {
|
|
126
|
+
throw new Error('[forty-cdk/drawer] snapPoints must be strictly increasing (closest-to-edge first).');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Returns the position of each snap point along the dismissal axis (in CSS
|
|
132
|
+
* pixels measured from the anchored edge). **Does not validate** — pair with
|
|
133
|
+
* {@link validateSnapPositions} when you need the strict-increase guarantee
|
|
134
|
+
* against a live dimension.
|
|
135
|
+
*/
|
|
136
|
+
function computeSnapPositions(snapPoints, dimension) {
|
|
137
|
+
return snapPoints.map((p) => snapPointToFraction(p, dimension) * dimension);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* First-measurement **monotonicity** check. Compares the resolved px
|
|
141
|
+
* positions against the live drawer dimension. Mixed `'NNpx'` + fraction
|
|
142
|
+
* inputs that look monotonic at one dimension and non-monotonic at another
|
|
143
|
+
* fail here — the error message names the offending point so the consumer
|
|
144
|
+
* doesn't have to manually back-out the conversion.
|
|
145
|
+
*/
|
|
146
|
+
function validateSnapPositions(snapPoints, positions, dimension) {
|
|
147
|
+
for (let i = 1; i < positions.length; i++) {
|
|
148
|
+
const cur = positions[i];
|
|
149
|
+
const prev = positions[i - 1];
|
|
150
|
+
if (cur <= prev) {
|
|
151
|
+
const curConfig = JSON.stringify(snapPoints[i]);
|
|
152
|
+
const prevConfig = JSON.stringify(snapPoints[i - 1]);
|
|
153
|
+
throw new Error(`[forty-cdk/drawer] snap point ${curConfig} at index ${i} resolves to ${cur}px which is <= snap point ${prevConfig} at ${prev}px (drawer dimension ${dimension}px).`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function sideToDirections(side) {
|
|
159
|
+
switch (side) {
|
|
160
|
+
case 'bottom':
|
|
161
|
+
return ['down'];
|
|
162
|
+
case 'top':
|
|
163
|
+
return ['up'];
|
|
164
|
+
case 'right':
|
|
165
|
+
return ['right'];
|
|
166
|
+
case 'left':
|
|
167
|
+
return ['left'];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** Axis on which the drawer's dimension lives. */
|
|
171
|
+
function sideAxis(side) {
|
|
172
|
+
return side === 'left' || side === 'right' ? 'x' : 'y';
|
|
173
|
+
}
|
|
174
|
+
/** Sign of a positive (toward-edge) drag offset along the host's axis. */
|
|
175
|
+
function sideSign(side) {
|
|
176
|
+
return side === 'bottom' || side === 'right' ? 1 : -1;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Allowed swipe directions for the drag gesture. Without snap points the
|
|
180
|
+
* drawer only drags *toward* its anchored edge (dismiss). With snap points
|
|
181
|
+
* the surface must also grow on a drag *away* from the edge to reach a
|
|
182
|
+
* larger snap, so both directions along the dismissal axis arm the gesture.
|
|
183
|
+
*/
|
|
184
|
+
function dragDirections(side, hasSnapPoints) {
|
|
185
|
+
if (!hasSnapPoints) {
|
|
186
|
+
return sideToDirections(side);
|
|
187
|
+
}
|
|
188
|
+
return sideAxis(side) === 'y' ? ['down', 'up'] : ['right', 'left'];
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Pointer / swipe / snap gesture engine for `[forDrawer]`, extracted from the
|
|
192
|
+
* directive so the velocity tracking, per-gesture offset bounds,
|
|
193
|
+
* dimension-keyed snap-position cache, and runtime monotonicity re-validation
|
|
194
|
+
* live in one unit-testable collaborator. The directive keeps its inputs /
|
|
195
|
+
* outputs, label / description registration, drawer-stack + scale-coordinator
|
|
196
|
+
* registration, and `injectModalShell`.
|
|
197
|
+
*
|
|
198
|
+
* Must be called from an injection context (the directive constructor). It
|
|
199
|
+
* registers the drag-translate publisher, the runtime snap-rebind validator,
|
|
200
|
+
* and the swipe-gate effect immediately, but defers the two ordering-sensitive
|
|
201
|
+
* steps to handle methods the directive drives from its own `afterNextRender`s:
|
|
202
|
+
* {@link DrawerDragHandle.validateOnMount} (mount-time snap validation, called
|
|
203
|
+
* after the drawer-stack push so it lands at the original point in the mount
|
|
204
|
+
* sequence) and {@link DrawerDragHandle.arm} (wires the pointer listeners onto
|
|
205
|
+
* a host already portaled to `document.body` by the shell).
|
|
206
|
+
*
|
|
207
|
+
* Forwards the host's `ElementRef` and the injected `ErrorHandler` — the
|
|
208
|
+
* caller never has to touch them. Pointer listeners are detached on destroy
|
|
209
|
+
* via the engine's own `DestroyRef.onDestroy` hook.
|
|
210
|
+
*/
|
|
211
|
+
function injectDrawerDrag(config) {
|
|
212
|
+
const host = inject(ElementRef);
|
|
213
|
+
const errorHandler = inject(ErrorHandler);
|
|
214
|
+
const prefersReducedMotion = injectPrefersReducedMotion();
|
|
215
|
+
const side = config.side;
|
|
216
|
+
const snapPoints = config.snapPoints;
|
|
217
|
+
const closeThreshold = config.closeThreshold;
|
|
218
|
+
const fadeFromIndex = config.fadeFromIndex;
|
|
219
|
+
const activeSnapPoint = config.activeSnapPoint;
|
|
220
|
+
const dragOffset = signal(0, /* @ts-ignore */
|
|
221
|
+
...(ngDevMode ? [{ debugName: "dragOffset" }] : /* istanbul ignore next */ [])); // px translated along the dismissal axis (positive = away from edge)
|
|
222
|
+
const dragging = signal(false, /* @ts-ignore */
|
|
223
|
+
...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
|
|
224
|
+
const dragProgress = signal(0, /* @ts-ignore */
|
|
225
|
+
...(ngDevMode ? [{ debugName: "dragProgress" }] : /* istanbul ignore next */ [])); // [0, 1] progress toward the anchored edge (dismiss direction)
|
|
226
|
+
const dragTranslate = computed(() => {
|
|
227
|
+
const offset = dragOffset();
|
|
228
|
+
if (offset === 0) {
|
|
229
|
+
return '0px 0px';
|
|
230
|
+
}
|
|
231
|
+
const px = sideSign(side()) * offset;
|
|
232
|
+
return sideAxis(side()) === 'y' ? `0px ${px}px` : `${px}px 0px`;
|
|
233
|
+
}, /* @ts-ignore */
|
|
234
|
+
...(ngDevMode ? [{ debugName: "dragTranslate" }] : /* istanbul ignore next */ []));
|
|
235
|
+
// Pointer state for velocity tracking.
|
|
236
|
+
let pointerStartTime = 0;
|
|
237
|
+
let pointerLastY = 0;
|
|
238
|
+
let pointerLastX = 0;
|
|
239
|
+
let pointerLastTime = 0;
|
|
240
|
+
let pointerVelocity = 0;
|
|
241
|
+
let dimensionAtStart = 0;
|
|
242
|
+
// Per-gesture lower bound for the drag offset (px toward the edge),
|
|
243
|
+
// resolved on `onSwipeStart`. A positive offset moves the surface toward
|
|
244
|
+
// the edge (shrink / dismiss) and is never capped — dragging past the edge
|
|
245
|
+
// is how a dismiss arms. A negative offset moves it away from the edge to
|
|
246
|
+
// grow. Without snap points the floor stays 0 (the surface can only shrink
|
|
247
|
+
// toward the edge); with snap points the floor reaches the largest snap so
|
|
248
|
+
// an upward drag can expand the surface.
|
|
249
|
+
let dragMinOffset = 0;
|
|
250
|
+
// Snap positions cache, keyed by BOTH the dimension they were resolved
|
|
251
|
+
// against AND the `snapPoints` array identity. First-measurement validation
|
|
252
|
+
// populates this; `onSwipeStart` refreshes it when the surface has resized
|
|
253
|
+
// between gestures, and a runtime `[snapPoints]` rebind (same dimension, new
|
|
254
|
+
// array reference) is a cache miss so positions are recomputed and
|
|
255
|
+
// re-validated. Always pre-validated, so `onSwipeRelease` can read it
|
|
256
|
+
// without re-running monotonicity checks.
|
|
257
|
+
let snapPositionsCache = null;
|
|
258
|
+
// Flipped true once the mount-time `afterNextRender` has validated the
|
|
259
|
+
// initial snap config. Gates the runtime-rebind effect so its first
|
|
260
|
+
// (pre-render) run is a no-op and only genuine post-mount changes
|
|
261
|
+
// re-validate.
|
|
262
|
+
let snapConfigMounted = false;
|
|
263
|
+
// Signal flipped true by `arm()` in the directive's post-shell
|
|
264
|
+
// `afterNextRender`. Gates the swipe gate effect so the gesture arms on a
|
|
265
|
+
// host already wired by the shell; being a signal, flipping it re-runs the
|
|
266
|
+
// effect for the first real attach.
|
|
267
|
+
const swipeReady = signal(false, /* @ts-ignore */
|
|
268
|
+
...(ngDevMode ? [{ debugName: "swipeReady" }] : /* istanbul ignore next */ []));
|
|
269
|
+
let swipeCleanup = null;
|
|
270
|
+
/**
|
|
271
|
+
* Dimension-independent snap-point validation: the per-point shape /
|
|
272
|
+
* strict-increase check (`validateSnapPointsShape`) plus the
|
|
273
|
+
* `fadeFromIndex` range check. Throws with a `[forty-cdk/drawer]`-prefixed
|
|
274
|
+
* message on the first failure. A `null` / empty array is a valid "no snap
|
|
275
|
+
* points" config and skips both checks. Shared by the mount-time
|
|
276
|
+
* `afterNextRender` and the runtime-rebind effect so the two paths stay in
|
|
277
|
+
* lockstep.
|
|
278
|
+
*/
|
|
279
|
+
function validateSnapPointConfig(points) {
|
|
280
|
+
if (!points || points.length === 0) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
validateSnapPointsShape(points);
|
|
284
|
+
const idx = fadeFromIndex();
|
|
285
|
+
if (idx !== undefined && (idx < 0 || idx >= points.length)) {
|
|
286
|
+
throw new Error(`[forty-cdk/drawer] fadeFromIndex (${idx}) is out of range for snapPoints (length ${points.length}).`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Resolve and validate snap positions against the host's current
|
|
291
|
+
* dimension. No-op if the cached positions already match BOTH the live
|
|
292
|
+
* dimension and the current `snapPoints` array. Throws (with the
|
|
293
|
+
* offending-point error message) when the live dimension flips a mixed
|
|
294
|
+
* `'NNpx'` + fraction array out of monotonic order.
|
|
295
|
+
*
|
|
296
|
+
* Called from `afterNextRender` (first measurement), from `onSwipeStart`
|
|
297
|
+
* (resize between gestures, or a runtime `[snapPoints]` rebind), and from
|
|
298
|
+
* the runtime-rebind effect. Never from `onSwipeRelease` — by the time
|
|
299
|
+
* release fires, the cache is already populated for this gesture's
|
|
300
|
+
* dimension and array.
|
|
301
|
+
*/
|
|
302
|
+
function refreshSnapPositions(points) {
|
|
303
|
+
const rect = host.nativeElement.getBoundingClientRect();
|
|
304
|
+
const dim = sideAxis(side()) === 'y' ? rect.height : rect.width;
|
|
305
|
+
if (dim <= 0) {
|
|
306
|
+
// No layout yet (jsdom, or display: none). Defer; the next call with
|
|
307
|
+
// a real dimension will do the work.
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const cached = snapPositionsCache;
|
|
311
|
+
if (cached && cached.dimension === dim && cached.snapPoints === points) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const positions = computeSnapPositions(points, dim);
|
|
315
|
+
validateSnapPositions(points, positions, dim);
|
|
316
|
+
snapPositionsCache = { dimension: dim, snapPoints: points, positions };
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Pixel position (from the anchored edge) of the currently-active snap
|
|
320
|
+
* point within `snapPositions`. Falls back to the closest-to-edge entry
|
|
321
|
+
* when the active point is unset or not found in `snapPoints`.
|
|
322
|
+
*/
|
|
323
|
+
function activeSnapPositionPx(points, snapPositions) {
|
|
324
|
+
const active = activeSnapPoint();
|
|
325
|
+
if (active == null) {
|
|
326
|
+
return snapPositions[0];
|
|
327
|
+
}
|
|
328
|
+
const idx = points.indexOf(active);
|
|
329
|
+
return idx >= 0 ? snapPositions[idx] : snapPositions[0];
|
|
330
|
+
}
|
|
331
|
+
function onSwipeStart(detail) {
|
|
332
|
+
// Bail out conditions that the swipe-dismiss helper can't know about:
|
|
333
|
+
// (a) `handleOnly` and the gesture didn't start on the registered handle,
|
|
334
|
+
// (b) the gesture started inside a scrollable element that hasn't reached
|
|
335
|
+
// its edge along the dismissal direction (don't steal scroll).
|
|
336
|
+
const target = detail.originalEvent.target;
|
|
337
|
+
const handle = config.handleEl();
|
|
338
|
+
if (config.handleOnly() && (!handle || !target || !handle.contains(target))) {
|
|
339
|
+
dragging.set(false);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (target && isScrollableAtEdge(target, detail.direction, host.nativeElement)) {
|
|
343
|
+
dragging.set(false);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
dragging.set(true);
|
|
347
|
+
dragProgress.set(0);
|
|
348
|
+
pointerStartTime = detail.originalEvent.timeStamp || performance.now();
|
|
349
|
+
pointerLastTime = pointerStartTime;
|
|
350
|
+
pointerLastX = detail.originalEvent.clientX;
|
|
351
|
+
pointerLastY = detail.originalEvent.clientY;
|
|
352
|
+
pointerVelocity = 0;
|
|
353
|
+
const rect = host.nativeElement.getBoundingClientRect();
|
|
354
|
+
dimensionAtStart = sideAxis(side()) === 'y' ? rect.height : rect.width;
|
|
355
|
+
// Refresh & validate snap positions for this gesture's dimension. If
|
|
356
|
+
// mount-time first-measurement saw a non-zero dimension equal to the
|
|
357
|
+
// current one, this is a cache hit and no work runs. Validation throws
|
|
358
|
+
// here (pre-drag) rather than from the release handler.
|
|
359
|
+
const points = snapPoints();
|
|
360
|
+
if (points && points.length > 0) {
|
|
361
|
+
refreshSnapPositions(points);
|
|
362
|
+
const cached = snapPositionsCache;
|
|
363
|
+
const positions = cached && cached.snapPoints === points
|
|
364
|
+
? cached.positions
|
|
365
|
+
: computeSnapPositions(points, dimensionAtStart);
|
|
366
|
+
const activePos = activeSnapPositionPx(points, positions);
|
|
367
|
+
const highestPos = positions[positions.length - 1] ?? activePos;
|
|
368
|
+
dragMinOffset = activePos - highestPos;
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
dragMinOffset = 0;
|
|
372
|
+
}
|
|
373
|
+
config.emitDrag({ percentageDragged: 0, originalEvent: detail.originalEvent });
|
|
374
|
+
}
|
|
375
|
+
function onSwipeMove(detail) {
|
|
376
|
+
if (!dragging()) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const event = detail.originalEvent;
|
|
380
|
+
const now = event.timeStamp || performance.now();
|
|
381
|
+
const dx = event.clientX - pointerLastX;
|
|
382
|
+
const dy = event.clientY - pointerLastY;
|
|
383
|
+
const dt = Math.max(1, now - pointerLastTime);
|
|
384
|
+
// Magnitude moved toward the anchored edge (positive = move toward edge).
|
|
385
|
+
const moveTowardEdge = (() => {
|
|
386
|
+
switch (side()) {
|
|
387
|
+
case 'bottom':
|
|
388
|
+
return dy; // pointer moves down → toward bottom edge → positive
|
|
389
|
+
case 'top':
|
|
390
|
+
return -dy;
|
|
391
|
+
case 'right':
|
|
392
|
+
return dx;
|
|
393
|
+
case 'left':
|
|
394
|
+
return -dx;
|
|
395
|
+
}
|
|
396
|
+
})();
|
|
397
|
+
pointerVelocity = moveTowardEdge / dt; // px per ms toward edge
|
|
398
|
+
pointerLastX = event.clientX;
|
|
399
|
+
pointerLastY = event.clientY;
|
|
400
|
+
pointerLastTime = now;
|
|
401
|
+
// Integrate the per-event pointer delta into the cumulative drag offset,
|
|
402
|
+
// clamped to this gesture's bounds. Without snap points the lower bound
|
|
403
|
+
// is 0 (the surface only moves toward the edge to dismiss); with snap
|
|
404
|
+
// points it goes negative so a drag away from the edge grows the surface
|
|
405
|
+
// toward a larger snap. The host's `[style.translate]` binding reflects
|
|
406
|
+
// the offset reactively — no imperative DOM write here.
|
|
407
|
+
const nextOffset = Math.max(dragMinOffset, dragOffset() + moveTowardEdge);
|
|
408
|
+
dragOffset.set(nextOffset);
|
|
409
|
+
const dim = dimensionAtStart || 1;
|
|
410
|
+
// `percentageDragged` tracks progress toward dismiss, so growth (a
|
|
411
|
+
// negative offset) reads as 0 rather than a negative number.
|
|
412
|
+
const percentageDragged = Math.min(1, Math.max(0, nextOffset / dim));
|
|
413
|
+
dragProgress.set(percentageDragged);
|
|
414
|
+
config.emitDrag({ percentageDragged, originalEvent: event });
|
|
415
|
+
}
|
|
416
|
+
function onSwipeRelease(detail) {
|
|
417
|
+
if (!dragging()) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const event = detail.originalEvent;
|
|
421
|
+
const offset = dragOffset();
|
|
422
|
+
const dim = dimensionAtStart || 1;
|
|
423
|
+
const threshold = closeThreshold();
|
|
424
|
+
const points = snapPoints();
|
|
425
|
+
let willClose = false;
|
|
426
|
+
let nextSnap = null;
|
|
427
|
+
if (points && points.length > 0) {
|
|
428
|
+
// `position` is the surface position along the dismissal axis from the
|
|
429
|
+
// anchored edge. With `offset` representing how far the surface has
|
|
430
|
+
// been pulled toward the edge (positive), the effective position from
|
|
431
|
+
// the edge is `currentSnapPosition - offset`.
|
|
432
|
+
//
|
|
433
|
+
// Read snap positions from the pre-validated cache populated by
|
|
434
|
+
// `refreshSnapPositions` at mount and on `onSwipeStart`. The release
|
|
435
|
+
// path is throw-free by construction: any input that would fail
|
|
436
|
+
// monotonicity at the live dimension has already failed before we get
|
|
437
|
+
// here.
|
|
438
|
+
const cached = snapPositionsCache;
|
|
439
|
+
const snapPositions = cached && cached.dimension === dim && cached.snapPoints === points
|
|
440
|
+
? cached.positions
|
|
441
|
+
: computeSnapPositions(points, dim);
|
|
442
|
+
const position = activeSnapPositionPx(points, snapPositions) - offset;
|
|
443
|
+
const resolved = resolveSnapTarget({
|
|
444
|
+
snapPoints: points,
|
|
445
|
+
snapPositions,
|
|
446
|
+
activeSnapPoint: activeSnapPoint(),
|
|
447
|
+
position,
|
|
448
|
+
velocity: -pointerVelocity, // helper sema: positive = away from edge
|
|
449
|
+
closeThreshold: threshold,
|
|
450
|
+
});
|
|
451
|
+
willClose = resolved.willClose;
|
|
452
|
+
nextSnap = resolved.nextSnapPoint;
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
// No snap points: dismiss when dragged past closeThreshold OR fast
|
|
456
|
+
// flick toward edge.
|
|
457
|
+
willClose = offset >= dim * threshold || pointerVelocity >= 0.4;
|
|
458
|
+
}
|
|
459
|
+
config.emitRelease({ willClose, nextSnapPoint: nextSnap, originalEvent: event });
|
|
460
|
+
// Zero the offset and flip `dragging` off together. Both writes (plus the
|
|
461
|
+
// `activeSnapPoint` change below) flush in one change-detection pass, so
|
|
462
|
+
// the host applies the `data-dragging` removal, the new
|
|
463
|
+
// `data-active-snap-point`, and the `translate` reset to zero in a single
|
|
464
|
+
// style recalc — the consumer's `transition: translate` then animates the
|
|
465
|
+
// drag delta away in lockstep with the snap-position transition, with no
|
|
466
|
+
// intermediate jump to the previous rest position.
|
|
467
|
+
dragOffset.set(0);
|
|
468
|
+
dragProgress.set(0);
|
|
469
|
+
dragging.set(false);
|
|
470
|
+
if (willClose) {
|
|
471
|
+
config.requestClose('swipe');
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (nextSnap !== null && nextSnap !== activeSnapPoint()) {
|
|
475
|
+
activeSnapPoint.set(nextSnap);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function attachSwipe() {
|
|
479
|
+
const el = host.nativeElement;
|
|
480
|
+
return attachSwipeDismiss({
|
|
481
|
+
element: el,
|
|
482
|
+
getDirections: () => dragDirections(side(), !!snapPoints()?.length),
|
|
483
|
+
// Always arm on a tiny gesture and let the move handler decide; we
|
|
484
|
+
// resolve the actual close threshold on release via resolveSnapTarget.
|
|
485
|
+
getThreshold: () => 1,
|
|
486
|
+
onSwipeStart: (detail) => onSwipeStart(detail),
|
|
487
|
+
onSwipeMove: (detail) => onSwipeMove(detail),
|
|
488
|
+
onSwipeEnd: (detail) => onSwipeRelease(detail),
|
|
489
|
+
onSwipeCancel: (detail) => onSwipeRelease(detail),
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
// ---- Drag-translate side effect. Publishes the drag delta as the
|
|
493
|
+
// `--for-drawer-translate` custom property (see `dragTranslate` for why a
|
|
494
|
+
// custom property rather than a directly-written `translate`/`transform`).
|
|
495
|
+
// Runs in the same change-detection flush as the host's attribute
|
|
496
|
+
// bindings, so the release path's `data-dragging` removal,
|
|
497
|
+
// `data-active-snap-point` change, and translate reset to "0px 0px" all
|
|
498
|
+
// land in one style recalc and transition together.
|
|
499
|
+
effect(() => {
|
|
500
|
+
host.nativeElement.style.setProperty('--for-drawer-translate', dragTranslate());
|
|
501
|
+
});
|
|
502
|
+
// ---- Mount-time snap validation. Invoked by the directive's pre-shell
|
|
503
|
+
// `afterNextRender` (via the returned `validateOnMount`), after the
|
|
504
|
+
// drawer-stack push and the `closeThreshold` check, so it lands at the same
|
|
505
|
+
// point in the mount sequence it always has — before the shell's
|
|
506
|
+
// dismissable / focus / scroll-lock wiring. A consumer passing a bad
|
|
507
|
+
// snapPoints array gets a clear error before the shell tries to focus an
|
|
508
|
+
// element that may not exist. The mount-time `activeSnapPoint` default is
|
|
509
|
+
// seeded here too. Subsequent runtime `[snapPoints]` rebinds are handled by
|
|
510
|
+
// the effect below, which re-runs the same validation and invalidates the
|
|
511
|
+
// position cache.
|
|
512
|
+
function validateOnMount() {
|
|
513
|
+
const points = snapPoints();
|
|
514
|
+
if (points && points.length > 0 && activeSnapPoint() === null) {
|
|
515
|
+
activeSnapPoint.set(points[0]);
|
|
516
|
+
}
|
|
517
|
+
validateSnapPointConfig(points);
|
|
518
|
+
// Try first measurement. In real browsers `getBoundingClientRect`
|
|
519
|
+
// returns the laid-out dimension here (we're inside `afterNextRender`,
|
|
520
|
+
// post-layout). In jsdom layout doesn't run, so dimension is 0 — defer
|
|
521
|
+
// to `onSwipeStart`'s rect read, which is also pre-gesture.
|
|
522
|
+
if (points && points.length > 0) {
|
|
523
|
+
refreshSnapPositions(points);
|
|
524
|
+
}
|
|
525
|
+
snapConfigMounted = true;
|
|
526
|
+
}
|
|
527
|
+
// ---- Runtime `[snapPoints]` / `[fadeFromIndex]` rebind. `validateOnMount`
|
|
528
|
+
// validates the INITIAL config once; this effect
|
|
529
|
+
// re-validates whenever the consumer swaps the array (or fadeFromIndex)
|
|
530
|
+
// at runtime. Without it a new array that is non-monotonic — or a
|
|
531
|
+
// fadeFromIndex that no longer fits — would slip through, and the
|
|
532
|
+
// position cache (keyed on array identity) would lazily recompute on the
|
|
533
|
+
// next gesture against the new array but never re-run the shape /
|
|
534
|
+
// range checks. Guarded by `snapConfigMounted` so the effect's first
|
|
535
|
+
// run (which fires before `validateOnMount`) doesn't duplicate the
|
|
536
|
+
// mount-time validation or race the layout measurement. Validation +
|
|
537
|
+
// cache invalidation are genuine side effects (route an error / clear a
|
|
538
|
+
// field), not reactive-state propagation, so an `effect` is the right
|
|
539
|
+
// tool here. Validation failures are funnelled through the injected
|
|
540
|
+
// `ErrorHandler` so they reach the consumer's handler exactly like the
|
|
541
|
+
// mount-time `afterNextRender` throw, instead of tearing down the effect.
|
|
542
|
+
effect(() => {
|
|
543
|
+
const points = snapPoints();
|
|
544
|
+
// Track fadeFromIndex too so a runtime range violation is caught.
|
|
545
|
+
fadeFromIndex();
|
|
546
|
+
if (!snapConfigMounted) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
// A new array reference invalidates the position cache so the next
|
|
550
|
+
// gesture (and `refreshSnapPositions`) resolves against fresh geometry
|
|
551
|
+
// instead of the previous array's cached positions.
|
|
552
|
+
if (snapPositionsCache && snapPositionsCache.snapPoints !== points) {
|
|
553
|
+
snapPositionsCache = null;
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
validateSnapPointConfig(points);
|
|
557
|
+
if (points && points.length > 0) {
|
|
558
|
+
refreshSnapPositions(points);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
errorHandler.handleError(error);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
// ---- Swipe-to-dismiss gate. Arms the pointer listeners only when
|
|
566
|
+
// `swipeToDismiss` is on AND the user hasn't asked for reduced motion
|
|
567
|
+
// (drag animations are vestibular-hostile). Both inputs are read
|
|
568
|
+
// reactively so a runtime flip of either — a `[swipeToDismiss]` rebind or
|
|
569
|
+
// a live `prefers-reduced-motion` change — arms or disarms the gesture,
|
|
570
|
+
// mirroring how `ForDrawerScaleCoordinator` already reacts to the
|
|
571
|
+
// preference. Attaching/detaching listeners is a DOM side effect, so an
|
|
572
|
+
// `effect` is the right tool; `swipeReady` gates the pre-render run so the
|
|
573
|
+
// gesture arms on a host already attached via the shell's portal.
|
|
574
|
+
effect(() => {
|
|
575
|
+
const shouldArm = config.swipeToDismiss() && !prefersReducedMotion();
|
|
576
|
+
if (!swipeReady()) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (shouldArm && swipeCleanup === null) {
|
|
580
|
+
swipeCleanup = attachSwipe();
|
|
581
|
+
}
|
|
582
|
+
else if (!shouldArm && swipeCleanup !== null) {
|
|
583
|
+
swipeCleanup();
|
|
584
|
+
swipeCleanup = null;
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
inject(DestroyRef).onDestroy(() => {
|
|
588
|
+
swipeCleanup?.();
|
|
589
|
+
swipeCleanup = null;
|
|
590
|
+
});
|
|
591
|
+
return {
|
|
592
|
+
dragging: dragging.asReadonly(),
|
|
593
|
+
dragProgress: dragProgress.asReadonly(),
|
|
594
|
+
dragTranslate,
|
|
595
|
+
validateOnMount,
|
|
596
|
+
arm: () => swipeReady.set(true),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Headless implementation of the [WAI-ARIA Modal Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/),
|
|
602
|
+
* specialised as a side / bottom-sheet drawer with optional swipe-to-dismiss
|
|
603
|
+
* and snap points.
|
|
604
|
+
*
|
|
605
|
+
* Apply `[forDrawer]` on the drawer surface itself — not on a wrapper. The
|
|
606
|
+
* directive moves the host to `document.body` (portal), traps focus, locks
|
|
607
|
+
* body scroll, listens for Escape, and (when `swipeToDismiss` is on) attaches
|
|
608
|
+
* pointer-based swipe handlers that translate the surface and resolve to the
|
|
609
|
+
* nearest snap point or a dismiss on release. `aria-labelledby` and
|
|
610
|
+
* `aria-describedby` wire automatically via `[forDrawerTitle]` /
|
|
611
|
+
* `[forDrawerDescription]`; pass `ariaLabel` instead if you don't render
|
|
612
|
+
* a visible title.
|
|
613
|
+
*
|
|
614
|
+
* Mount/unmount is the consumer's responsibility — the directive does not
|
|
615
|
+
* manage `[hidden]`. Wrap with `@if (open())` and let `animate.enter` /
|
|
616
|
+
* `animate.leave` handle transitions.
|
|
617
|
+
*
|
|
618
|
+
* For programmatic use (open arbitrary components imperatively, manage
|
|
619
|
+
* lifecycle externally), see `ForDrawerManager.open()`.
|
|
620
|
+
*/
|
|
621
|
+
class ForDrawer {
|
|
622
|
+
#defaults = inject(FOR_DRAWER_DEFAULTS);
|
|
623
|
+
/**
|
|
624
|
+
* Edge the drawer is anchored to. Default `'bottom'` — the most common
|
|
625
|
+
* mobile-first pattern. The keyboard navigation axis (for swipe direction)
|
|
626
|
+
* derives from this, as does the `data-side` attribute on the host.
|
|
627
|
+
*/
|
|
628
|
+
side = input(this.#defaults.side ?? 'bottom', /* @ts-ignore */
|
|
629
|
+
...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
|
|
630
|
+
/**
|
|
631
|
+
* When true (default), Escape, backdrop click, pointer-down outside,
|
|
632
|
+
* focus outside, and a swipe past the close threshold all emit `(dismiss)`.
|
|
633
|
+
* Disable for confirm flows that must be answered explicitly.
|
|
634
|
+
*/
|
|
635
|
+
dismissible = input(this.#defaults.dismissible ?? true, { ...(ngDevMode ? { debugName: "dismissible" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
636
|
+
/**
|
|
637
|
+
* When true (default), sets `aria-modal="true"`, locks body scroll, traps
|
|
638
|
+
* focus, and inerts body siblings. Set to `false` for a non-modal
|
|
639
|
+
* navigation drawer that coexists with the rest of the page.
|
|
640
|
+
*/
|
|
641
|
+
modal = input(this.#defaults.modal ?? true, { ...(ngDevMode ? { debugName: "modal" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
642
|
+
/** When true, role becomes `alertdialog` (interrupts assistive tech). */
|
|
643
|
+
alert = input(false, { ...(ngDevMode ? { debugName: "alert" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
644
|
+
/** When true (default), focus returns to the previously focused element on close. */
|
|
645
|
+
returnFocus = input(this.#defaults.returnFocus ?? true, { ...(ngDevMode ? { debugName: "returnFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
646
|
+
/**
|
|
647
|
+
* Where to send focus on mount. `'first'` (default) finds the first
|
|
648
|
+
* focusable descendant; `'container'` focuses the drawer surface itself
|
|
649
|
+
* (useful when there's nothing focusable inside).
|
|
650
|
+
*/
|
|
651
|
+
initialFocus = input(this.#defaults.initialFocus ?? 'first', /* @ts-ignore */
|
|
652
|
+
...(ngDevMode ? [{ debugName: "initialFocus" }] : /* istanbul ignore next */ []));
|
|
653
|
+
/** Manual `aria-label`. Use this when no visible title element exists. */
|
|
654
|
+
ariaLabel = input(null, /* @ts-ignore */
|
|
655
|
+
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
656
|
+
/**
|
|
657
|
+
* Portal target for the surface. Defaults to `document.body`. Pass a
|
|
658
|
+
* positioned (`position: relative`) container — paired with
|
|
659
|
+
* `[modal]="false"` — for a drawer scoped to a region instead of the
|
|
660
|
+
* viewport. Read once at mount (the portal moves the host once); changing
|
|
661
|
+
* it after open has no effect. The backdrop portals to the same container.
|
|
662
|
+
*/
|
|
663
|
+
container = input(null, /* @ts-ignore */
|
|
664
|
+
...(ngDevMode ? [{ debugName: "container" }] : /* istanbul ignore next */ []));
|
|
665
|
+
/**
|
|
666
|
+
* Callback invoked just before focus moves into the drawer on mount.
|
|
667
|
+
* `event.preventDefault()` skips the imperative move (Tab cycling and
|
|
668
|
+
* return-focus capture still wire up). See Dialog for the same shape and
|
|
669
|
+
* the rationale for `input<>` rather than `output<>` on free-floating
|
|
670
|
+
* overlays.
|
|
671
|
+
*/
|
|
672
|
+
autoFocusOnOpen = input(undefined, /* @ts-ignore */
|
|
673
|
+
...(ngDevMode ? [{ debugName: "autoFocusOnOpen" }] : /* istanbul ignore next */ []));
|
|
674
|
+
/**
|
|
675
|
+
* Callback invoked just before focus returns to the previously focused
|
|
676
|
+
* element on unmount. `event.preventDefault()` skips the return-focus.
|
|
677
|
+
*/
|
|
678
|
+
autoFocusOnClose = input(undefined, /* @ts-ignore */
|
|
679
|
+
...(ngDevMode ? [{ debugName: "autoFocusOnClose" }] : /* istanbul ignore next */ []));
|
|
680
|
+
/** When true (default), swipe toward the anchored edge dismisses past `closeThreshold`. */
|
|
681
|
+
swipeToDismiss = input(this.#defaults.swipeToDismiss ?? true, { ...(ngDevMode ? { debugName: "swipeToDismiss" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
682
|
+
/**
|
|
683
|
+
* Fraction past which a release dismisses instead of snapping back.
|
|
684
|
+
* Default `0.25`. Without `snapPoints` it is a fraction of the
|
|
685
|
+
* full drawer dimension along the dismissal axis (dragged > 25% of the
|
|
686
|
+
* surface size toward the edge dismisses); with `snapPoints` it is a
|
|
687
|
+
* fraction of the **lowest snap's** extent, so a small "peek" snap stays
|
|
688
|
+
* dismissable without dragging it entirely off-screen.
|
|
689
|
+
*/
|
|
690
|
+
closeThreshold = input(this.#defaults.closeThreshold ?? 0.25, /* @ts-ignore */
|
|
691
|
+
...(ngDevMode ? [{ debugName: "closeThreshold" }] : /* istanbul ignore next */ []));
|
|
692
|
+
/**
|
|
693
|
+
* When true, the swipe gesture only arms on a pointerdown that started on
|
|
694
|
+
* the registered `[forDrawerHandle]` element. Useful when the surface
|
|
695
|
+
* contains scrollable content that should keep its scroll gesture.
|
|
696
|
+
*/
|
|
697
|
+
handleOnly = input(this.#defaults.handleOnly ?? false, { ...(ngDevMode ? { debugName: "handleOnly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
698
|
+
/**
|
|
699
|
+
* When true, asks the registered `[forDrawerWrapper]` to scale and
|
|
700
|
+
* translate behind this drawer (the "scale background" effect). No
|
|
701
|
+
* effect under `prefers-reduced-motion: reduce`, and a no-op if no
|
|
702
|
+
* wrapper is mounted. Default `false` so existing consumers are
|
|
703
|
+
* untouched.
|
|
704
|
+
*/
|
|
705
|
+
scaleBackground = input(this.#defaults.scaleBackground ?? false, { ...(ngDevMode ? { debugName: "scaleBackground" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
706
|
+
/**
|
|
707
|
+
* When true (default) and `scaleBackground` is active, paints
|
|
708
|
+
* `<body>` with `scaleBackgroundColor` so the gap between the scaled
|
|
709
|
+
* wrapper and the viewport edge does not show through. Only applies
|
|
710
|
+
* when `scaleBackground` is `true`.
|
|
711
|
+
*/
|
|
712
|
+
setBackgroundColorOnScale = input(this.#defaults.setBackgroundColorOnScale ?? true, { ...(ngDevMode ? { debugName: "setBackgroundColorOnScale" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
713
|
+
/**
|
|
714
|
+
* Snap points: each entry is a `number ∈ [0, 1]`,
|
|
715
|
+
* a `'NN%'` string, or a `'NNpx'` string. Strictly increasing
|
|
716
|
+
* (closest-to-edge first). The surface settles at the nearest snap on
|
|
717
|
+
* release; dragging past the lowest snap by `closeThreshold` of that
|
|
718
|
+
* snap's own extent dismisses.
|
|
719
|
+
*/
|
|
720
|
+
snapPoints = input(undefined, /* @ts-ignore */
|
|
721
|
+
...(ngDevMode ? [{ debugName: "snapPoints" }] : /* istanbul ignore next */ []));
|
|
722
|
+
/**
|
|
723
|
+
* Active snap point. Two-way bindable: write via `[(activeSnapPoint)]`
|
|
724
|
+
* to drive the surface programmatically, or just read for analytics.
|
|
725
|
+
* Initialised to `snapPoints?.[0]` on mount when the consumer left it
|
|
726
|
+
* `null`. The implicit `(activeSnapPointChange)` emitter fires on
|
|
727
|
+
* internal transitions (mount-time default and drag release), and
|
|
728
|
+
* stays silent on consumer writes through `[(activeSnapPoint)]`.
|
|
729
|
+
*/
|
|
730
|
+
activeSnapPoint = model(null, /* @ts-ignore */
|
|
731
|
+
...(ngDevMode ? [{ debugName: "activeSnapPoint" }] : /* istanbul ignore next */ []));
|
|
732
|
+
/**
|
|
733
|
+
* First index of `snapPoints` from which the backdrop should reflect
|
|
734
|
+
* `data-fade-from-active`. Consumers tie a CSS opacity transition to
|
|
735
|
+
* that attribute. Out-of-range values throw on mount.
|
|
736
|
+
*/
|
|
737
|
+
fadeFromIndex = input(undefined, /* @ts-ignore */
|
|
738
|
+
...(ngDevMode ? [{ debugName: "fadeFromIndex" }] : /* istanbul ignore next */ []));
|
|
739
|
+
/**
|
|
740
|
+
* Emitted when the drawer wants to close. Consumer wires this to flip
|
|
741
|
+
* the signal that gates the surrounding `@if`.
|
|
742
|
+
*/
|
|
743
|
+
dismiss = output();
|
|
744
|
+
/** Vetoable Escape. `preventDefault()` suppresses the auto `(dismiss)`. */
|
|
745
|
+
escapeKeyDown = output();
|
|
746
|
+
/** Vetoable pointer-down outside. `preventDefault()` suppresses the auto `(dismiss)`. */
|
|
747
|
+
pointerDownOutside = output();
|
|
748
|
+
/** Vetoable focus-outside. `preventDefault()` suppresses the auto `(dismiss)`. */
|
|
749
|
+
focusOutside = output();
|
|
750
|
+
/**
|
|
751
|
+
* Composite event: fires alongside `pointerDownOutside` and `focusOutside`
|
|
752
|
+
* and shares their veto state — `preventDefault()` on either suppresses
|
|
753
|
+
* the auto `(dismiss)`.
|
|
754
|
+
*/
|
|
755
|
+
interactOutside = output();
|
|
756
|
+
/** Drag stream. `percentageDragged` ∈ `[0, 1]`. */
|
|
757
|
+
dragMove = output();
|
|
758
|
+
/** Release event. The directive has already updated `activeSnapPoint` / requested close. */
|
|
759
|
+
release = output();
|
|
760
|
+
// ---- Internal reactive state ----
|
|
761
|
+
#labelIds = signal([], /* @ts-ignore */
|
|
762
|
+
...(ngDevMode ? [{ debugName: "#labelIds" }] : /* istanbul ignore next */ []));
|
|
763
|
+
#describedByIds = signal([], /* @ts-ignore */
|
|
764
|
+
...(ngDevMode ? [{ debugName: "#describedByIds" }] : /* istanbul ignore next */ []));
|
|
765
|
+
#handleEl = signal(null, /* @ts-ignore */
|
|
766
|
+
...(ngDevMode ? [{ debugName: "#handleEl" }] : /* istanbul ignore next */ []));
|
|
767
|
+
#backdropEl = signal(null, /* @ts-ignore */
|
|
768
|
+
...(ngDevMode ? [{ debugName: "#backdropEl" }] : /* istanbul ignore next */ []));
|
|
769
|
+
// Captures the `value` argument from the most recent `requestClose(reason, value)`
|
|
770
|
+
// call. Read by `ForDrawerManager` to bridge `[forDrawerClose] [closeWith]`
|
|
771
|
+
// into `ForDrawerRef.close(value)`. Plain in declarative usage (no consumer
|
|
772
|
+
// ever reads it), so the API surface is unchanged.
|
|
773
|
+
#lastCloseValue = signal(undefined, /* @ts-ignore */
|
|
774
|
+
...(ngDevMode ? [{ debugName: "#lastCloseValue" }] : /* istanbul ignore next */ []));
|
|
775
|
+
labelledBy = computed(() => {
|
|
776
|
+
const ids = this.#labelIds();
|
|
777
|
+
return ids.length === 0 ? null : ids.join(' ');
|
|
778
|
+
}, /* @ts-ignore */
|
|
779
|
+
...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
|
|
780
|
+
describedBy = computed(() => {
|
|
781
|
+
const ids = this.#describedByIds();
|
|
782
|
+
return ids.length === 0 ? null : ids.join(' ');
|
|
783
|
+
}, /* @ts-ignore */
|
|
784
|
+
...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
785
|
+
activeSnapPointAttr = computed(() => {
|
|
786
|
+
const v = this.activeSnapPoint();
|
|
787
|
+
return v == null ? null : String(v);
|
|
788
|
+
}, /* @ts-ignore */
|
|
789
|
+
...(ngDevMode ? [{ debugName: "activeSnapPointAttr" }] : /* istanbul ignore next */ []));
|
|
790
|
+
fadeFromActive = computed(() => {
|
|
791
|
+
const idx = this.fadeFromIndex();
|
|
792
|
+
const points = this.snapPoints();
|
|
793
|
+
const active = this.activeSnapPoint();
|
|
794
|
+
if (idx === undefined || !points || active == null) {
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
const activeIdx = points.indexOf(active);
|
|
798
|
+
return activeIdx >= 0 && activeIdx >= idx;
|
|
799
|
+
}, /* @ts-ignore */
|
|
800
|
+
...(ngDevMode ? [{ debugName: "fadeFromActive" }] : /* istanbul ignore next */ []));
|
|
801
|
+
#host = inject(ElementRef);
|
|
802
|
+
#scaleCoordinator = inject(ForDrawerScaleCoordinator);
|
|
803
|
+
#drawerStack = inject(ForDrawerStack);
|
|
804
|
+
#parentDrawer = inject(FOR_DRAWER_CONTEXT, { skipSelf: true, optional: true });
|
|
805
|
+
// Pointer / swipe / snap gesture engine. Owns the velocity tracking,
|
|
806
|
+
// per-gesture offset bounds, the dimension-keyed snap-position cache, and
|
|
807
|
+
// the runtime snap re-validation; the directive host-binds the three signals
|
|
808
|
+
// it returns (`dragging`, `dragProgress`, `dragTranslate`) and drives its
|
|
809
|
+
// `validateOnMount()` / `arm()` hooks from its own `afterNextRender`s so the
|
|
810
|
+
// engine's side effects keep `[forDrawer]`'s original ordering relative to
|
|
811
|
+
// the modal shell.
|
|
812
|
+
#drag = injectDrawerDrag({
|
|
813
|
+
side: this.side,
|
|
814
|
+
snapPoints: this.snapPoints,
|
|
815
|
+
closeThreshold: this.closeThreshold,
|
|
816
|
+
handleOnly: this.handleOnly,
|
|
817
|
+
swipeToDismiss: this.swipeToDismiss,
|
|
818
|
+
fadeFromIndex: this.fadeFromIndex,
|
|
819
|
+
activeSnapPoint: this.activeSnapPoint,
|
|
820
|
+
handleEl: this.#handleEl.asReadonly(),
|
|
821
|
+
emitDrag: (event) => this.dragMove.emit(event),
|
|
822
|
+
emitRelease: (event) => this.release.emit(event),
|
|
823
|
+
requestClose: (reason) => this.requestClose(reason),
|
|
824
|
+
});
|
|
825
|
+
/**
|
|
826
|
+
* `true` while a pointer drag gesture is in flight. Mirrors the host's
|
|
827
|
+
* `data-dragging` attribute; surfaced through `ForDrawerContext` so pieces
|
|
828
|
+
* portaled away from the surface (the backdrop) can suppress their own
|
|
829
|
+
* transitions during the gesture.
|
|
830
|
+
*/
|
|
831
|
+
dragging = this.#drag.dragging;
|
|
832
|
+
/**
|
|
833
|
+
* Progress of the current drag toward the anchored edge, `∈ [0, 1]`.
|
|
834
|
+
* Surfaced through `ForDrawerContext` so the backdrop can publish it as the
|
|
835
|
+
* `--for-drawer-drag-progress` custom property.
|
|
836
|
+
*/
|
|
837
|
+
dragProgress = this.#drag.dragProgress;
|
|
838
|
+
/**
|
|
839
|
+
* Live drag displacement as a CSS `translate` value (`"<x> <y>"`),
|
|
840
|
+
* published on the host as the `--for-drawer-translate` custom property by
|
|
841
|
+
* the gesture engine. The consumer composes it on the surface with
|
|
842
|
+
* `translate: var(--for-drawer-translate, 0px 0px)`.
|
|
843
|
+
*
|
|
844
|
+
* A **custom property** is used (rather than writing `translate` /
|
|
845
|
+
* `transform` directly) because `transform` is reserved for
|
|
846
|
+
* `ForDrawerScaleCoordinator` and a directly-written inline `translate` is
|
|
847
|
+
* dropped by Angular when the consumer also binds a template `[style.*]` on
|
|
848
|
+
* the same host. `"0px 0px"` at rest; the write rides the same change-
|
|
849
|
+
* detection pass as the `data-dragging` / `data-active-snap-point` changes
|
|
850
|
+
* on release so the consumer's `transition: translate` animates back in
|
|
851
|
+
* lockstep with the snap transition rather than jumping to the old rest
|
|
852
|
+
* position first.
|
|
853
|
+
*/
|
|
854
|
+
dragTranslate = this.#drag.dragTranslate;
|
|
855
|
+
/** Host element of this drawer surface — exposed via `ForDrawerContext`. */
|
|
856
|
+
get hostElement() {
|
|
857
|
+
return this.#host.nativeElement;
|
|
858
|
+
}
|
|
859
|
+
/** Depth of this drawer in the nesting stack (0 root, 1 first child, …). */
|
|
860
|
+
#depth = signal(0, /* @ts-ignore */
|
|
861
|
+
...(ngDevMode ? [{ debugName: "#depth" }] : /* istanbul ignore next */ []));
|
|
862
|
+
depthAttr = computed(() => String(this.#depth()), /* @ts-ignore */
|
|
863
|
+
...(ngDevMode ? [{ debugName: "depthAttr" }] : /* istanbul ignore next */ []));
|
|
864
|
+
/**
|
|
865
|
+
* `true` while at least one descendant `[forDrawer]` is registered with
|
|
866
|
+
* `ForDrawerStack` underneath this one — reflected as
|
|
867
|
+
* `data-state-nested="true"`. The actual nested-visual transform
|
|
868
|
+
* (`scale + translate3d`) on the parent surface is owned by
|
|
869
|
+
* `ForDrawerScaleCoordinator`, which subscribes to the same stack
|
|
870
|
+
* signal and applies the inline `style.transform` once per affected
|
|
871
|
+
* host. The check is one-level (direct child) because the visual
|
|
872
|
+
* contract only cares about whether *some* child is currently covering
|
|
873
|
+
* this drawer; deeper descendants cascade naturally through their own
|
|
874
|
+
* ancestor chain.
|
|
875
|
+
*/
|
|
876
|
+
hasChild = computed(() => {
|
|
877
|
+
const stack = this.#drawerStack.stack();
|
|
878
|
+
const me = this.#host.nativeElement;
|
|
879
|
+
return stack.some((n) => n.parent === me);
|
|
880
|
+
}, /* @ts-ignore */
|
|
881
|
+
...(ngDevMode ? [{ debugName: "hasChild" }] : /* istanbul ignore next */ []));
|
|
882
|
+
/**
|
|
883
|
+
* Reflected as `data-scale-background` on the host: `true` when the
|
|
884
|
+
* directive opted into the effect AND the coordinator is currently
|
|
885
|
+
* applying it (wrapper registered, not under reduced-motion). Lets
|
|
886
|
+
* consumers style the drawer differently in scale mode (e.g. larger
|
|
887
|
+
* corner radii) without polling the coordinator.
|
|
888
|
+
*/
|
|
889
|
+
scaleBackgroundActive = computed(() => this.scaleBackground() && this.#scaleCoordinator.active(), /* @ts-ignore */
|
|
890
|
+
...(ngDevMode ? [{ debugName: "scaleBackgroundActive" }] : /* istanbul ignore next */ []));
|
|
891
|
+
// Cleanup refs for drawer-specific side effects (the modal-shell owns its
|
|
892
|
+
// portal / dismissable / focus-trap / scroll-lock / inert-siblings teardown;
|
|
893
|
+
// the gesture engine owns its own swipe-listener teardown). They are layered
|
|
894
|
+
// around the shell on SETUP — the drawer-stack push runs BEFORE the shell's
|
|
895
|
+
// afterNextRender so descendants see consistent topology, scale runs AFTER so
|
|
896
|
+
// the coordinator composes on a fully-wired surface — and both are torn down
|
|
897
|
+
// from a single `DestroyRef.onDestroy` (see the constructor).
|
|
898
|
+
#scaleCleanup = null;
|
|
899
|
+
#stackCleanup = null;
|
|
900
|
+
constructor() {
|
|
901
|
+
// ---- Pre-shell setup. Runs in its own afterNextRender registered BEFORE
|
|
902
|
+
// injectModalShell so the drawer-stack push, closeThreshold validation, and
|
|
903
|
+
// snap-point validation precede the shell's dismissable / focus /
|
|
904
|
+
// scroll-lock wiring. Descendants observing `hasChild` / depth see
|
|
905
|
+
// consistent topology throughout their mount sequence; consumers passing bad
|
|
906
|
+
// closeThreshold / snapPoints get a clear error before the shell focuses.
|
|
907
|
+
afterNextRender(() => {
|
|
908
|
+
// 1. Drawer-stack push. The `dragging` signal + resolved
|
|
909
|
+
// nested-transform tunables ride along so `ForDrawerScaleCoordinator`
|
|
910
|
+
// owns the `style.transform` write for the parent surface (issue #180)
|
|
911
|
+
// and reads scope-local defaults through this node.
|
|
912
|
+
const handle = this.#drawerStack.push({
|
|
913
|
+
host: this.#host.nativeElement,
|
|
914
|
+
side: this.side(),
|
|
915
|
+
scaleBackground: this.scaleBackground(),
|
|
916
|
+
parent: this.#parentDrawer?.hostElement ?? null,
|
|
917
|
+
dragging: this.dragging,
|
|
918
|
+
nestedScaleAmount: this.#defaults.nestedScaleAmount ?? 0.93,
|
|
919
|
+
nestedTranslateYpx: this.#defaults.nestedTranslateYpx ?? 8,
|
|
920
|
+
});
|
|
921
|
+
this.#depth.set(handle.depth);
|
|
922
|
+
this.#stackCleanup = handle.cleanup;
|
|
923
|
+
// 2. closeThreshold validation. Throws here so consumers get a clear
|
|
924
|
+
// mount-time error instead of a silently-broken dismissal.
|
|
925
|
+
const ct = this.closeThreshold();
|
|
926
|
+
if (!Number.isFinite(ct) || ct < 0 || ct > 1) {
|
|
927
|
+
throw new Error(`[forty-cdk/drawer] closeThreshold must be in [0, 1], got ${ct}.`);
|
|
928
|
+
}
|
|
929
|
+
// 3. Snap-point validation (shape + fadeFromIndex range + first
|
|
930
|
+
// live-dimension measurement) and mount-time `activeSnapPoint`
|
|
931
|
+
// default, owned by the gesture engine. Runs here — after the stack
|
|
932
|
+
// push and closeThreshold check — so the mount sequence matches what
|
|
933
|
+
// it was before the engine was extracted. Runtime `[snapPoints]`
|
|
934
|
+
// rebinds are re-validated by the engine's own effect.
|
|
935
|
+
this.#drag.validateOnMount();
|
|
936
|
+
});
|
|
937
|
+
// ---- The shared modal-shell. Owns: synchronous return-focus capture
|
|
938
|
+
// (WebKit #136), portal, dismissable layer (triple-veto + composite
|
|
939
|
+
// `interactOutside`), modal vs non-modal branching (focus trap + scroll
|
|
940
|
+
// lock + inert siblings), and the `autoFocusOnOpen` / `autoFocusOnClose`
|
|
941
|
+
// veto hooks. Anything drawer-specific (drag gesture, snap points, scale
|
|
942
|
+
// coordinator, drawer-stack registration, backdrop exemption) stays in
|
|
943
|
+
// this directive / the gesture engine.
|
|
944
|
+
injectModalShell({
|
|
945
|
+
modal: this.modal,
|
|
946
|
+
returnFocus: this.returnFocus,
|
|
947
|
+
initialFocus: this.initialFocus,
|
|
948
|
+
container: this.container,
|
|
949
|
+
autoFocusOnOpen: () => this.autoFocusOnOpen(),
|
|
950
|
+
autoFocusOnClose: () => this.autoFocusOnClose(),
|
|
951
|
+
dismiss: {
|
|
952
|
+
dismissible: this.dismissible,
|
|
953
|
+
requestClose: (reason) => this.requestClose(reason),
|
|
954
|
+
emitEscapeKeyDown: (veto) => this.escapeKeyDown.emit(veto),
|
|
955
|
+
emitPointerDownOutside: (veto) => this.pointerDownOutside.emit(veto),
|
|
956
|
+
emitFocusOutside: (veto) => this.focusOutside.emit(veto),
|
|
957
|
+
emitInteractOutside: (veto) => this.interactOutside.emit(veto),
|
|
958
|
+
// The backdrop is portaled to body (sibling of the drawer host), so
|
|
959
|
+
// without exemption a pointer-down on it fires `pointerDownOutside`
|
|
960
|
+
// BEFORE the backdrop's click handler runs — the drawer would close
|
|
961
|
+
// with the wrong reason. Marking it exempt routes the gesture
|
|
962
|
+
// through the backdrop's own click → `requestClose('backdrop')`
|
|
963
|
+
// path instead. The handle is a child of the host so it is already
|
|
964
|
+
// covered by `host.contains(target)`.
|
|
965
|
+
exemptElements: () => {
|
|
966
|
+
const backdrop = this.#backdropEl();
|
|
967
|
+
return backdrop ? [backdrop] : [];
|
|
968
|
+
},
|
|
969
|
+
},
|
|
970
|
+
});
|
|
971
|
+
// ---- Post-shell setup. Runs in its own afterNextRender registered
|
|
972
|
+
// AFTER injectModalShell so swipe-dismiss arms on a host already attached
|
|
973
|
+
// to body via the shell's portal, and the scale coordinator composes on
|
|
974
|
+
// top of a fully-wired surface.
|
|
975
|
+
afterNextRender(() => {
|
|
976
|
+
// Register with the scale coordinator last so the wrapper transition
|
|
977
|
+
// composes after every other side effect has stabilised. The coordinator
|
|
978
|
+
// enforces the reduced-motion + wrapper-presence gates.
|
|
979
|
+
if (this.scaleBackground()) {
|
|
980
|
+
this.#scaleCleanup = this.#scaleCoordinator.registerDrawer({
|
|
981
|
+
setBackgroundColorOnScale: this.setBackgroundColorOnScale(),
|
|
982
|
+
scaleAmount: this.#defaults.scaleAmount ?? 0.95,
|
|
983
|
+
scaleTranslateYpx: this.#defaults.scaleTranslateYpx ?? 14,
|
|
984
|
+
scaleBorderRadiusPx: this.#defaults.scaleBorderRadiusPx ?? 8,
|
|
985
|
+
scaleBackgroundColor: this.#defaults.scaleBackgroundColor ?? 'black',
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
// Arm the swipe gate now that the surface is fully wired. The gesture
|
|
989
|
+
// engine's effect owns the actual attach/detach; arming unblocks it.
|
|
990
|
+
this.#drag.arm();
|
|
991
|
+
});
|
|
992
|
+
// ---- Drawer-owned teardown, in one hook. Neither step depends on
|
|
993
|
+
// running before or after the shell's dismissable / focus-trap /
|
|
994
|
+
// scroll-lock teardown: the scale handle is self-contained (pops the scale
|
|
995
|
+
// stack), the swipe listeners are detached by the gesture engine's own
|
|
996
|
+
// destroy hook, and the drawer-stack pop only requires that descendant
|
|
997
|
+
// *drawers* are already popped — which Angular guarantees by destroying
|
|
998
|
+
// child components before their parents. Scale cleanup runs first, then the
|
|
999
|
+
// drawer-stack pop (so any sibling reads of the stack during scale cleanup
|
|
1000
|
+
// still see this node; `ForDrawerStack.cleanup` throws if a descendant is
|
|
1001
|
+
// still registered, surfacing consumer template bugs). The portal's own
|
|
1002
|
+
// destroy hook still removes the host from the DOM after this runs, so
|
|
1003
|
+
// return-focus is unaffected.
|
|
1004
|
+
inject(DestroyRef).onDestroy(() => {
|
|
1005
|
+
this.#scaleCleanup?.();
|
|
1006
|
+
this.#scaleCleanup = null;
|
|
1007
|
+
this.#stackCleanup?.();
|
|
1008
|
+
this.#stackCleanup = null;
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
// ---- Context implementation ----
|
|
1012
|
+
registerLabel(id) {
|
|
1013
|
+
this.#labelIds.update((arr) => (arr.includes(id) ? arr : [...arr, id]));
|
|
1014
|
+
}
|
|
1015
|
+
unregisterLabel(id) {
|
|
1016
|
+
this.#labelIds.update((arr) => arr.filter((x) => x !== id));
|
|
1017
|
+
}
|
|
1018
|
+
registerDescription(id) {
|
|
1019
|
+
this.#describedByIds.update((arr) => (arr.includes(id) ? arr : [...arr, id]));
|
|
1020
|
+
}
|
|
1021
|
+
unregisterDescription(id) {
|
|
1022
|
+
this.#describedByIds.update((arr) => arr.filter((x) => x !== id));
|
|
1023
|
+
}
|
|
1024
|
+
registerHandle(el) {
|
|
1025
|
+
const current = this.#handleEl();
|
|
1026
|
+
if (el !== null && current !== null && current !== el) {
|
|
1027
|
+
throw new Error('[forty-cdk/drawer] Multiple [forDrawerHandle] inside the same [forDrawer]; only one is allowed.');
|
|
1028
|
+
}
|
|
1029
|
+
this.#handleEl.set(el);
|
|
1030
|
+
}
|
|
1031
|
+
registerBackdrop(el) {
|
|
1032
|
+
const current = this.#backdropEl();
|
|
1033
|
+
if (el !== null && current !== null && current !== el) {
|
|
1034
|
+
throw new Error('[forty-cdk/drawer] Multiple [forDrawerBackdrop] inside the same [forDrawer]; only one is allowed.');
|
|
1035
|
+
}
|
|
1036
|
+
this.#backdropEl.set(el);
|
|
1037
|
+
}
|
|
1038
|
+
requestClose(reason, value) {
|
|
1039
|
+
if (reason !== 'closeButton' && reason !== 'programmatic' && !this.dismissible()) {
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
this.#lastCloseValue.set(value);
|
|
1043
|
+
this.dismiss.emit(reason);
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* The `value` argument from the most recent `requestClose(reason, value)`
|
|
1047
|
+
* call. Read by `ForDrawerManager` to bridge `[forDrawerClose] [closeWith]`
|
|
1048
|
+
* into `ForDrawerRef.close(value)`. Declarative consumers never need this —
|
|
1049
|
+
* it is plumbing for the imperative bootstrap path.
|
|
1050
|
+
*
|
|
1051
|
+
* @internal
|
|
1052
|
+
*/
|
|
1053
|
+
lastCloseValue = this.#lastCloseValue.asReadonly();
|
|
1054
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawer, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1055
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForDrawer, isStandalone: true, selector: "[forDrawer]", inputs: { side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, alert: { classPropertyName: "alert", publicName: "alert", isSignal: true, isRequired: false, transformFunction: null }, returnFocus: { classPropertyName: "returnFocus", publicName: "returnFocus", isSignal: true, isRequired: false, transformFunction: null }, initialFocus: { classPropertyName: "initialFocus", publicName: "initialFocus", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, container: { classPropertyName: "container", publicName: "container", isSignal: true, isRequired: false, transformFunction: null }, autoFocusOnOpen: { classPropertyName: "autoFocusOnOpen", publicName: "autoFocusOnOpen", isSignal: true, isRequired: false, transformFunction: null }, autoFocusOnClose: { classPropertyName: "autoFocusOnClose", publicName: "autoFocusOnClose", isSignal: true, isRequired: false, transformFunction: null }, swipeToDismiss: { classPropertyName: "swipeToDismiss", publicName: "swipeToDismiss", isSignal: true, isRequired: false, transformFunction: null }, closeThreshold: { classPropertyName: "closeThreshold", publicName: "closeThreshold", isSignal: true, isRequired: false, transformFunction: null }, handleOnly: { classPropertyName: "handleOnly", publicName: "handleOnly", isSignal: true, isRequired: false, transformFunction: null }, scaleBackground: { classPropertyName: "scaleBackground", publicName: "scaleBackground", isSignal: true, isRequired: false, transformFunction: null }, setBackgroundColorOnScale: { classPropertyName: "setBackgroundColorOnScale", publicName: "setBackgroundColorOnScale", isSignal: true, isRequired: false, transformFunction: null }, snapPoints: { classPropertyName: "snapPoints", publicName: "snapPoints", isSignal: true, isRequired: false, transformFunction: null }, activeSnapPoint: { classPropertyName: "activeSnapPoint", publicName: "activeSnapPoint", isSignal: true, isRequired: false, transformFunction: null }, fadeFromIndex: { classPropertyName: "fadeFromIndex", publicName: "fadeFromIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeSnapPoint: "activeSnapPointChange", dismiss: "dismiss", escapeKeyDown: "escapeKeyDown", pointerDownOutside: "pointerDownOutside", focusOutside: "focusOutside", interactOutside: "interactOutside", dragMove: "dragMove", release: "release" }, host: { attributes: { "data-state": "open", "tabindex": "-1" }, properties: { "attr.role": "alert() ? \"alertdialog\" : \"dialog\"", "attr.aria-modal": "modal() ? \"true\" : null", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "labelledBy()", "attr.aria-describedby": "describedBy()", "attr.data-side": "side()", "attr.data-active-snap-point": "activeSnapPointAttr()", "attr.data-dragging": "dragging() ? \"\" : null", "attr.data-scale-background": "scaleBackgroundActive() ? \"\" : null", "attr.data-depth": "depthAttr()", "attr.data-state-nested": "hasChild() ? \"true\" : null" } }, providers: [{ provide: FOR_DRAWER_CONTEXT, useExisting: ForDrawer }], exportAs: ["forDrawer"], ngImport: i0 });
|
|
1056
|
+
}
|
|
1057
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawer, decorators: [{
|
|
1058
|
+
type: Directive,
|
|
1059
|
+
args: [{
|
|
1060
|
+
selector: '[forDrawer]',
|
|
1061
|
+
exportAs: 'forDrawer',
|
|
1062
|
+
host: {
|
|
1063
|
+
'[attr.role]': 'alert() ? "alertdialog" : "dialog"',
|
|
1064
|
+
'[attr.aria-modal]': 'modal() ? "true" : null',
|
|
1065
|
+
'[attr.aria-label]': 'ariaLabel() || null',
|
|
1066
|
+
'[attr.aria-labelledby]': 'labelledBy()',
|
|
1067
|
+
'[attr.aria-describedby]': 'describedBy()',
|
|
1068
|
+
'[attr.data-side]': 'side()',
|
|
1069
|
+
'[attr.data-active-snap-point]': 'activeSnapPointAttr()',
|
|
1070
|
+
'[attr.data-dragging]': 'dragging() ? "" : null',
|
|
1071
|
+
'[attr.data-scale-background]': 'scaleBackgroundActive() ? "" : null',
|
|
1072
|
+
'[attr.data-depth]': 'depthAttr()',
|
|
1073
|
+
'[attr.data-state-nested]': 'hasChild() ? "true" : null',
|
|
1074
|
+
'data-state': 'open',
|
|
1075
|
+
tabindex: '-1',
|
|
1076
|
+
},
|
|
1077
|
+
providers: [{ provide: FOR_DRAWER_CONTEXT, useExisting: ForDrawer }],
|
|
1078
|
+
}]
|
|
1079
|
+
}], ctorParameters: () => [], propDecorators: { side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], alert: [{ type: i0.Input, args: [{ isSignal: true, alias: "alert", required: false }] }], returnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "returnFocus", required: false }] }], initialFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialFocus", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], container: [{ type: i0.Input, args: [{ isSignal: true, alias: "container", required: false }] }], autoFocusOnOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFocusOnOpen", required: false }] }], autoFocusOnClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFocusOnClose", required: false }] }], swipeToDismiss: [{ type: i0.Input, args: [{ isSignal: true, alias: "swipeToDismiss", required: false }] }], closeThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeThreshold", required: false }] }], handleOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "handleOnly", required: false }] }], scaleBackground: [{ type: i0.Input, args: [{ isSignal: true, alias: "scaleBackground", required: false }] }], setBackgroundColorOnScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "setBackgroundColorOnScale", required: false }] }], snapPoints: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapPoints", required: false }] }], activeSnapPoint: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeSnapPoint", required: false }] }, { type: i0.Output, args: ["activeSnapPointChange"] }], fadeFromIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "fadeFromIndex", required: false }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }], escapeKeyDown: [{ type: i0.Output, args: ["escapeKeyDown"] }], pointerDownOutside: [{ type: i0.Output, args: ["pointerDownOutside"] }], focusOutside: [{ type: i0.Output, args: ["focusOutside"] }], interactOutside: [{ type: i0.Output, args: ["interactOutside"] }], dragMove: [{ type: i0.Output, args: ["dragMove"] }], release: [{ type: i0.Output, args: ["release"] }] } });
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Button that toggles the drawer when clicked. Apply on a focusable element —
|
|
1083
|
+
* preferably a `<button>` — so keyboard users can reach it.
|
|
1084
|
+
*
|
|
1085
|
+
* Wires `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls`, and
|
|
1086
|
+
* `data-state` on the host. Two-way bind `[(open)]` to the same signal that
|
|
1087
|
+
* gates the surrounding `@if` around `[forDrawer]`. Focus return is owned
|
|
1088
|
+
* by `[forDrawer]` (modal mode captures the previously-focused element on
|
|
1089
|
+
* mount and restores it on destroy).
|
|
1090
|
+
*
|
|
1091
|
+
* ```html
|
|
1092
|
+
* <button forDrawerTrigger [(open)]="drawerOpen" controls="my-drawer">Open</button>
|
|
1093
|
+
* @if (drawerOpen()) {
|
|
1094
|
+
* <div forDrawer id="my-drawer" (dismiss)="drawerOpen.set(false)">…</div>
|
|
1095
|
+
* }
|
|
1096
|
+
* ```
|
|
1097
|
+
*/
|
|
1098
|
+
class ForDrawerTrigger {
|
|
1099
|
+
/**
|
|
1100
|
+
* Two-way bindable. Bind to the same signal that gates the surrounding
|
|
1101
|
+
* `@if` around `[forDrawer]`. The `model()` change emitter
|
|
1102
|
+
* (`(openChange)`) fires only on internal transitions (trigger click).
|
|
1103
|
+
*/
|
|
1104
|
+
open = model(false, /* @ts-ignore */
|
|
1105
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
1106
|
+
/**
|
|
1107
|
+
* Id of the controlled drawer surface. Mirrored to `aria-controls` while
|
|
1108
|
+
* the drawer is open. The consumer is responsible for setting the same
|
|
1109
|
+
* `id` on `[forDrawer]`.
|
|
1110
|
+
*/
|
|
1111
|
+
controls = input(null, /* @ts-ignore */
|
|
1112
|
+
...(ngDevMode ? [{ debugName: "controls" }] : /* istanbul ignore next */ []));
|
|
1113
|
+
/**
|
|
1114
|
+
* When true, click is ignored and the host reflects `data-disabled=""`,
|
|
1115
|
+
* `aria-disabled="true"`, and the native `disabled` attribute so the
|
|
1116
|
+
* trigger is announced as disabled by assistive tech and dropped from
|
|
1117
|
+
* the tab order.
|
|
1118
|
+
*/
|
|
1119
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1120
|
+
#host = inject(ElementRef);
|
|
1121
|
+
constructor() {
|
|
1122
|
+
reflectDisabled(this.disabled);
|
|
1123
|
+
}
|
|
1124
|
+
onClick() {
|
|
1125
|
+
if (this.disabled()) {
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
// Same WebKit return-focus quirk as ForDialogTrigger (#136): force focus
|
|
1129
|
+
// back onto the trigger before opening so the drawer captures it as the
|
|
1130
|
+
// return target.
|
|
1131
|
+
this.#host.nativeElement.focus();
|
|
1132
|
+
this.open.update((v) => !v);
|
|
1133
|
+
}
|
|
1134
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1135
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForDrawerTrigger, isStandalone: true, selector: "[forDrawerTrigger]", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, controls: { classPropertyName: "controls", publicName: "controls", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { attributes: { "type": "button" }, listeners: { "click": "onClick()" }, properties: { "attr.aria-haspopup": "\"dialog\"", "attr.aria-expanded": "open() ? \"true\" : \"false\"", "attr.aria-controls": "open() ? controls() : null", "attr.data-state": "open() ? \"open\" : \"closed\"", "attr.data-disabled": "disabled() ? \"\" : null", "attr.aria-disabled": "disabled() ? \"true\" : null" } }, exportAs: ["forDrawerTrigger"], ngImport: i0 });
|
|
1136
|
+
}
|
|
1137
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerTrigger, decorators: [{
|
|
1138
|
+
type: Directive,
|
|
1139
|
+
args: [{
|
|
1140
|
+
selector: '[forDrawerTrigger]',
|
|
1141
|
+
exportAs: 'forDrawerTrigger',
|
|
1142
|
+
host: {
|
|
1143
|
+
type: 'button',
|
|
1144
|
+
'[attr.aria-haspopup]': '"dialog"',
|
|
1145
|
+
'[attr.aria-expanded]': 'open() ? "true" : "false"',
|
|
1146
|
+
'[attr.aria-controls]': 'open() ? controls() : null',
|
|
1147
|
+
'[attr.data-state]': 'open() ? "open" : "closed"',
|
|
1148
|
+
'[attr.data-disabled]': 'disabled() ? "" : null',
|
|
1149
|
+
'[attr.aria-disabled]': 'disabled() ? "true" : null',
|
|
1150
|
+
'(click)': 'onClick()',
|
|
1151
|
+
},
|
|
1152
|
+
}]
|
|
1153
|
+
}], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], controls: [{ type: i0.Input, args: [{ isSignal: true, alias: "controls", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Optional backdrop overlay. Portaled to `document.body` so it sits
|
|
1157
|
+
* underneath the drawer regardless of where it's declared. While mounted
|
|
1158
|
+
* the backdrop is visible; mount/unmount it alongside the drawer with the
|
|
1159
|
+
* same `@if` so `animate.enter` / `animate.leave` works on both.
|
|
1160
|
+
*
|
|
1161
|
+
* Reflects `data-fade-from-active` when `fadeFromIndex` on the drawer root
|
|
1162
|
+
* is set and the active snap point is at or past that index — consumers
|
|
1163
|
+
* tie this to a CSS opacity transition for the "backdrop fades
|
|
1164
|
+
* in once you snap up past N" effect.
|
|
1165
|
+
*
|
|
1166
|
+
* Publishes the live drag progress toward the anchored edge as the
|
|
1167
|
+
* `--for-drawer-drag-progress` custom property (`0` at rest → `1` fully
|
|
1168
|
+
* dragged off-screen) and mirrors the surface's `data-dragging` attribute.
|
|
1169
|
+
* Together these drive the "backdrop fades out as you swipe to
|
|
1170
|
+
* dismiss" effect with pure CSS:
|
|
1171
|
+
*
|
|
1172
|
+
* ```css
|
|
1173
|
+
* [forDrawerBackdrop] {
|
|
1174
|
+
* opacity: calc(1 - var(--for-drawer-drag-progress, 0));
|
|
1175
|
+
* transition: opacity 0.3s ease;
|
|
1176
|
+
* }
|
|
1177
|
+
* [forDrawerBackdrop][data-dragging] {
|
|
1178
|
+
* transition: none; \/* track the pointer 1:1 *\/
|
|
1179
|
+
* }
|
|
1180
|
+
* ```
|
|
1181
|
+
*
|
|
1182
|
+
* The directive applies no visual styles itself.
|
|
1183
|
+
*/
|
|
1184
|
+
class ForDrawerBackdrop {
|
|
1185
|
+
ctx = injectDrawerContext('ForDrawerBackdrop');
|
|
1186
|
+
#host = inject(ElementRef);
|
|
1187
|
+
/**
|
|
1188
|
+
* Per-instance drawer id when opened through `ForDrawerManager` (reflected
|
|
1189
|
+
* as `data-for-drawer-id` so the manager can pair this portaled backdrop
|
|
1190
|
+
* with its drawer and drive its exit animation). `null` in the declarative
|
|
1191
|
+
* path, where the host binding emits no attribute.
|
|
1192
|
+
*/
|
|
1193
|
+
instanceId = inject(FOR_DRAWER_INSTANCE_ID, { optional: true });
|
|
1194
|
+
constructor() {
|
|
1195
|
+
injectPortal({ target: this.ctx.container });
|
|
1196
|
+
// Register so the drawer's dismissable layer treats pointer-down on the
|
|
1197
|
+
// backdrop as "inside" — see ForDrawerContext#registerBackdrop.
|
|
1198
|
+
this.ctx.registerBackdrop(this.#host.nativeElement);
|
|
1199
|
+
inject(DestroyRef).onDestroy(() => this.ctx.registerBackdrop(null));
|
|
1200
|
+
}
|
|
1201
|
+
onClick(event) {
|
|
1202
|
+
// Only close on direct backdrop click, not events bubbled from a child.
|
|
1203
|
+
if (event.target === event.currentTarget) {
|
|
1204
|
+
this.ctx.requestClose('backdrop');
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerBackdrop, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1208
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDrawerBackdrop, isStandalone: true, selector: "[forDrawerBackdrop]", host: { attributes: { "data-for-drawer-backdrop": "", "data-for-modal-peer": "", "data-state": "open" }, listeners: { "click": "onClick($event)" }, properties: { "attr.data-for-drawer-id": "instanceId", "attr.data-fade-from-active": "ctx.fadeFromActive() ? \"\" : null", "attr.data-dragging": "ctx.dragging() ? \"\" : null", "style.--for-drawer-drag-progress": "ctx.dragProgress()" } }, exportAs: ["forDrawerBackdrop"], ngImport: i0 });
|
|
1209
|
+
}
|
|
1210
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerBackdrop, decorators: [{
|
|
1211
|
+
type: Directive,
|
|
1212
|
+
args: [{
|
|
1213
|
+
selector: '[forDrawerBackdrop]',
|
|
1214
|
+
exportAs: 'forDrawerBackdrop',
|
|
1215
|
+
host: {
|
|
1216
|
+
'data-for-drawer-backdrop': '',
|
|
1217
|
+
// Marker the inert-siblings utility looks for so the backdrop, which is
|
|
1218
|
+
// portaled to body alongside the drawer, is not inerted alongside the
|
|
1219
|
+
// rest of the document.
|
|
1220
|
+
'data-for-modal-peer': '',
|
|
1221
|
+
'data-state': 'open',
|
|
1222
|
+
'[attr.data-for-drawer-id]': 'instanceId',
|
|
1223
|
+
'[attr.data-fade-from-active]': 'ctx.fadeFromActive() ? "" : null',
|
|
1224
|
+
'[attr.data-dragging]': 'ctx.dragging() ? "" : null',
|
|
1225
|
+
'[style.--for-drawer-drag-progress]': 'ctx.dragProgress()',
|
|
1226
|
+
'(click)': 'onClick($event)',
|
|
1227
|
+
},
|
|
1228
|
+
}]
|
|
1229
|
+
}], ctorParameters: () => [] });
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Visual swipe handle (the "grabber" pill at the top of a bottom sheet).
|
|
1233
|
+
* Registers itself with the drawer so that `[handleOnly]="true"` can scope
|
|
1234
|
+
* the swipe gesture to gestures starting on this element. Reflects
|
|
1235
|
+
* `aria-hidden="true"` because the handle is a decorative styling target —
|
|
1236
|
+
* keyboard users dismiss via Escape / a `[forDrawerClose]` button.
|
|
1237
|
+
*
|
|
1238
|
+
* Apply on the visual handle element. Applies no styles itself; the
|
|
1239
|
+
* consumer is responsible for sizing, positioning, and color.
|
|
1240
|
+
*/
|
|
1241
|
+
class ForDrawerHandle {
|
|
1242
|
+
#ctx = injectDrawerContext('ForDrawerHandle');
|
|
1243
|
+
#host = inject(ElementRef);
|
|
1244
|
+
constructor() {
|
|
1245
|
+
this.#ctx.registerHandle(this.#host.nativeElement);
|
|
1246
|
+
inject(DestroyRef).onDestroy(() => this.#ctx.registerHandle(null));
|
|
1247
|
+
}
|
|
1248
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerHandle, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1249
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDrawerHandle, isStandalone: true, selector: "[forDrawerHandle]", host: { attributes: { "aria-hidden": "true", "data-for-drawer-handle": "" } }, exportAs: ["forDrawerHandle"], ngImport: i0 });
|
|
1250
|
+
}
|
|
1251
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerHandle, decorators: [{
|
|
1252
|
+
type: Directive,
|
|
1253
|
+
args: [{
|
|
1254
|
+
selector: '[forDrawerHandle]',
|
|
1255
|
+
exportAs: 'forDrawerHandle',
|
|
1256
|
+
host: {
|
|
1257
|
+
'aria-hidden': 'true',
|
|
1258
|
+
'data-for-drawer-handle': '',
|
|
1259
|
+
},
|
|
1260
|
+
}]
|
|
1261
|
+
}], ctorParameters: () => [] });
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Convenience: gives the host element a stable id and registers it as the
|
|
1265
|
+
* drawer's accessible name (`aria-labelledby`). Apply on a heading element
|
|
1266
|
+
* (`<h2>`, `<h3>`...) inside `[forDrawer]`.
|
|
1267
|
+
*
|
|
1268
|
+
* If you don't render visible title text, omit this and pass `ariaLabel`
|
|
1269
|
+
* on the drawer root instead.
|
|
1270
|
+
*/
|
|
1271
|
+
class ForDrawerTitle {
|
|
1272
|
+
id = registerA11yName(injectDrawerContext('ForDrawerTitle'), 'for-drawer-title');
|
|
1273
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerTitle, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1274
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDrawerTitle, isStandalone: true, selector: "[forDrawerTitle]", host: { properties: { "id": "id()" } }, exportAs: ["forDrawerTitle"], ngImport: i0 });
|
|
1275
|
+
}
|
|
1276
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerTitle, decorators: [{
|
|
1277
|
+
type: Directive,
|
|
1278
|
+
args: [{
|
|
1279
|
+
selector: '[forDrawerTitle]',
|
|
1280
|
+
exportAs: 'forDrawerTitle',
|
|
1281
|
+
host: {
|
|
1282
|
+
'[id]': 'id()',
|
|
1283
|
+
},
|
|
1284
|
+
}]
|
|
1285
|
+
}] });
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Convenience: gives the host element a stable id and registers it as the
|
|
1289
|
+
* drawer's accessible description (`aria-describedby`). Apply on a `<p>` or
|
|
1290
|
+
* other element inside `[forDrawer]`. Optional — only use when there's
|
|
1291
|
+
* non-title supporting copy.
|
|
1292
|
+
*/
|
|
1293
|
+
class ForDrawerDescription {
|
|
1294
|
+
id = registerA11yDescription(injectDrawerContext('ForDrawerDescription'), 'for-drawer-description');
|
|
1295
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerDescription, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1296
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDrawerDescription, isStandalone: true, selector: "[forDrawerDescription]", host: { properties: { "id": "id()" } }, exportAs: ["forDrawerDescription"], ngImport: i0 });
|
|
1297
|
+
}
|
|
1298
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerDescription, decorators: [{
|
|
1299
|
+
type: Directive,
|
|
1300
|
+
args: [{
|
|
1301
|
+
selector: '[forDrawerDescription]',
|
|
1302
|
+
exportAs: 'forDrawerDescription',
|
|
1303
|
+
host: {
|
|
1304
|
+
'[id]': 'id()',
|
|
1305
|
+
},
|
|
1306
|
+
}]
|
|
1307
|
+
}] });
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Button that closes the surrounding drawer when clicked. Apply on a
|
|
1311
|
+
* `<button>` inside `[forDrawer]`. Pass `[closeWith]` to propagate a return
|
|
1312
|
+
* value to `ForDrawerRef.close()` in programmatic usage; ignored in
|
|
1313
|
+
* declarative usage.
|
|
1314
|
+
*/
|
|
1315
|
+
class ForDrawerClose {
|
|
1316
|
+
#ctx = injectDrawerContext('ForDrawerClose');
|
|
1317
|
+
/** Optional value passed to `ForDrawerRef.close()` (programmatic mode). */
|
|
1318
|
+
closeWith = input(undefined, /* @ts-ignore */
|
|
1319
|
+
...(ngDevMode ? [{ debugName: "closeWith" }] : /* istanbul ignore next */ []));
|
|
1320
|
+
onClick() {
|
|
1321
|
+
this.#ctx.requestClose('closeButton', this.closeWith());
|
|
1322
|
+
}
|
|
1323
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerClose, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1324
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForDrawerClose, isStandalone: true, selector: "[forDrawerClose]", inputs: { closeWith: { classPropertyName: "closeWith", publicName: "closeWith", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button", "data-state": "open" }, listeners: { "click": "onClick()" } }, exportAs: ["forDrawerClose"], ngImport: i0 });
|
|
1325
|
+
}
|
|
1326
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerClose, decorators: [{
|
|
1327
|
+
type: Directive,
|
|
1328
|
+
args: [{
|
|
1329
|
+
selector: '[forDrawerClose]',
|
|
1330
|
+
exportAs: 'forDrawerClose',
|
|
1331
|
+
host: {
|
|
1332
|
+
type: 'button',
|
|
1333
|
+
'data-state': 'open',
|
|
1334
|
+
'(click)': 'onClick()',
|
|
1335
|
+
},
|
|
1336
|
+
}]
|
|
1337
|
+
}], propDecorators: { closeWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeWith", required: false }] }] } });
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Marks the consumer's app-shell wrapper so {@link ForDrawer}'s
|
|
1341
|
+
* `scaleBackground` effect can transform the rest of the page when a
|
|
1342
|
+
* drawer opens — applied through Angular DI rather than a global selector.
|
|
1343
|
+
*
|
|
1344
|
+
* Apply on the element that contains everything the drawer should appear
|
|
1345
|
+
* to recede behind (typically the root shell). Reflects
|
|
1346
|
+
* `data-state="scaled"` while the effect is active and `"idle"` at rest
|
|
1347
|
+
* — useful for layering CSS styling without re-querying the coordinator.
|
|
1348
|
+
*
|
|
1349
|
+
* Only one wrapper may be registered at a time; mounting a second
|
|
1350
|
+
* `[forDrawerWrapper]` while another is alive throws. Drawers that opt
|
|
1351
|
+
* into `scaleBackground` without a registered wrapper are a no-op (no
|
|
1352
|
+
* effect applied) so test composition and partial consumer adoption
|
|
1353
|
+
* remain safe.
|
|
1354
|
+
*/
|
|
1355
|
+
class ForDrawerWrapper {
|
|
1356
|
+
#coordinator = inject(ForDrawerScaleCoordinator);
|
|
1357
|
+
#host = inject(ElementRef);
|
|
1358
|
+
/** `'scaled'` while the coordinator is applying the effect, otherwise `'idle'`. */
|
|
1359
|
+
state = computed(() => this.#coordinator.active() ? 'scaled' : 'idle', /* @ts-ignore */
|
|
1360
|
+
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
1361
|
+
constructor() {
|
|
1362
|
+
const cleanup = this.#coordinator.registerWrapper(this.#host.nativeElement);
|
|
1363
|
+
inject(DestroyRef).onDestroy(cleanup);
|
|
1364
|
+
}
|
|
1365
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerWrapper, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1366
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDrawerWrapper, isStandalone: true, selector: "[forDrawerWrapper]", host: { properties: { "attr.data-state": "state()" } }, exportAs: ["forDrawerWrapper"], ngImport: i0 });
|
|
1367
|
+
}
|
|
1368
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerWrapper, decorators: [{
|
|
1369
|
+
type: Directive,
|
|
1370
|
+
args: [{
|
|
1371
|
+
selector: '[forDrawerWrapper]',
|
|
1372
|
+
exportAs: 'forDrawerWrapper',
|
|
1373
|
+
host: {
|
|
1374
|
+
'[attr.data-state]': 'state()',
|
|
1375
|
+
},
|
|
1376
|
+
}]
|
|
1377
|
+
}], ctorParameters: () => [] });
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* Handle returned by `ForDrawerManager.open()`. Inject inside the opened
|
|
1381
|
+
* component to drive close imperatively. Both reactive (`isClosed`, `result`)
|
|
1382
|
+
* and awaitable (`closed`) APIs are exposed — pick what fits the call site.
|
|
1383
|
+
*
|
|
1384
|
+
* `R` is the close-result type. Default `unknown`; specify it on
|
|
1385
|
+
* `manager.open<C, R>(...)` to get type safety.
|
|
1386
|
+
*/
|
|
1387
|
+
class ForDrawerRef extends OverlayRef {
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* @internal Exposes the element injector at a child of the row's `[forDrawer]`
|
|
1392
|
+
* element so the user component rendered via `NgComponentOutlet` resolves
|
|
1393
|
+
* `FOR_DRAWER_CONTEXT` from the enclosing `[forDrawer]` host. The shared body
|
|
1394
|
+
* lives in `OverlayContextInjector`; this only carries the drawer selector.
|
|
1395
|
+
*/
|
|
1396
|
+
class ForDrawerContextInjector extends OverlayContextInjector {
|
|
1397
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerContextInjector, deps: null, target: i0.ɵɵFactoryTarget.Directive });
|
|
1398
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDrawerContextInjector, isStandalone: true, selector: "[forDrawerContextInjector]", exportAs: ["forDrawerContextInjector"], usesInheritance: true, ngImport: i0 });
|
|
1399
|
+
}
|
|
1400
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerContextInjector, decorators: [{
|
|
1401
|
+
type: Directive,
|
|
1402
|
+
args: [{
|
|
1403
|
+
selector: '[forDrawerContextInjector]',
|
|
1404
|
+
exportAs: 'forDrawerContextInjector',
|
|
1405
|
+
}]
|
|
1406
|
+
}] });
|
|
1407
|
+
/**
|
|
1408
|
+
* @internal Outlet component created once by `ForDrawerManager` on the first
|
|
1409
|
+
* `open()` call. Renders every live entry from the manager's entries signal
|
|
1410
|
+
* with `@for`, so Angular's control-flow unmount triggers `animate.leave` on
|
|
1411
|
+
* close — identical to the declarative `@if (open()) { <div forDrawer …> }` path.
|
|
1412
|
+
*
|
|
1413
|
+
* Constructed exclusively by `ForDrawerManager`. Not exported from
|
|
1414
|
+
* `public-api.ts`.
|
|
1415
|
+
*/
|
|
1416
|
+
class ForDrawerOutlet {
|
|
1417
|
+
#destroyRef = inject(DestroyRef);
|
|
1418
|
+
#host = null;
|
|
1419
|
+
/**
|
|
1420
|
+
* @internal Called once by `ForDrawerManager` right after creating this
|
|
1421
|
+
* outlet to wire the reactive entries accessor and the destroy hook.
|
|
1422
|
+
*/
|
|
1423
|
+
init(host) {
|
|
1424
|
+
this.#host = host;
|
|
1425
|
+
this.#destroyRef.onDestroy(() => host.closeAllForDestroy());
|
|
1426
|
+
}
|
|
1427
|
+
entries() {
|
|
1428
|
+
return this.#host?.entries() ?? [];
|
|
1429
|
+
}
|
|
1430
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerOutlet, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1431
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ForDrawerOutlet, isStandalone: true, selector: "for-drawer-outlet", ngImport: i0, template: `
|
|
1432
|
+
@for (entry of entries(); track entry.id) {
|
|
1433
|
+
<div
|
|
1434
|
+
forDrawer
|
|
1435
|
+
[attr.data-for-drawer-id]="entry.id"
|
|
1436
|
+
[animate.enter]="entry.animateEnter ?? ''"
|
|
1437
|
+
[class]="entry.hostClass"
|
|
1438
|
+
[side]="entry.side ?? 'bottom'"
|
|
1439
|
+
[dismissible]="entry.dismissible ?? true"
|
|
1440
|
+
[modal]="entry.modal ?? true"
|
|
1441
|
+
[alert]="entry.alert ?? false"
|
|
1442
|
+
[returnFocus]="entry.returnFocus ?? true"
|
|
1443
|
+
[initialFocus]="entry.initialFocus ?? 'first'"
|
|
1444
|
+
[ariaLabel]="entry.ariaLabel ?? null"
|
|
1445
|
+
[container]="entry.container ?? null"
|
|
1446
|
+
[autoFocusOnOpen]="entry.autoFocusOnOpen"
|
|
1447
|
+
[autoFocusOnClose]="entry.autoFocusOnClose"
|
|
1448
|
+
[swipeToDismiss]="entry.swipeToDismiss ?? true"
|
|
1449
|
+
[closeThreshold]="entry.closeThreshold ?? 0.25"
|
|
1450
|
+
[handleOnly]="entry.handleOnly ?? false"
|
|
1451
|
+
[scaleBackground]="entry.scaleBackground ?? false"
|
|
1452
|
+
[setBackgroundColorOnScale]="entry.setBackgroundColorOnScale ?? true"
|
|
1453
|
+
[snapPoints]="entry.snapPoints"
|
|
1454
|
+
[activeSnapPoint]="entry.activeSnapPoint ?? null"
|
|
1455
|
+
[fadeFromIndex]="entry.fadeFromIndex"
|
|
1456
|
+
(dismiss)="entry.handleClose(fd.lastCloseValue())"
|
|
1457
|
+
(escapeKeyDown)="entry.escapeKeyDown?.($event)"
|
|
1458
|
+
(pointerDownOutside)="entry.pointerDownOutside?.($event)"
|
|
1459
|
+
(focusOutside)="entry.focusOutside?.($event)"
|
|
1460
|
+
(interactOutside)="entry.interactOutside?.($event)"
|
|
1461
|
+
(dragMove)="entry.onDrag?.($event)"
|
|
1462
|
+
(release)="entry.onRelease?.($event)"
|
|
1463
|
+
(activeSnapPointChange)="entry.onActiveSnapPointChange?.($event)"
|
|
1464
|
+
#fd="forDrawer"
|
|
1465
|
+
>
|
|
1466
|
+
<ng-container forDrawerContextInjector #ctx="forDrawerContextInjector">
|
|
1467
|
+
<ng-container
|
|
1468
|
+
[ngComponentOutlet]="entry.component"
|
|
1469
|
+
[ngComponentOutletInjector]="entry.injectorFor(ctx.injector)"
|
|
1470
|
+
/>
|
|
1471
|
+
</ng-container>
|
|
1472
|
+
</div>
|
|
1473
|
+
}
|
|
1474
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: ForDrawer, selector: "[forDrawer]", inputs: ["side", "dismissible", "modal", "alert", "returnFocus", "initialFocus", "ariaLabel", "container", "autoFocusOnOpen", "autoFocusOnClose", "swipeToDismiss", "closeThreshold", "handleOnly", "scaleBackground", "setBackgroundColorOnScale", "snapPoints", "activeSnapPoint", "fadeFromIndex"], outputs: ["activeSnapPointChange", "dismiss", "escapeKeyDown", "pointerDownOutside", "focusOutside", "interactOutside", "dragMove", "release"], exportAs: ["forDrawer"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: ForDrawerContextInjector, selector: "[forDrawerContextInjector]", exportAs: ["forDrawerContextInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1475
|
+
}
|
|
1476
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerOutlet, decorators: [{
|
|
1477
|
+
type: Component,
|
|
1478
|
+
args: [{
|
|
1479
|
+
selector: 'for-drawer-outlet',
|
|
1480
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1481
|
+
imports: [ForDrawer, NgComponentOutlet, ForDrawerContextInjector],
|
|
1482
|
+
template: `
|
|
1483
|
+
@for (entry of entries(); track entry.id) {
|
|
1484
|
+
<div
|
|
1485
|
+
forDrawer
|
|
1486
|
+
[attr.data-for-drawer-id]="entry.id"
|
|
1487
|
+
[animate.enter]="entry.animateEnter ?? ''"
|
|
1488
|
+
[class]="entry.hostClass"
|
|
1489
|
+
[side]="entry.side ?? 'bottom'"
|
|
1490
|
+
[dismissible]="entry.dismissible ?? true"
|
|
1491
|
+
[modal]="entry.modal ?? true"
|
|
1492
|
+
[alert]="entry.alert ?? false"
|
|
1493
|
+
[returnFocus]="entry.returnFocus ?? true"
|
|
1494
|
+
[initialFocus]="entry.initialFocus ?? 'first'"
|
|
1495
|
+
[ariaLabel]="entry.ariaLabel ?? null"
|
|
1496
|
+
[container]="entry.container ?? null"
|
|
1497
|
+
[autoFocusOnOpen]="entry.autoFocusOnOpen"
|
|
1498
|
+
[autoFocusOnClose]="entry.autoFocusOnClose"
|
|
1499
|
+
[swipeToDismiss]="entry.swipeToDismiss ?? true"
|
|
1500
|
+
[closeThreshold]="entry.closeThreshold ?? 0.25"
|
|
1501
|
+
[handleOnly]="entry.handleOnly ?? false"
|
|
1502
|
+
[scaleBackground]="entry.scaleBackground ?? false"
|
|
1503
|
+
[setBackgroundColorOnScale]="entry.setBackgroundColorOnScale ?? true"
|
|
1504
|
+
[snapPoints]="entry.snapPoints"
|
|
1505
|
+
[activeSnapPoint]="entry.activeSnapPoint ?? null"
|
|
1506
|
+
[fadeFromIndex]="entry.fadeFromIndex"
|
|
1507
|
+
(dismiss)="entry.handleClose(fd.lastCloseValue())"
|
|
1508
|
+
(escapeKeyDown)="entry.escapeKeyDown?.($event)"
|
|
1509
|
+
(pointerDownOutside)="entry.pointerDownOutside?.($event)"
|
|
1510
|
+
(focusOutside)="entry.focusOutside?.($event)"
|
|
1511
|
+
(interactOutside)="entry.interactOutside?.($event)"
|
|
1512
|
+
(dragMove)="entry.onDrag?.($event)"
|
|
1513
|
+
(release)="entry.onRelease?.($event)"
|
|
1514
|
+
(activeSnapPointChange)="entry.onActiveSnapPointChange?.($event)"
|
|
1515
|
+
#fd="forDrawer"
|
|
1516
|
+
>
|
|
1517
|
+
<ng-container forDrawerContextInjector #ctx="forDrawerContextInjector">
|
|
1518
|
+
<ng-container
|
|
1519
|
+
[ngComponentOutlet]="entry.component"
|
|
1520
|
+
[ngComponentOutletInjector]="entry.injectorFor(ctx.injector)"
|
|
1521
|
+
/>
|
|
1522
|
+
</ng-container>
|
|
1523
|
+
</div>
|
|
1524
|
+
}
|
|
1525
|
+
`,
|
|
1526
|
+
}]
|
|
1527
|
+
}] });
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Injection token for the `data` payload passed to `ForDrawerManager.open(component, { data })`.
|
|
1531
|
+
* Inject inside the opened component:
|
|
1532
|
+
*
|
|
1533
|
+
* ```ts
|
|
1534
|
+
* readonly data = inject(FOR_DRAWER_DATA) as MyShape;
|
|
1535
|
+
* ```
|
|
1536
|
+
*
|
|
1537
|
+
* Prefer `injectDrawerData<T>()` for typed access without manual casts.
|
|
1538
|
+
*/
|
|
1539
|
+
const FOR_DRAWER_DATA = new InjectionToken('FOR_DRAWER_DATA');
|
|
1540
|
+
/** Typed accessor for the `data` payload. Equivalent to `inject(FOR_DRAWER_DATA) as T`. */
|
|
1541
|
+
function injectDrawerData() {
|
|
1542
|
+
return inject(FOR_DRAWER_DATA);
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Programmatic drawer opener — symmetric with `ForDialogManager`. Inject
|
|
1546
|
+
* anywhere, call `open(MyComponent, { data, side, ... })` and get a
|
|
1547
|
+
* `ForDrawerRef<R>` back. Internally the manager renders the user component
|
|
1548
|
+
* inside a manager-owned `@for` outlet, so Angular's control-flow unmount
|
|
1549
|
+
* fires `animate.leave` before the node leaves the DOM — identical to the
|
|
1550
|
+
* declarative `@if (open()) { <div forDrawer animate.leave="…"> }` path.
|
|
1551
|
+
*
|
|
1552
|
+
* Inside the opened component the usual drawer pieces work without any extra
|
|
1553
|
+
* wiring: `[forDrawerTitle]`, `[forDrawerDescription]`, `[forDrawerBackdrop]`,
|
|
1554
|
+
* `[forDrawerHandle]`, `[forDrawerClose]`. `[forDrawerClose] [closeWith]`
|
|
1555
|
+
* propagates through to `ForDrawerRef.close(value)`.
|
|
1556
|
+
*
|
|
1557
|
+
* Inject `ForDrawerRef` to drive close imperatively and `FOR_DRAWER_DATA` (or
|
|
1558
|
+
* `injectDrawerData<T>()`) for the payload.
|
|
1559
|
+
*/
|
|
1560
|
+
class ForDrawerManager extends OverlayManagerCore {
|
|
1561
|
+
#defaults = inject(FOR_DRAWER_DEFAULTS);
|
|
1562
|
+
constructor() {
|
|
1563
|
+
super({
|
|
1564
|
+
idPrefix: 'for-drawer-instance',
|
|
1565
|
+
idAttribute: 'data-for-drawer-id',
|
|
1566
|
+
backdropAttribute: 'data-for-drawer-backdrop',
|
|
1567
|
+
createOutlet: (environmentInjector) => createComponent(ForDrawerOutlet, { environmentInjector }),
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
open(component, config = {}) {
|
|
1571
|
+
if (config.closeThreshold !== undefined) {
|
|
1572
|
+
const ct = config.closeThreshold;
|
|
1573
|
+
if (!Number.isFinite(ct) || ct < 0 || ct > 1) {
|
|
1574
|
+
throw new Error(`[forty-cdk/drawer] closeThreshold must be in [0, 1], got ${ct}.`);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
const { id, remove } = this.nextId();
|
|
1578
|
+
const animateEnter = config.animateEnter ?? this.#defaults.animateEnter;
|
|
1579
|
+
const animateLeave = config.animateLeave ?? this.#defaults.animateLeave;
|
|
1580
|
+
const backdropAnimateLeave = config.backdropAnimateLeave ?? this.#defaults.backdropAnimateLeave;
|
|
1581
|
+
const ref = new ForDrawerRef(() => this.beginLeave(id, animateLeave, backdropAnimateLeave, remove));
|
|
1582
|
+
const hostClass = resolveConfigClass(config) ?? '';
|
|
1583
|
+
const data = config.data ?? null;
|
|
1584
|
+
const entry = {
|
|
1585
|
+
id,
|
|
1586
|
+
component: component,
|
|
1587
|
+
hostClass,
|
|
1588
|
+
side: config.side ?? this.#defaults.side,
|
|
1589
|
+
dismissible: config.dismissible ?? this.#defaults.dismissible,
|
|
1590
|
+
modal: config.modal ?? this.#defaults.modal,
|
|
1591
|
+
alert: config.alert,
|
|
1592
|
+
returnFocus: config.returnFocus ?? this.#defaults.returnFocus,
|
|
1593
|
+
initialFocus: config.initialFocus ?? this.#defaults.initialFocus,
|
|
1594
|
+
ariaLabel: config.ariaLabel,
|
|
1595
|
+
container: config.container,
|
|
1596
|
+
animateEnter,
|
|
1597
|
+
autoFocusOnOpen: config.autoFocusOnOpen,
|
|
1598
|
+
autoFocusOnClose: config.autoFocusOnClose,
|
|
1599
|
+
swipeToDismiss: config.swipeToDismiss ?? this.#defaults.swipeToDismiss,
|
|
1600
|
+
closeThreshold: config.closeThreshold ?? this.#defaults.closeThreshold,
|
|
1601
|
+
handleOnly: config.handleOnly ?? this.#defaults.handleOnly,
|
|
1602
|
+
scaleBackground: config.scaleBackground ?? this.#defaults.scaleBackground,
|
|
1603
|
+
setBackgroundColorOnScale: config.setBackgroundColorOnScale ?? this.#defaults.setBackgroundColorOnScale,
|
|
1604
|
+
snapPoints: config.snapPoints,
|
|
1605
|
+
activeSnapPoint: config.defaultSnapPoint,
|
|
1606
|
+
fadeFromIndex: config.fadeFromIndex,
|
|
1607
|
+
escapeKeyDown: config.escapeKeyDown,
|
|
1608
|
+
pointerDownOutside: config.pointerDownOutside,
|
|
1609
|
+
focusOutside: config.focusOutside,
|
|
1610
|
+
interactOutside: config.interactOutside,
|
|
1611
|
+
onDrag: config.onDrag,
|
|
1612
|
+
onRelease: config.onRelease,
|
|
1613
|
+
onActiveSnapPointChange: config.onActiveSnapPointChange,
|
|
1614
|
+
ref: ref,
|
|
1615
|
+
handleClose(value) {
|
|
1616
|
+
ref.close(value);
|
|
1617
|
+
},
|
|
1618
|
+
injectorFor: this.createInjectorFactory([
|
|
1619
|
+
{ provide: FOR_DRAWER_DATA, useValue: data },
|
|
1620
|
+
{ provide: FOR_DRAWER_INSTANCE_ID, useValue: id },
|
|
1621
|
+
{ provide: ForDrawerRef, useValue: ref },
|
|
1622
|
+
...(config.providers ?? []),
|
|
1623
|
+
]),
|
|
1624
|
+
};
|
|
1625
|
+
this.register(entry);
|
|
1626
|
+
return ref;
|
|
1627
|
+
}
|
|
1628
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1629
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerManager, providedIn: 'root' });
|
|
1630
|
+
}
|
|
1631
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerManager, decorators: [{
|
|
1632
|
+
type: Injectable,
|
|
1633
|
+
args: [{ providedIn: 'root' }]
|
|
1634
|
+
}], ctorParameters: () => [] });
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* Generated bundle index. Do not edit.
|
|
1638
|
+
*/
|
|
1639
|
+
|
|
1640
|
+
export { FOR_DRAWER_CONTEXT, FOR_DRAWER_DATA, FOR_DRAWER_DEFAULTS, ForDrawer, ForDrawerBackdrop, ForDrawerClose, ForDrawerDescription, ForDrawerHandle, ForDrawerManager, ForDrawerRef, ForDrawerTitle, ForDrawerTrigger, ForDrawerWrapper, injectDrawerData, provideForDrawerDefaults };
|
|
1641
|
+
//# sourceMappingURL=forty-cdk-drawer.mjs.map
|