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
package/drawer/README.md
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
# Drawer
|
|
2
|
+
|
|
3
|
+
Headless side / bottom-sheet drawer with optional swipe-to-dismiss and snap points. Built on top of the [WAI-ARIA Modal Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) — same focus trap, scroll lock, Escape-to-close, dismissable-layer, and portal behaviors as `ForDialog`, plus pointer-driven drag.
|
|
4
|
+
|
|
5
|
+
## Anatomy
|
|
6
|
+
|
|
7
|
+
| Piece | Selector | Purpose |
|
|
8
|
+
| ---------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
9
|
+
| `ForDrawer` | `[forDrawer]` | Root surface. `role="dialog"` (or `"alertdialog"`), `aria-modal`, side effects, swipe & snap engine. |
|
|
10
|
+
| `ForDrawerTrigger` | `[forDrawerTrigger]` | Conveniently wires a `<button>` to the same `[(open)]` signal that gates the surrounding `@if`. |
|
|
11
|
+
| `ForDrawerBackdrop` | `[forDrawerBackdrop]` | Optional overlay portaled to body. Reflects `data-fade-from-active` (snap-driven) + `data-dragging`, and publishes `--for-drawer-drag-progress` for the swipe-to-dismiss fade. |
|
|
12
|
+
| `ForDrawerHandle` | `[forDrawerHandle]` | Visual swipe handle. With `[handleOnly]="true"` the swipe gesture only arms on this element. |
|
|
13
|
+
| `ForDrawerTitle` | `[forDrawerTitle]` | Registers an id for `aria-labelledby`. |
|
|
14
|
+
| `ForDrawerDescription` | `[forDrawerDescription]` | Registers an id for `aria-describedby`. |
|
|
15
|
+
| `ForDrawerClose` | `[forDrawerClose]` | Closes the drawer with reason `'closeButton'`. |
|
|
16
|
+
| `ForDrawerWrapper` | `[forDrawerWrapper]` | Marks the app shell so `[scaleBackground]` drawers can scale + translate it behind them. |
|
|
17
|
+
|
|
18
|
+
## Two flows, one engine
|
|
19
|
+
|
|
20
|
+
Same engine as Dialog: the directive composes focus trap + scroll lock + dismissable layer + portal + (additionally) swipe-dismiss. Pick declarative or programmatic.
|
|
21
|
+
|
|
22
|
+
### Declarative — `[forDrawer]`
|
|
23
|
+
|
|
24
|
+
Mount equals open. The consumer's signal drives `@if`; the directive emits `(dismiss)` when it wants to be unmounted.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { Component, signal } from '@angular/core';
|
|
28
|
+
import {
|
|
29
|
+
ForDrawer,
|
|
30
|
+
ForDrawerBackdrop,
|
|
31
|
+
ForDrawerClose,
|
|
32
|
+
ForDrawerDescription,
|
|
33
|
+
ForDrawerHandle,
|
|
34
|
+
type ForDrawerSnapPoint,
|
|
35
|
+
ForDrawerTitle,
|
|
36
|
+
ForDrawerTrigger,
|
|
37
|
+
} from 'forty-cdk/drawer';
|
|
38
|
+
|
|
39
|
+
@Component({
|
|
40
|
+
selector: 'demo-filters',
|
|
41
|
+
imports: [
|
|
42
|
+
ForDrawer,
|
|
43
|
+
ForDrawerTrigger,
|
|
44
|
+
ForDrawerBackdrop,
|
|
45
|
+
ForDrawerHandle,
|
|
46
|
+
ForDrawerTitle,
|
|
47
|
+
ForDrawerDescription,
|
|
48
|
+
ForDrawerClose,
|
|
49
|
+
],
|
|
50
|
+
template: `
|
|
51
|
+
<button forDrawerTrigger [(open)]="open" controls="filters-drawer">Filters</button>
|
|
52
|
+
|
|
53
|
+
@if (open()) {
|
|
54
|
+
<div
|
|
55
|
+
forDrawer
|
|
56
|
+
id="filters-drawer"
|
|
57
|
+
side="bottom"
|
|
58
|
+
[snapPoints]="snaps"
|
|
59
|
+
[(activeSnapPoint)]="snap"
|
|
60
|
+
(dismiss)="open.set(false)"
|
|
61
|
+
animate.enter="slide-up"
|
|
62
|
+
animate.leave="slide-down"
|
|
63
|
+
>
|
|
64
|
+
<div
|
|
65
|
+
forDrawerBackdrop
|
|
66
|
+
class="drawer-backdrop"
|
|
67
|
+
animate.enter="fade-in"
|
|
68
|
+
animate.leave="fade-out"
|
|
69
|
+
></div>
|
|
70
|
+
<div forDrawerHandle aria-hidden="true"></div>
|
|
71
|
+
<h2 forDrawerTitle>Filters</h2>
|
|
72
|
+
<p forDrawerDescription>Apply filters to the listing.</p>
|
|
73
|
+
<button forDrawerClose>Close</button>
|
|
74
|
+
</div>
|
|
75
|
+
}
|
|
76
|
+
`,
|
|
77
|
+
})
|
|
78
|
+
export class DemoFilters {
|
|
79
|
+
readonly open = signal(false);
|
|
80
|
+
readonly snaps: ReadonlyArray<ForDrawerSnapPoint> = ['148px', '50%', 1];
|
|
81
|
+
readonly snap = signal<ForDrawerSnapPoint | null>(null);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Wrapping with `@if` is what makes Angular's native `animate.enter` / `animate.leave` work — they fire on real mount / unmount, not on attribute toggling.
|
|
86
|
+
|
|
87
|
+
### Programmatic — `ForDrawerManager.open()`
|
|
88
|
+
|
|
89
|
+
The manager mounts the user component underneath the same `[forDrawer]` directive that powers the declarative shape, so every child piece (`[forDrawerTitle]`, `[forDrawerDescription]`, `[forDrawerBackdrop]`, `[forDrawerHandle]`, `[forDrawerClose]`) and every `ForDrawer` input (`side`, `snapPoints`, `swipeToDismiss`, `closeThreshold`, `handleOnly`, `scaleBackground`, `setBackgroundColorOnScale`, `fadeFromIndex`, …) work identically. `[forDrawerClose] [closeWith]` propagates straight through to `ForDrawerRef.close(value)`.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { Component, inject } from '@angular/core';
|
|
93
|
+
import {
|
|
94
|
+
ForDrawerBackdrop,
|
|
95
|
+
ForDrawerClose,
|
|
96
|
+
ForDrawerDescription,
|
|
97
|
+
ForDrawerHandle,
|
|
98
|
+
ForDrawerManager,
|
|
99
|
+
ForDrawerRef,
|
|
100
|
+
ForDrawerTitle,
|
|
101
|
+
injectDrawerData,
|
|
102
|
+
} from 'forty-cdk/drawer';
|
|
103
|
+
|
|
104
|
+
@Component({
|
|
105
|
+
imports: [
|
|
106
|
+
ForDrawerBackdrop,
|
|
107
|
+
ForDrawerHandle,
|
|
108
|
+
ForDrawerTitle,
|
|
109
|
+
ForDrawerDescription,
|
|
110
|
+
ForDrawerClose,
|
|
111
|
+
],
|
|
112
|
+
template: `
|
|
113
|
+
<div
|
|
114
|
+
forDrawerBackdrop
|
|
115
|
+
class="drawer-backdrop"
|
|
116
|
+
animate.enter="fade-in"
|
|
117
|
+
animate.leave="fade-out"
|
|
118
|
+
></div>
|
|
119
|
+
<div forDrawerHandle aria-hidden="true"></div>
|
|
120
|
+
<h2 forDrawerTitle>Delete account?</h2>
|
|
121
|
+
<p forDrawerDescription>{{ data.message }}</p>
|
|
122
|
+
<button forDrawerClose [closeWith]="'cancel'">Cancel</button>
|
|
123
|
+
<button forDrawerClose [closeWith]="'confirm'">Confirm</button>
|
|
124
|
+
`,
|
|
125
|
+
})
|
|
126
|
+
class ConfirmDrawer {
|
|
127
|
+
readonly data = injectDrawerData<{ message: string }>();
|
|
128
|
+
readonly ref = inject(ForDrawerRef) as ForDrawerRef<'confirm' | 'cancel'>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@Component({
|
|
132
|
+
selector: 'demo-host',
|
|
133
|
+
template: `<button (click)="askToDelete()">Delete</button>`,
|
|
134
|
+
})
|
|
135
|
+
class DemoHost {
|
|
136
|
+
readonly #drawers = inject(ForDrawerManager);
|
|
137
|
+
|
|
138
|
+
async askToDelete(): Promise<void> {
|
|
139
|
+
const ref = this.#drawers.open<ConfirmDrawer, 'confirm' | 'cancel'>(ConfirmDrawer, {
|
|
140
|
+
data: { message: 'This action cannot be undone.' },
|
|
141
|
+
side: 'bottom',
|
|
142
|
+
snapPoints: ['148px', 1],
|
|
143
|
+
});
|
|
144
|
+
const result = await ref.closed;
|
|
145
|
+
if (result === 'confirm') {
|
|
146
|
+
// ...
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Drawers opened by the manager join the same `ForDrawerStack` as declarative ones, so mixed stacking (a programmatic drawer over a declarative parent, or vice versa) reflects correct `data-depth` / `data-state-nested` and routes Escape through the LIFO dismissable layer.
|
|
153
|
+
|
|
154
|
+
**Styling the programmatic overlay root.** The manager creates the `[forDrawer]` host for you and it is class-less. Pass `class` / `classList` to style it — the tokens land on the real host alongside `data-side` / `data-state` / the `--for-drawer-translate` custom property, so positioning CSS keyed on `data-side` works:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
this.#drawers.open(ConfirmDrawer, { data, side: 'bottom', class: 'my-drawer' });
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```css
|
|
161
|
+
.my-drawer[data-side='bottom'] {
|
|
162
|
+
inset: auto 0 0 0;
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Enter / exit animations.** A programmatic drawer is portaled to `document.body` and torn down imperatively, so the consumer can't attach `animate.leave` to the host the way a declarative `@if` block can. Pass `animateEnter` / `animateLeave` (CSS class names) instead: the manager applies `animateEnter` on mount (via `animate.enter`) and, on `close()`, keeps the host mounted with `animateLeave` until its CSS animations / transitions finish before tearing down. `close()` still resolves its promise and flips `isClosed()` immediately — only the visual teardown waits. Set them once for a scope with `provideForDrawerDefaults({ animateEnter, animateLeave })`; a per-`open()` value wins over the scope default.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
this.#drawers.open(ConfirmDrawer, {
|
|
170
|
+
data,
|
|
171
|
+
side: 'bottom',
|
|
172
|
+
class: 'my-drawer',
|
|
173
|
+
animateEnter: 'drawer-in',
|
|
174
|
+
animateLeave: 'drawer-out',
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`class` is a single or space-separated string; `classList` is an array or space-separated string; both merge and de-dup and never clobber the host attributes. This replaces the old `inject(FOR_DRAWER_CONTEXT).hostElement.classList.add('my-drawer')` workaround.
|
|
179
|
+
|
|
180
|
+
**Observing drag / release / active snap point.** A snap-point drawer opened imperatively has the same observability as the declarative `(dragMove)` / `(release)` / `(activeSnapPointChange)` outputs via the `onDrag` / `onRelease` / `onActiveSnapPointChange` config callbacks:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
this.#drawers.open(ConfirmDrawer, {
|
|
184
|
+
data,
|
|
185
|
+
snapPoints: ['148px', '50%', 1],
|
|
186
|
+
defaultSnapPoint: '148px',
|
|
187
|
+
onDrag: ({ percentageDragged }) => this.dragProgress.set(percentageDragged),
|
|
188
|
+
onRelease: ({ willClose, nextSnapPoint }) => {
|
|
189
|
+
/* … */
|
|
190
|
+
},
|
|
191
|
+
onActiveSnapPointChange: (snap) => this.activeSnap.set(snap),
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`onActiveSnapPointChange` fires with the landed snap on the mount-time default and every drag release — the read-back the declarative API exposes through `[(activeSnapPoint)]`. All three subscriptions are released automatically when the drawer closes.
|
|
196
|
+
|
|
197
|
+
### Per-channel dismissal (Escape-only drawers)
|
|
198
|
+
|
|
199
|
+
`dismissible` is **not** all-or-nothing. The four dismiss channels — Escape, pointer-down-outside, focus-outside, and the composite outside-interaction — are independently vetoable on both APIs, so you can keep some live and suppress others (e.g. a non-modal floater that closes on Escape but stays put on an outside click). Programmatically the channels are callbacks on the open config, mirroring the `autoFocusOn*` shape:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
this.#drawers.open(ConfirmDrawer, {
|
|
203
|
+
data,
|
|
204
|
+
modal: false,
|
|
205
|
+
// dismissible: true (the default) keeps Escape live.
|
|
206
|
+
interactOutside: (event) => event.preventDefault(), // ignore outside interaction
|
|
207
|
+
// escapeKeyDown / pointerDownOutside / focusOutside are available too.
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Declaratively the same recipe is the four vetoable outputs on `[forDrawer]`: `(interactOutside)="$event.preventDefault()"` suppresses the outside-click close while Escape (its own channel) still closes; veto `(escapeKeyDown)` instead to suppress Escape. Each callback's / output's `event.event` carries the originating DOM event. The callbacks behave identically to the outputs — same events, same veto semantics — and are torn down with the drawer.
|
|
212
|
+
|
|
213
|
+
## ForDrawer inputs / models
|
|
214
|
+
|
|
215
|
+
| Name | Type | Default | Notes |
|
|
216
|
+
| --------------------------- | ------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
217
|
+
| `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'bottom'` | Anchored edge. Drives swipe direction and `data-side`. |
|
|
218
|
+
| `modal` | `boolean` | `true` | `aria-modal`, scroll lock, focus trap, inert siblings. |
|
|
219
|
+
| `dismissible` | `boolean` | `true` | Whether Escape / backdrop / outside / swipe close. |
|
|
220
|
+
| `alert` | `boolean` | `false` | `role="alertdialog"`. |
|
|
221
|
+
| `returnFocus` | `boolean` | `true` | Restore focus on close. |
|
|
222
|
+
| `initialFocus` | `'first' \| 'container'` | `'first'` | |
|
|
223
|
+
| `ariaLabel` | `string \| null` | `null` | Use when no visible title is rendered. |
|
|
224
|
+
| `animateEnter` | `string` | — | CSS class applied on mount (via `animate.enter`) to play an enter animation. |
|
|
225
|
+
| `animateLeave` | `string` | — | CSS class applied on close; the host stays mounted until its animation finishes, then tears down. |
|
|
226
|
+
| `autoFocusOnOpen` | `(e: VetoableEvent) => void` \| `undefined` | — | `event.preventDefault()` skips the imperative focus move. |
|
|
227
|
+
| `autoFocusOnClose` | `(e: VetoableEvent) => void` \| `undefined` | — | Fires on every close path regardless of mode. In non-modal mode the directive doesn't move focus, so the veto is informational; in modal mode `event.preventDefault()` skips return-focus. |
|
|
228
|
+
| `swipeToDismiss` | `boolean` | `true` | Disabled automatically under `prefers-reduced-motion: reduce`. |
|
|
229
|
+
| `closeThreshold` | `number` | `0.25` | Fraction past which a release dismisses — of the full dimension without `snapPoints`, of the lowest snap's extent with them. |
|
|
230
|
+
| `handleOnly` | `boolean` | `false` | Swipe arms only on the registered `[forDrawerHandle]`. |
|
|
231
|
+
| `snapPoints` | `ReadonlyArray<ForDrawerSnapPoint>` | — | `number ∈ [0,1]` \| `'NN%'` \| `'NNpx'`. Strictly increasing. |
|
|
232
|
+
| `activeSnapPoint` | `ModelSignal<ForDrawerSnapPoint \| null>` | `null` | Two-way bindable. Initialised to `snapPoints[0]` on mount when null. |
|
|
233
|
+
| `fadeFromIndex` | `number` | — | Backdrop reflects `data-fade-from-active` once active >= this index. |
|
|
234
|
+
| `scaleBackground` | `boolean` | `false` | Asks `[forDrawerWrapper]` to scale + translate behind the drawer. |
|
|
235
|
+
| `setBackgroundColorOnScale` | `boolean` | `true` | Paints `<body>` to mask the gap between scaled wrapper and viewport edge. |
|
|
236
|
+
|
|
237
|
+
## ForDrawer outputs
|
|
238
|
+
|
|
239
|
+
| Name | Payload | Notes |
|
|
240
|
+
| -------------------- | ------------------------------------------------- | --------------------------------------------------------------- |
|
|
241
|
+
| `dismiss` | `ForDrawerCloseReason` | Wire to `(dismiss)="open.set(false)"`. |
|
|
242
|
+
| `escapeKeyDown` | `VetoableNativeEvent<KeyboardEvent>` | `preventDefault()` suppresses auto-close. |
|
|
243
|
+
| `pointerDownOutside` | `VetoableNativeEvent<PointerEvent>` | " |
|
|
244
|
+
| `focusOutside` | `VetoableNativeEvent<FocusEvent>` | " |
|
|
245
|
+
| `interactOutside` | `VetoableNativeEvent<PointerEvent \| FocusEvent>` | Composite — vetoed by either specific event. |
|
|
246
|
+
| `dragMove` | `ForDrawerDragEvent` | Streams `percentageDragged` and the originating `PointerEvent`. |
|
|
247
|
+
| `release` | `ForDrawerReleaseEvent` | `willClose`, `nextSnapPoint`. Directive already updated state. |
|
|
248
|
+
|
|
249
|
+
`ForDrawerCloseReason`: `'escape' | 'backdrop' | 'pointerDownOutside' | 'focusOutside' | 'closeButton' | 'swipe' | 'programmatic'`.
|
|
250
|
+
|
|
251
|
+
> **Declarative vs. imperative naming asymmetry.** The declarative output is `(dismiss)`, but the imperative handle method stays `ForDrawerRef.close()`, the `[forDrawerClose]` directive selector is unchanged, and the `ForDrawerCloseReason` type keeps its name. This is intentional: the output rename removes the native-event collision (see [#814](https://github.com/tutkli/forty-cdk/issues/814)) while the imperative surface follows the convention established before that rename.
|
|
252
|
+
|
|
253
|
+
## Snap points
|
|
254
|
+
|
|
255
|
+
Three accepted shapes:
|
|
256
|
+
|
|
257
|
+
- `number ∈ [0, 1]` — fraction of the dismissal-axis dimension.
|
|
258
|
+
- `'NN%'` — equivalent to a fraction (`'50%' === 0.5`).
|
|
259
|
+
- `'NNpx'` — absolute pixel size measured from the anchored edge.
|
|
260
|
+
|
|
261
|
+
Pass them in **strictly increasing** order (closest-to-edge first). The directive throws `[forty-cdk/drawer] snapPoints must be strictly increasing (closest-to-edge first).` otherwise. `fadeFromIndex` must be a valid index into `snapPoints`.
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
[snapPoints] =
|
|
265
|
+
"['148px', '50%', 1]"[activeSnapPoint] = // peek → mid → full
|
|
266
|
+
'snap'[fadeFromIndex] = // current snap; written on drag release
|
|
267
|
+
'1'; // backdrop fades once we cross the second snap
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The `model<>()` change emitter (`(activeSnapPointChange)`) fires on internal transitions (the mount-time default and every drag release), and stays silent on consumer writes through `[(activeSnapPoint)]`.
|
|
271
|
+
|
|
272
|
+
### Positioning the snaps (CSS contract)
|
|
273
|
+
|
|
274
|
+
The directive does **not** position the surface at each snap — that is the consumer's job, keyed off `data-active-snap-point`. Position the rest state with a layout property such as `bottom` / `top` (or `left` / `right`), and transition it for the snap-to-snap animation.
|
|
275
|
+
|
|
276
|
+
The live drag delta is published on the host as the **`--for-drawer-translate`** custom property (a `"<x> <y>"` value, `"0px 0px"` at rest); apply it on the surface with `translate: var(--for-drawer-translate, 0px 0px)`. A custom property is used — rather than the directive writing `translate` / `transform` directly — for two reasons: `transform` is reserved for the scale-background / nested effect, and a directly-written inline `translate` is silently dropped by Angular when you also bind a template `[style.*]` on the same host. Reading it through the var keeps the drag working regardless of any inline style bindings you put on the surface, and composes with `transform` without clobbering it.
|
|
277
|
+
|
|
278
|
+
For a seamless release, transition **both** `translate` and your snap-position property with the same timing, and suppress that transition while `data-dragging` is present. The directive resets `--for-drawer-translate` to `"0px 0px"`, removes `data-dragging`, and updates `data-active-snap-point` in a single change-detection pass on release, so the drag delta animates back to zero in lockstep with the snap-position change — the surface never jumps to the previous rest position before sliding to the new snap.
|
|
279
|
+
|
|
280
|
+
```css
|
|
281
|
+
.sheet {
|
|
282
|
+
/* The directive publishes the live drag delta here; compose it on the surface. */
|
|
283
|
+
translate: var(--for-drawer-translate, 0px 0px);
|
|
284
|
+
}
|
|
285
|
+
.sheet[data-active-snap-point] {
|
|
286
|
+
height: 80vh;
|
|
287
|
+
transition:
|
|
288
|
+
bottom 0.42s cubic-bezier(0.32, 0.72, 0, 1),
|
|
289
|
+
translate 0.42s cubic-bezier(0.32, 0.72, 0, 1);
|
|
290
|
+
}
|
|
291
|
+
.sheet[data-active-snap-point][data-dragging] {
|
|
292
|
+
transition: none; /* the drag follows the pointer 1:1 */
|
|
293
|
+
}
|
|
294
|
+
.sheet[data-active-snap-point='148px'] {
|
|
295
|
+
bottom: calc(148px - 80vh);
|
|
296
|
+
}
|
|
297
|
+
.sheet[data-active-snap-point='0.5'] {
|
|
298
|
+
bottom: -40vh;
|
|
299
|
+
}
|
|
300
|
+
.sheet[data-active-snap-point='1'] {
|
|
301
|
+
bottom: 0;
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Backdrop drag-fade (CSS contract)
|
|
306
|
+
|
|
307
|
+
`[forDrawerBackdrop]` publishes the live drag progress _toward the anchored edge_ as the **`--for-drawer-drag-progress`** custom property (`0` at rest → `1` fully dragged off-screen) and mirrors the surface's **`data-dragging`** attribute. This drives the "backdrop fades out as you swipe to dismiss" cue with pure CSS — no `(dragMove)` listener required:
|
|
308
|
+
|
|
309
|
+
```css
|
|
310
|
+
.drawer-backdrop {
|
|
311
|
+
/* Fades the backdrop as the surface is dragged off-screen. */
|
|
312
|
+
opacity: calc(1 - var(--for-drawer-drag-progress, 0));
|
|
313
|
+
transition: opacity 0.3s ease;
|
|
314
|
+
}
|
|
315
|
+
.drawer-backdrop[data-dragging] {
|
|
316
|
+
transition: none; /* track the pointer 1:1 during the gesture */
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
`--for-drawer-drag-progress` only reflects the _dismiss_ direction: with snap points, a drag **away** from the edge (growing the surface) keeps it at `0`. On release it resets to `0` in the same change-detection pass that flips `data-dragging` off, so the backdrop animates back to full opacity in lockstep with the surface settling. The snap-driven `data-fade-from-active` cue (see above) is independent and can be combined or used on its own.
|
|
321
|
+
|
|
322
|
+
## Swipe-to-dismiss
|
|
323
|
+
|
|
324
|
+
- Pointer drag toward the anchored edge translates the surface and resolves to the nearest snap (or a dismiss) on release.
|
|
325
|
+
- With `snapPoints`, the drag is bidirectional: a drag **away** from the anchored edge grows the surface toward a larger snap (bounded by the largest snap), and a drag toward the edge shrinks it / dismisses past the lowest one. Without `snapPoints` the gesture is one-way (toward the edge to dismiss).
|
|
326
|
+
- `closeThreshold` (default `0.25`) is the fraction past which a release from the lowest snap dismisses — measured against that snap's own extent (not the full dimension), so a small "peek" snap stays dismissable without dragging it off-screen.
|
|
327
|
+
- `handleOnly: true` confines the gesture to a registered `[forDrawerHandle]`, leaving the rest of the surface free for content scroll.
|
|
328
|
+
- Gestures starting inside a scrollable element that hasn't reached its edge are NOT treated as swipes (the helper defers to inner scroll).
|
|
329
|
+
- **`prefers-reduced-motion: reduce`** disables the swipe listener entirely. Escape, backdrop, outside-pointer, and close button continue to work.
|
|
330
|
+
|
|
331
|
+
## Scale background
|
|
332
|
+
|
|
333
|
+
Opt in to the "viewport recedes behind the drawer" effect: when the drawer opens, the rest of the app shrinks slightly and rounds its corners to read as a layered surface. Two pieces required:
|
|
334
|
+
|
|
335
|
+
1. Apply `[forDrawerWrapper]` on the element that wraps the rest of the app (typically the root shell). Only one wrapper may be registered at a time.
|
|
336
|
+
2. Set `[scaleBackground]="true"` on the drawer that should drive the effect.
|
|
337
|
+
|
|
338
|
+
```html
|
|
339
|
+
<!-- Root shell -->
|
|
340
|
+
<div forDrawerWrapper>
|
|
341
|
+
<header>…</header>
|
|
342
|
+
<main>…</main>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<!-- Anywhere in the tree -->
|
|
346
|
+
@if (open()) {
|
|
347
|
+
<div
|
|
348
|
+
forDrawer
|
|
349
|
+
side="bottom"
|
|
350
|
+
[scaleBackground]="true"
|
|
351
|
+
(dismiss)="open.set(false)"
|
|
352
|
+
animate.enter="slide-up"
|
|
353
|
+
animate.leave="slide-down"
|
|
354
|
+
>
|
|
355
|
+
…
|
|
356
|
+
</div>
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
While the effect is active the wrapper reflects `data-state="scaled"` (and `"idle"` at rest); the drawer reflects `data-scale-background` so consumers can style the surface differently when scale is in play (e.g. larger corner radii).
|
|
361
|
+
|
|
362
|
+
`setBackgroundColorOnScale` (default `true`) paints `<body>` with `scaleBackgroundColor` while the effect is active. Disable it (`[setBackgroundColorOnScale]="false"`) when the application shell already covers the viewport edge — a themed `<html>` / `<body>` background, a fixed root layer, or a full-bleed CSS-framework wrapper. In those flows the body-color mutation is redundant and would briefly overwrite a theme-managed value on every open / close; leaving it off keeps the consumer's own paint authoritative, the rounded gap behind the scaled wrapper composes with whatever colour they ship. The flag has no effect under `prefers-reduced-motion: reduce` (the whole effect is suppressed).
|
|
363
|
+
|
|
364
|
+
`prefers-reduced-motion: reduce` suppresses the effect entirely — wrapper styles, body color, and `data-scale-background` are all bypassed without affecting the rest of the drawer's behaviour.
|
|
365
|
+
|
|
366
|
+
Tune the magic numbers via `provideForDrawerDefaults` (`scaleAmount`, `scaleTranslateYpx`, `scaleBorderRadiusPx`, `scaleBackgroundColor`).
|
|
367
|
+
|
|
368
|
+
## Nested drawers
|
|
369
|
+
|
|
370
|
+
A drawer mounted inside another drawer's `@if` is automatically detected as a child and joins a LIFO stack — no `nested` flag required. The directive composes the existing dismissable-layer / focus / scroll-lock stacks (Escape closes the topmost first; focus stays trapped in the topmost; body scroll lock is refcounted so closing the child does not unlock the parent), and adds two visual hooks on the parent surface:
|
|
371
|
+
|
|
372
|
+
- **`data-state-nested="true"`** while at least one descendant is registered — useful for styling the parent differently when it is "covered" by a child.
|
|
373
|
+
- An inline `transform: scale(N) translate3d(...)` that scales the parent surface and translates it slightly away from its anchored edge, so the child reads as a layer in front. Suppressed under `prefers-reduced-motion: reduce`. Tune via `nestedScaleAmount` (default `0.93`) and `nestedTranslateYpx` (default `8`).
|
|
374
|
+
|
|
375
|
+
Each drawer also reflects its position in the stack as `data-depth` (`"0"` for the root, `"1"` for the first child, …).
|
|
376
|
+
|
|
377
|
+
```html
|
|
378
|
+
@if (parentOpen()) {
|
|
379
|
+
<div forDrawer side="bottom" (dismiss)="parentOpen.set(false)" animate.leave="slide-down">
|
|
380
|
+
<h2 forDrawerTitle>Filters</h2>
|
|
381
|
+
|
|
382
|
+
<button (click)="childOpen.set(true)">Date range</button>
|
|
383
|
+
|
|
384
|
+
@if (childOpen()) {
|
|
385
|
+
<div forDrawer side="bottom" (dismiss)="childOpen.set(false)" animate.leave="slide-down">
|
|
386
|
+
<h2 forDrawerTitle>Date range</h2>
|
|
387
|
+
…
|
|
388
|
+
</div>
|
|
389
|
+
}
|
|
390
|
+
</div>
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Always nest the child's `@if` inside the parent's `@if`. That guarantees Angular's bottom-up destroy order tears the child down before the parent — the topology stack throws otherwise so the bug is loud at dev time. If both drawers opt into `[scaleBackground]="true"`, the wrapper effect composes with the parent's nested transform automatically.
|
|
395
|
+
|
|
396
|
+
## Styling
|
|
397
|
+
|
|
398
|
+
forty-cdk ships no styles. Add your own class to each piece — the `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../../../docs/styling.md)). Key your CSS off the reflected `data-*` attributes below.
|
|
399
|
+
|
|
400
|
+
### Data attributes
|
|
401
|
+
|
|
402
|
+
| Piece | Attribute | Values |
|
|
403
|
+
| --------------------- | ------------------------ | -------------------------------------------- |
|
|
404
|
+
| `[forDrawer]` | `data-state` | `open` |
|
|
405
|
+
| `[forDrawer]` | `data-side` | `top` \| `right` \| `bottom` \| `left` |
|
|
406
|
+
| `[forDrawer]` | `data-active-snap-point` | the active snap point stringified, or absent |
|
|
407
|
+
| `[forDrawer]` | `data-dragging` | present / absent |
|
|
408
|
+
| `[forDrawer]` | `data-scale-background` | present / absent |
|
|
409
|
+
| `[forDrawer]` | `data-depth` | `0` (root) \| `1` (first child) \| … |
|
|
410
|
+
| `[forDrawer]` | `data-state-nested` | `true` / absent |
|
|
411
|
+
| `[forDrawerBackdrop]` | `data-state` | `open` |
|
|
412
|
+
| `[forDrawerBackdrop]` | `data-fade-from-active` | present / absent |
|
|
413
|
+
| `[forDrawerBackdrop]` | `data-dragging` | present / absent |
|
|
414
|
+
| `[forDrawerClose]` | `data-state` | `open` |
|
|
415
|
+
| `[forDrawerTrigger]` | `data-state` | `open` \| `closed` |
|
|
416
|
+
| `[forDrawerTrigger]` | `data-disabled` | present / absent |
|
|
417
|
+
| `[forDrawerWrapper]` | `data-state` | `scaled` \| `idle` |
|
|
418
|
+
|
|
419
|
+
### CSS custom properties
|
|
420
|
+
|
|
421
|
+
| Property | Meaning |
|
|
422
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
423
|
+
| `--for-drawer-translate` | Written on `[forDrawer]` (the surface). Live drag delta as a `"<x> <y>"` length pair (`"0px 0px"` at rest). Apply with `translate: var(--for-drawer-translate, 0px 0px)` so it composes with the consumer's `transform`. See [Positioning the snaps](#positioning-the-snaps-css-contract). |
|
|
424
|
+
| `--for-drawer-drag-progress` | Written on `[forDrawerBackdrop]`. Drag progress toward the anchored edge, `0` (at rest) → `1` (fully dragged off-screen). Fade with `opacity: calc(1 - var(--for-drawer-drag-progress, 0))`. See [Backdrop drag-fade](#backdrop-drag-fade-css-contract). |
|
|
425
|
+
|
|
426
|
+
> This is a modal overlay: the surface and backdrop portal to `document.body`. Style them with global CSS or classes — declaratively, add your class to the surface element (`<div forDrawer class="my-drawer">`); for drawers opened with `ForDrawerManager.open()`, pass `class` / `classList` on the open config so the tokens land on the real `[forDrawer]` host.
|
|
427
|
+
|
|
428
|
+
```css
|
|
429
|
+
.sheet[data-active-snap-point] {
|
|
430
|
+
transition: translate 0.42s cubic-bezier(0.32, 0.72, 0, 1);
|
|
431
|
+
}
|
|
432
|
+
.sheet[data-dragging] {
|
|
433
|
+
transition: none;
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## Accessibility
|
|
438
|
+
|
|
439
|
+
Implements the WAI-ARIA Modal Dialog pattern. `role="dialog"` (or `"alertdialog"` when `alert`), `aria-modal="true"` in modal mode, `aria-labelledby` / `aria-describedby` auto-wired by `[forDrawerTitle]` / `[forDrawerDescription]`. Modal mode applies `inert` and `aria-hidden="true"` to body siblings so AT cannot reach them. The handle is `aria-hidden="true"` because keyboard users dismiss via Escape or `[forDrawerClose]`.
|
|
440
|
+
|
|
441
|
+
## Defaults provider
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
import { provideForDrawerDefaults } from 'forty-cdk/drawer';
|
|
445
|
+
|
|
446
|
+
bootstrapApplication(App, {
|
|
447
|
+
providers: [
|
|
448
|
+
provideForDrawerDefaults({
|
|
449
|
+
side: 'right',
|
|
450
|
+
closeThreshold: 0.4,
|
|
451
|
+
handleOnly: true,
|
|
452
|
+
// Scale-background (opt-in per drawer; the keys below tune the visual)
|
|
453
|
+
scaleAmount: 0.93,
|
|
454
|
+
scaleTranslateYpx: 16,
|
|
455
|
+
scaleBorderRadiusPx: 12,
|
|
456
|
+
scaleBackgroundColor: '#000',
|
|
457
|
+
}),
|
|
458
|
+
],
|
|
459
|
+
});
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Per-component overrides nest:
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
@Component({
|
|
466
|
+
providers: [provideForDrawerDefaults({ side: 'left' })],
|
|
467
|
+
// ...
|
|
468
|
+
})
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
## Scoped / contained drawer (`container`)
|
|
472
|
+
|
|
473
|
+
Pass `[container]` to portal the surface **and** the backdrop into a specific element instead of `document.body`. The supported shape is `[container]` paired with `[modal]="false"`.
|
|
474
|
+
|
|
475
|
+
```html
|
|
476
|
+
<section
|
|
477
|
+
#listBox
|
|
478
|
+
data-testid="container"
|
|
479
|
+
style="position: relative; height: 400px; overflow: hidden;"
|
|
480
|
+
>
|
|
481
|
+
<button forDrawerTrigger [(open)]="open">Open</button>
|
|
482
|
+
|
|
483
|
+
@if (open()) {
|
|
484
|
+
<div forDrawer side="right" [modal]="false" [container]="listBox" (dismiss)="open.set(false)">
|
|
485
|
+
<div forDrawerBackdrop></div>
|
|
486
|
+
<h2 forDrawerTitle>Filters</h2>
|
|
487
|
+
<button forDrawerClose>Close</button>
|
|
488
|
+
</div>
|
|
489
|
+
}
|
|
490
|
+
</section>
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**CSS contract.** The container must be positioned (`position: relative`); the surface and backdrop must use `position: absolute` (not `fixed`) so they are bounded to the container's box:
|
|
494
|
+
|
|
495
|
+
```css
|
|
496
|
+
section[data-testid='container'] {
|
|
497
|
+
position: relative;
|
|
498
|
+
}
|
|
499
|
+
[forDrawer] {
|
|
500
|
+
position: absolute;
|
|
501
|
+
top: 0;
|
|
502
|
+
right: 0;
|
|
503
|
+
bottom: 0;
|
|
504
|
+
width: 300px;
|
|
505
|
+
background: #fff;
|
|
506
|
+
}
|
|
507
|
+
[forDrawerBackdrop] {
|
|
508
|
+
position: absolute;
|
|
509
|
+
inset: 0;
|
|
510
|
+
background: rgba(0, 0, 0, 0.4);
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
**`[container]` + `[modal]="true"` — region-isolating modal.** When `modal` is `true` alongside `container`, the drawer isolates **within the container**:
|
|
515
|
+
|
|
516
|
+
- **Focus trap** stays scoped to the drawer surface (unchanged from non-contained modal mode).
|
|
517
|
+
- **Inert siblings** are applied to the container's other children only — body-level siblings outside the container stay fully interactive.
|
|
518
|
+
- **Scroll lock** targets the container's own `overflow`, not `<body>` — the rest of the page keeps scrolling.
|
|
519
|
+
|
|
520
|
+
```html
|
|
521
|
+
<section
|
|
522
|
+
#listBox
|
|
523
|
+
data-testid="container"
|
|
524
|
+
style="position: relative; height: 400px; overflow: auto;"
|
|
525
|
+
>
|
|
526
|
+
<button forDrawerTrigger [(open)]="open">Open</button>
|
|
527
|
+
|
|
528
|
+
@if (open()) {
|
|
529
|
+
<div forDrawer side="right" [modal]="true" [container]="listBox" (dismiss)="open.set(false)">
|
|
530
|
+
<div forDrawerBackdrop></div>
|
|
531
|
+
<h2 forDrawerTitle>Filters</h2>
|
|
532
|
+
<button forDrawerClose>Close</button>
|
|
533
|
+
</div>
|
|
534
|
+
}
|
|
535
|
+
</section>
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**Programmatic equivalent.** `ForDrawerManager.open(Cmp, { modal: true, container: boxEl })` portals both the surface and any `[forDrawerBackdrop]` inside the opened component into `boxEl` and scopes all three isolation behaviours to it.
|
|
539
|
+
|
|
540
|
+
**Swipe-to-dismiss and snap points** keep working inside a container — the math is dimension-based (`getBoundingClientRect`), not viewport-based.
|
|
541
|
+
|
|
542
|
+
**`scaleBackground` / nested visual transforms** assume a full-screen model and are not meaningful inside a container.
|
|
543
|
+
|
|
544
|
+
## Mount/unmount and animations
|
|
545
|
+
|
|
546
|
+
The directive deliberately does **not** apply `[hidden]` to its surface. Wrap with `@if (open())` and use Angular's native `animate.enter` / `animate.leave` for transitions. `data-state="open"` reflects the logical state for CSS hooks but is never tied to visibility — that is `@if`'s job.
|
|
547
|
+
|
|
548
|
+
```html
|
|
549
|
+
@if (open()) {
|
|
550
|
+
<div
|
|
551
|
+
forDrawer
|
|
552
|
+
side="bottom"
|
|
553
|
+
(dismiss)="open.set(false)"
|
|
554
|
+
animate.enter="slide-up"
|
|
555
|
+
animate.leave="slide-down"
|
|
556
|
+
>
|
|
557
|
+
…
|
|
558
|
+
</div>
|
|
559
|
+
}
|
|
560
|
+
```
|