@ng-zen/cli 22.5.2 → 22.6.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/README.md +1 -0
- package/package.json +2 -1
- package/schematics/ui/files/drawer/drawer-gesture.spec.ts +100 -0
- package/schematics/ui/files/drawer/drawer-gesture.ts +162 -0
- package/schematics/ui/files/drawer/drawer.html +24 -0
- package/schematics/ui/files/drawer/drawer.scss +253 -0
- package/schematics/ui/files/drawer/drawer.spec.ts +267 -0
- package/schematics/ui/files/drawer/drawer.stories.ts +200 -0
- package/schematics/ui/files/drawer/drawer.ts +299 -0
- package/schematics/ui/files/drawer/drawer.types.ts +14 -0
- package/schematics/ui/files/drawer/index.ts +1 -0
- package/schematics/ui/schema.js.map +1 -1
- package/schematics/ui/schema.json +1 -0
- package/schematics/ui/schema.ts +1 -0
package/README.md
CHANGED
|
@@ -167,6 +167,7 @@ ng generate @ng-zen/cli:ui --ui button input --stories
|
|
|
167
167
|
| ----------- | ---------------------------- | ------------------------------------------------------------------------- |
|
|
168
168
|
| **Button** | Interactive buttons | Primary/secondary variants, loading states, icons |
|
|
169
169
|
| **Dialog** | Native modal dialogs | Native dialog element, service API, size variants, backdrop config |
|
|
170
|
+
| **Drawer** | Side-sheet overlays | Native dialog element, swipe-to-dismiss, 4 sides, size variants |
|
|
170
171
|
| **Pin** | Anchor-positioned overlay | CSS Anchor Positioning, TemplateRef content, position and offset controls |
|
|
171
172
|
| **Popover** | Native Popover API directive | Template/string content, placements, click toggle, CSS anchor positioning |
|
|
172
173
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ng-zen/cli",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.6.0",
|
|
4
4
|
"description": "Angular CLI generator for creating fully customizable UI components.",
|
|
5
5
|
"license": "BSD-2-Clause",
|
|
6
6
|
"private": false,
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"checkbox",
|
|
57
57
|
"dialog",
|
|
58
58
|
"divider",
|
|
59
|
+
"drawer",
|
|
59
60
|
"form-control",
|
|
60
61
|
"icon",
|
|
61
62
|
"input",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { GestureCallbacks } from './drawer.types';
|
|
4
|
+
import { DrawerGesture } from './drawer-gesture';
|
|
5
|
+
|
|
6
|
+
const W = 384;
|
|
7
|
+
|
|
8
|
+
function mockCb(): GestureCallbacks & { el: { style: Record<string, string> } } {
|
|
9
|
+
const el = { style: {} as Record<string, string> };
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
el,
|
|
13
|
+
onOpenChange: vi.fn(),
|
|
14
|
+
setDragStyle: vi.fn((_cssVar: string, value: string) => {
|
|
15
|
+
el.style[_cssVar] = value;
|
|
16
|
+
}),
|
|
17
|
+
removeDragStyle: vi.fn((cssVar: string) => {
|
|
18
|
+
delete el.style[cssVar];
|
|
19
|
+
}),
|
|
20
|
+
getDimension: vi.fn(() => W),
|
|
21
|
+
setPointerCapture: vi.fn(),
|
|
22
|
+
setAttribute: vi.fn(),
|
|
23
|
+
removeAttribute: vi.fn(),
|
|
24
|
+
setOverscrollStyle: vi.fn((sizePx: number) => {
|
|
25
|
+
el.style['width'] = `${sizePx}px`;
|
|
26
|
+
}),
|
|
27
|
+
removeOverscrollStyle: vi.fn(() => {
|
|
28
|
+
delete el.style['width'];
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function down(g: DrawerGesture, x: number, cb: GestureCallbacks): void {
|
|
34
|
+
g.pointerDown({ clientX: x, clientY: 200, pointerId: 1, button: 0 } as PointerEvent, 'right', cb);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function move(g: DrawerGesture, x: number, cb: GestureCallbacks): void {
|
|
38
|
+
g.pointerMove({ clientX: x, clientY: 200 } as PointerEvent, 'right', cb);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function up(g: DrawerGesture, cb: GestureCallbacks): void {
|
|
42
|
+
g.pointerUp(1, 'right', cb);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('DrawerGesture', () => {
|
|
46
|
+
describe('overscroll → close reversal', () => {
|
|
47
|
+
it('overscroll extends width, not translate', () => {
|
|
48
|
+
const g = new DrawerGesture();
|
|
49
|
+
const cb = mockCb();
|
|
50
|
+
|
|
51
|
+
down(g, 500, cb);
|
|
52
|
+
move(g, 400, cb);
|
|
53
|
+
|
|
54
|
+
expect(cb.el.style['width']).toBeTruthy();
|
|
55
|
+
expect(cb.el.style['--zen-drawer-drag']).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('reversal closes without width jump', () => {
|
|
59
|
+
const g = new DrawerGesture();
|
|
60
|
+
const cb = mockCb();
|
|
61
|
+
|
|
62
|
+
down(g, 500, cb);
|
|
63
|
+
move(g, 400, cb);
|
|
64
|
+
const w1 = parseFloat(cb.el.style['width'] ?? '0');
|
|
65
|
+
expect(w1).toBeGreaterThan(W);
|
|
66
|
+
|
|
67
|
+
move(g, 480, cb);
|
|
68
|
+
const w2 = parseFloat(cb.el.style['width'] ?? '0');
|
|
69
|
+
expect(w2).toBeLessThanOrEqual(w1);
|
|
70
|
+
|
|
71
|
+
move(g, 510, cb);
|
|
72
|
+
expect(cb.el.style['width']).toBeUndefined();
|
|
73
|
+
expect(cb.el.style['--zen-drawer-drag']).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('close translate is 1:1 with pointer', () => {
|
|
77
|
+
const g = new DrawerGesture();
|
|
78
|
+
const cb = mockCb();
|
|
79
|
+
|
|
80
|
+
down(g, 500, cb);
|
|
81
|
+
move(g, 530, cb);
|
|
82
|
+
|
|
83
|
+
const pct = parseFloat(cb.el.style['--zen-drawer-drag'] ?? '0');
|
|
84
|
+
expect(pct).toBeCloseTo((30 / W) * 100, 2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('release during overscroll stays open', () => {
|
|
88
|
+
const g = new DrawerGesture();
|
|
89
|
+
const cb = mockCb();
|
|
90
|
+
|
|
91
|
+
down(g, 500, cb);
|
|
92
|
+
move(g, 400, cb);
|
|
93
|
+
up(g, cb);
|
|
94
|
+
|
|
95
|
+
expect(cb.onOpenChange).not.toHaveBeenCalled();
|
|
96
|
+
expect(cb.el.style['width']).toBeUndefined();
|
|
97
|
+
expect(cb.el.style['--zen-drawer-drag']).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { DrawerSide, GestureCallbacks } from './drawer.types';
|
|
2
|
+
|
|
3
|
+
const FLING_VELOCITY_THRESHOLD = 0.5;
|
|
4
|
+
const CLOSE_THRESHOLD_RATIO = 0.3;
|
|
5
|
+
const CLOSE_THRESHOLD_MIN = 80;
|
|
6
|
+
const LOCK_THRESHOLD = 5;
|
|
7
|
+
|
|
8
|
+
const OVERSCROLL_DAMP_RATE = 0.003;
|
|
9
|
+
const OVERSCROLL_MAX_CAP = 120;
|
|
10
|
+
|
|
11
|
+
export class DrawerGesture {
|
|
12
|
+
private startX = 0;
|
|
13
|
+
private startY = 0;
|
|
14
|
+
private dragCandidate = false;
|
|
15
|
+
private dragLocked = false;
|
|
16
|
+
private offset = 0;
|
|
17
|
+
private startTime = 0;
|
|
18
|
+
private lastMoveTime = 0;
|
|
19
|
+
private lastMoveX = 0;
|
|
20
|
+
private lastMoveY = 0;
|
|
21
|
+
private restDimension = 0;
|
|
22
|
+
|
|
23
|
+
get isLocked(): boolean {
|
|
24
|
+
return this.dragLocked;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pointerDown(event: PointerEvent, side: DrawerSide, cb: GestureCallbacks): void {
|
|
28
|
+
this.startX = event.clientX;
|
|
29
|
+
this.startY = event.clientY;
|
|
30
|
+
this.dragCandidate = true;
|
|
31
|
+
this.dragLocked = false;
|
|
32
|
+
this.offset = 0;
|
|
33
|
+
this.startTime = Date.now();
|
|
34
|
+
this.lastMoveTime = this.startTime;
|
|
35
|
+
this.lastMoveX = event.clientX;
|
|
36
|
+
this.lastMoveY = event.clientY;
|
|
37
|
+
this.restDimension = cb.getDimension();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pointerMove(event: PointerEvent, side: DrawerSide, cb: GestureCallbacks): void {
|
|
41
|
+
if (!this.dragCandidate) return;
|
|
42
|
+
|
|
43
|
+
const dx = event.clientX - this.startX;
|
|
44
|
+
const dy = event.clientY - this.startY;
|
|
45
|
+
const absDx = Math.abs(dx);
|
|
46
|
+
const absDy = Math.abs(dy);
|
|
47
|
+
|
|
48
|
+
if (!this.dragLocked) {
|
|
49
|
+
if (absDx < LOCK_THRESHOLD && absDy < LOCK_THRESHOLD) return;
|
|
50
|
+
|
|
51
|
+
const isHorizontal = absDx > absDy;
|
|
52
|
+
const sideAxis = side === 'left' || side === 'right' ? 'x' : 'y';
|
|
53
|
+
const movementAxis = isHorizontal ? 'x' : 'y';
|
|
54
|
+
|
|
55
|
+
if (sideAxis !== movementAxis) {
|
|
56
|
+
this.dragCandidate = false;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.dragLocked = true;
|
|
61
|
+
cb.setPointerCapture(event.pointerId);
|
|
62
|
+
cb.setAttribute('data-swiping', '');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.lastMoveTime = Date.now();
|
|
66
|
+
this.lastMoveX = event.clientX;
|
|
67
|
+
this.lastMoveY = event.clientY;
|
|
68
|
+
|
|
69
|
+
this.transformDrag(dx, dy, side, cb);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pointerUp(pointerId: number, side: DrawerSide, cb: GestureCallbacks): void {
|
|
73
|
+
if (!this.dragLocked) {
|
|
74
|
+
this.dragCandidate = false;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const velocity = this.computeVelocity();
|
|
79
|
+
this.finishDrag(side, velocity, cb);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pointerCancel(pointerId: number, side: DrawerSide, cb: GestureCallbacks): void {
|
|
83
|
+
if (!this.dragLocked) {
|
|
84
|
+
this.dragCandidate = false;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.finishDrag(side, { x: 0, y: 0 }, cb);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private transformDrag(dx: number, dy: number, side: DrawerSide, cb: GestureCallbacks): void {
|
|
91
|
+
let closeDist: number;
|
|
92
|
+
|
|
93
|
+
switch (side) {
|
|
94
|
+
case 'right':
|
|
95
|
+
closeDist = dx;
|
|
96
|
+
break;
|
|
97
|
+
case 'left':
|
|
98
|
+
closeDist = -dx;
|
|
99
|
+
break;
|
|
100
|
+
case 'bottom':
|
|
101
|
+
closeDist = dy;
|
|
102
|
+
break;
|
|
103
|
+
case 'top':
|
|
104
|
+
closeDist = -dy;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (closeDist < 0) {
|
|
109
|
+
const ext = this.dampOverscroll(-closeDist);
|
|
110
|
+
this.offset = 0;
|
|
111
|
+
cb.removeDragStyle('--zen-drawer-drag');
|
|
112
|
+
cb.setOverscrollStyle(this.restDimension + ext);
|
|
113
|
+
} else {
|
|
114
|
+
this.offset = closeDist;
|
|
115
|
+
cb.removeOverscrollStyle();
|
|
116
|
+
cb.setDragStyle('--zen-drawer-drag', `${(closeDist / this.restDimension) * 100}%`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private dampOverscroll(dist: number): number {
|
|
121
|
+
const maxOverscroll = Math.min(OVERSCROLL_MAX_CAP, this.restDimension * 0.25);
|
|
122
|
+
return maxOverscroll * (1 - Math.exp(-dist * OVERSCROLL_DAMP_RATE));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private finishDrag(side: DrawerSide, velocity: { x: number; y: number }, cb: GestureCallbacks): void {
|
|
126
|
+
cb.removeAttribute('data-swiping');
|
|
127
|
+
cb.removeOverscrollStyle();
|
|
128
|
+
|
|
129
|
+
const dimension = cb.getDimension();
|
|
130
|
+
const threshold = Math.max(CLOSE_THRESHOLD_MIN, dimension * CLOSE_THRESHOLD_RATIO);
|
|
131
|
+
const flingInCloseDirection = this.isFlingInCloseDirection(side, velocity);
|
|
132
|
+
|
|
133
|
+
if (this.offset >= threshold || flingInCloseDirection) {
|
|
134
|
+
cb.onOpenChange(false);
|
|
135
|
+
} else {
|
|
136
|
+
cb.removeDragStyle('--zen-drawer-drag');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.dragCandidate = false;
|
|
140
|
+
this.dragLocked = false;
|
|
141
|
+
this.offset = 0;
|
|
142
|
+
this.restDimension = 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private computeVelocity(): { x: number; y: number } {
|
|
146
|
+
const dt = this.lastMoveTime - this.startTime;
|
|
147
|
+
if (dt <= 0) return { x: 0, y: 0 };
|
|
148
|
+
return {
|
|
149
|
+
x: (this.lastMoveX - this.startX) / dt,
|
|
150
|
+
y: (this.lastMoveY - this.startY) / dt,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private isFlingInCloseDirection(side: DrawerSide, velocity: { x: number; y: number }): boolean {
|
|
155
|
+
const v = FLING_VELOCITY_THRESHOLD;
|
|
156
|
+
if (side === 'right') return velocity.x > v;
|
|
157
|
+
if (side === 'left') return velocity.x < -v;
|
|
158
|
+
if (side === 'bottom') return velocity.y > v;
|
|
159
|
+
if (side === 'top') return velocity.y < -v;
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<dialog
|
|
2
|
+
#drawerDialog
|
|
3
|
+
class="zen-drawer"
|
|
4
|
+
[attr.data-side]="side()"
|
|
5
|
+
[attr.data-size]="size()"
|
|
6
|
+
[attr.data-swipe-direction]="side()"
|
|
7
|
+
(cancel)="cancel($event)"
|
|
8
|
+
(close)="close()"
|
|
9
|
+
(pointercancel)="pointerCancel($event)"
|
|
10
|
+
(pointerdown)="pointerDown($event)"
|
|
11
|
+
(pointermove)="pointerMove($event)"
|
|
12
|
+
(pointerup)="pointerUp($event)"
|
|
13
|
+
>
|
|
14
|
+
@if (swipeHandle()) {
|
|
15
|
+
<div class="zen-drawer-swipe-handle"></div>
|
|
16
|
+
}
|
|
17
|
+
<ng-content select="[drawer-header]" />
|
|
18
|
+
<div class="zen-drawer-content">
|
|
19
|
+
<ng-content />
|
|
20
|
+
</div>
|
|
21
|
+
<div class="zen-drawer-footer">
|
|
22
|
+
<ng-content select="[drawer-footer]" />
|
|
23
|
+
</div>
|
|
24
|
+
</dialog>
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
@use 'sass:map';
|
|
2
|
+
|
|
3
|
+
$transition-duration: var(--zen-transition-duration, 0.3s);
|
|
4
|
+
$transition-timing: cubic-bezier(0.32, 0.72, 0, 1);
|
|
5
|
+
$sides: (
|
|
6
|
+
right: (
|
|
7
|
+
closed: translateX(100%),
|
|
8
|
+
open: translateX(var(--zen-drawer-drag, 0px)),
|
|
9
|
+
),
|
|
10
|
+
left: (
|
|
11
|
+
closed: translateX(-100%),
|
|
12
|
+
open: translateX(calc(-1 * var(--zen-drawer-drag, 0px))),
|
|
13
|
+
),
|
|
14
|
+
top: (
|
|
15
|
+
closed: translateY(-100%),
|
|
16
|
+
open: translateY(calc(-1 * var(--zen-drawer-drag, 0px))),
|
|
17
|
+
),
|
|
18
|
+
bottom: (
|
|
19
|
+
closed: translateY(100%),
|
|
20
|
+
open: translateY(var(--zen-drawer-drag, 0px)),
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
$default-side-width: min(24rem, 75vw);
|
|
24
|
+
$default-vertical-max-height: calc(100dvh - 6rem);
|
|
25
|
+
|
|
26
|
+
.zen-drawer {
|
|
27
|
+
--zen-drawer-padding: 1rem;
|
|
28
|
+
--zen-drawer-bg: #fff;
|
|
29
|
+
--zen-drawer-leading-border-radius: 12px;
|
|
30
|
+
--zen-drawer-shadow: 0 4px 24px rgb(0 0 0 / 20%);
|
|
31
|
+
--zen-drawer-backdrop-bg: rgb(0 0 0 / 50%);
|
|
32
|
+
--zen-drawer-overlay-min-opacity: 0;
|
|
33
|
+
--zen-drawer-inset: 0px;
|
|
34
|
+
--zen-drawer-drag: 0px;
|
|
35
|
+
--zen-drawer-height: auto;
|
|
36
|
+
--zen-size-inline-sm: clamp(16rem, 25vw, 24rem);
|
|
37
|
+
--zen-size-inline-md: clamp(22rem, 40vw, 34rem);
|
|
38
|
+
--zen-size-inline-lg: clamp(30rem, 55vw, 46rem);
|
|
39
|
+
--zen-size-inline-xl: clamp(38rem, 70vw, 58rem);
|
|
40
|
+
--zen-size-inline-full: calc(100% - var(--zen-drawer-inset) * 2);
|
|
41
|
+
--zen-size-block-sm: clamp(14rem, 30dvh, 22rem);
|
|
42
|
+
--zen-size-block-md: clamp(18rem, 45dvh, 30rem);
|
|
43
|
+
--zen-size-block-lg: clamp(24rem, 60dvh, 38rem);
|
|
44
|
+
--zen-size-block-xl: clamp(30rem, 75dvh, 46rem);
|
|
45
|
+
--zen-size-block-full: calc(100% - var(--zen-drawer-inset) * 2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
:host {
|
|
49
|
+
display: contents;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* stylelint-disable-next-line no-duplicate-selectors -- separate variables and styles */
|
|
53
|
+
.zen-drawer {
|
|
54
|
+
display: none;
|
|
55
|
+
position: fixed;
|
|
56
|
+
box-sizing: border-box;
|
|
57
|
+
padding: var(--zen-drawer-padding);
|
|
58
|
+
background: var(--zen-drawer-bg);
|
|
59
|
+
border: none;
|
|
60
|
+
box-shadow: var(--zen-drawer-shadow);
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
opacity: 0;
|
|
63
|
+
transition:
|
|
64
|
+
transform $transition-duration $transition-timing,
|
|
65
|
+
opacity $transition-duration ease,
|
|
66
|
+
width $transition-duration ease,
|
|
67
|
+
height $transition-duration ease;
|
|
68
|
+
|
|
69
|
+
@each $side, $transforms in $sides {
|
|
70
|
+
$closed: map.get($transforms, closed);
|
|
71
|
+
$open: map.get($transforms, open);
|
|
72
|
+
|
|
73
|
+
&[data-side='#{$side}'] {
|
|
74
|
+
transform: $closed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
&[open][data-side='#{$side}'] {
|
|
78
|
+
transform: $open;
|
|
79
|
+
|
|
80
|
+
@starting-style {
|
|
81
|
+
opacity: 0;
|
|
82
|
+
transform: $closed;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
&[open][data-closing][data-side='#{$side}'] {
|
|
87
|
+
opacity: 0;
|
|
88
|
+
transform: $closed !important;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&[open] {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
opacity: 1;
|
|
96
|
+
|
|
97
|
+
@starting-style {
|
|
98
|
+
opacity: 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
&[open][data-swiping] {
|
|
103
|
+
cursor: grabbing;
|
|
104
|
+
transition: none !important;
|
|
105
|
+
user-select: none;
|
|
106
|
+
|
|
107
|
+
.zen-drawer-swipe-handle {
|
|
108
|
+
cursor: grabbing;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
&[data-side='right'],
|
|
113
|
+
&[data-side='left'] {
|
|
114
|
+
inset: var(--zen-drawer-inset);
|
|
115
|
+
margin: 0;
|
|
116
|
+
height: 100%;
|
|
117
|
+
max-height: 100%;
|
|
118
|
+
width: var(--zen-drawer-width, $default-side-width);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&[data-side='right'] {
|
|
122
|
+
inset: var(--zen-drawer-inset) var(--zen-drawer-inset) var(--zen-drawer-inset) auto;
|
|
123
|
+
border-radius: var(--zen-drawer-leading-border-radius) 0 0 var(--zen-drawer-leading-border-radius);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
&[data-side='left'] {
|
|
127
|
+
inset: var(--zen-drawer-inset) auto var(--zen-drawer-inset) var(--zen-drawer-inset);
|
|
128
|
+
border-radius: 0 var(--zen-drawer-leading-border-radius) var(--zen-drawer-leading-border-radius) 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
&[data-side='top'],
|
|
132
|
+
&[data-side='bottom'] {
|
|
133
|
+
margin: 0;
|
|
134
|
+
width: auto;
|
|
135
|
+
height: var(--zen-drawer-height, auto);
|
|
136
|
+
max-height: $default-vertical-max-height;
|
|
137
|
+
max-width: 100vw;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
&[data-side='top'] {
|
|
141
|
+
inset: var(--zen-drawer-inset) var(--zen-drawer-inset) auto var(--zen-drawer-inset);
|
|
142
|
+
border-radius: 0 0 var(--zen-drawer-leading-border-radius) var(--zen-drawer-leading-border-radius);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
&[data-side='bottom'] {
|
|
146
|
+
inset: auto var(--zen-drawer-inset) var(--zen-drawer-inset) var(--zen-drawer-inset);
|
|
147
|
+
border-radius: var(--zen-drawer-leading-border-radius) var(--zen-drawer-leading-border-radius) 0 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@each $name in (sm, md, lg, xl, full) {
|
|
151
|
+
&[data-side='right'][data-size='#{$name}'],
|
|
152
|
+
&[data-side='left'][data-size='#{$name}'] {
|
|
153
|
+
width: var(--zen-size-inline-#{$name});
|
|
154
|
+
max-width: 100vw;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
&[data-side='top'][data-size='#{$name}'],
|
|
158
|
+
&[data-side='bottom'][data-size='#{$name}'] {
|
|
159
|
+
height: var(--zen-size-block-#{$name});
|
|
160
|
+
max-height: 100dvh;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
&::backdrop {
|
|
165
|
+
background: var(--zen-drawer-backdrop-bg);
|
|
166
|
+
transition: opacity $transition-duration ease;
|
|
167
|
+
opacity: var(--zen-drawer-overlay-min-opacity);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
&[open]::backdrop {
|
|
171
|
+
opacity: 1;
|
|
172
|
+
|
|
173
|
+
@starting-style {
|
|
174
|
+
opacity: var(--zen-drawer-overlay-min-opacity);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
&[open][data-closing]::backdrop {
|
|
179
|
+
opacity: 0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.zen-drawer-swipe-handle {
|
|
184
|
+
cursor: grab;
|
|
185
|
+
position: absolute;
|
|
186
|
+
z-index: 10;
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
justify-content: center;
|
|
190
|
+
touch-action: none;
|
|
191
|
+
user-select: none;
|
|
192
|
+
|
|
193
|
+
&::after {
|
|
194
|
+
content: '';
|
|
195
|
+
display: block;
|
|
196
|
+
background: hsl(0deg 0% 70%);
|
|
197
|
+
border-radius: 3px;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.zen-drawer[data-side='bottom'] .zen-drawer-swipe-handle,
|
|
202
|
+
.zen-drawer[data-side='top'] .zen-drawer-swipe-handle {
|
|
203
|
+
left: 0;
|
|
204
|
+
right: 0;
|
|
205
|
+
height: 1.5rem;
|
|
206
|
+
width: 100%;
|
|
207
|
+
|
|
208
|
+
&::after {
|
|
209
|
+
width: 48px;
|
|
210
|
+
height: 5px;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.zen-drawer[data-side='bottom'] .zen-drawer-swipe-handle {
|
|
215
|
+
top: 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.zen-drawer[data-side='top'] .zen-drawer-swipe-handle {
|
|
219
|
+
bottom: 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.zen-drawer[data-side='right'] .zen-drawer-swipe-handle,
|
|
223
|
+
.zen-drawer[data-side='left'] .zen-drawer-swipe-handle {
|
|
224
|
+
top: 0;
|
|
225
|
+
bottom: 0;
|
|
226
|
+
width: 1.5rem;
|
|
227
|
+
|
|
228
|
+
&::after {
|
|
229
|
+
width: 5px;
|
|
230
|
+
height: 48px;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.zen-drawer[data-side='right'] .zen-drawer-swipe-handle {
|
|
235
|
+
left: 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.zen-drawer[data-side='left'] .zen-drawer-swipe-handle {
|
|
239
|
+
right: 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.zen-drawer-footer {
|
|
243
|
+
&:not(:empty) {
|
|
244
|
+
flex-shrink: 0;
|
|
245
|
+
padding-top: var(--zen-drawer-padding);
|
|
246
|
+
border-top: 1px solid hsl(0deg 0% 90%);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.zen-drawer-content {
|
|
251
|
+
overflow: auto;
|
|
252
|
+
flex: 1;
|
|
253
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { Component, provideZonelessChangeDetection, signal } from '@angular/core';
|
|
2
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
3
|
+
import { By } from '@angular/platform-browser';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { ZenDrawer } from './drawer';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
template: `
|
|
10
|
+
<zen-drawer
|
|
11
|
+
[backdrop]="backdrop()"
|
|
12
|
+
[closeOnEscape]="closeOnEscape()"
|
|
13
|
+
[handleOnly]="handleOnly()"
|
|
14
|
+
[scaleBackground]="scaleBackground()"
|
|
15
|
+
[side]="side()"
|
|
16
|
+
[size]="size()"
|
|
17
|
+
[swipeHandle]="swipeHandle()"
|
|
18
|
+
[(open)]="isOpen"
|
|
19
|
+
>
|
|
20
|
+
<div drawer-header>Test Drawer</div>
|
|
21
|
+
<p>Drawer content</p>
|
|
22
|
+
<div drawer-footer>Footer text</div>
|
|
23
|
+
</zen-drawer>
|
|
24
|
+
`,
|
|
25
|
+
standalone: true,
|
|
26
|
+
imports: [ZenDrawer],
|
|
27
|
+
})
|
|
28
|
+
class DrawerTestComponent {
|
|
29
|
+
readonly isOpen = signal(false);
|
|
30
|
+
readonly side = signal<'left' | 'right' | 'top' | 'bottom'>('right');
|
|
31
|
+
readonly size = signal<'sm' | 'md' | 'lg' | 'xl' | 'full'>('md');
|
|
32
|
+
readonly closeOnEscape = signal(true);
|
|
33
|
+
readonly swipeHandle = signal(true);
|
|
34
|
+
readonly handleOnly = signal(false);
|
|
35
|
+
readonly scaleBackground = signal(false);
|
|
36
|
+
readonly backdrop = signal(true);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getDrawerEl(fixture: ComponentFixture<unknown>): HTMLDialogElement | null {
|
|
40
|
+
return fixture.nativeElement.querySelector('zen-drawer dialog.zen-drawer') as HTMLDialogElement | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('ZenDrawer', () => {
|
|
44
|
+
beforeEach(async () => {
|
|
45
|
+
HTMLDialogElement.prototype.showModal = vi.fn();
|
|
46
|
+
HTMLDialogElement.prototype.close = vi.fn();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('Component usage', () => {
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
await TestBed.configureTestingModule({
|
|
52
|
+
imports: [DrawerTestComponent],
|
|
53
|
+
providers: [provideZonelessChangeDetection()],
|
|
54
|
+
}).compileComponents();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
document.body.classList.remove('zen-drawer-open');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should create', () => {
|
|
62
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
63
|
+
fixture.detectChanges();
|
|
64
|
+
expect(getDrawerEl(fixture)).toBeTruthy();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should call showModal when open is set to true', async () => {
|
|
68
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
69
|
+
fixture.detectChanges();
|
|
70
|
+
|
|
71
|
+
const drawerEl = getDrawerEl(fixture)!;
|
|
72
|
+
expect(drawerEl.showModal).not.toHaveBeenCalled();
|
|
73
|
+
|
|
74
|
+
fixture.componentInstance.isOpen.set(true);
|
|
75
|
+
fixture.detectChanges();
|
|
76
|
+
await fixture.whenStable();
|
|
77
|
+
|
|
78
|
+
expect(drawerEl.showModal).toHaveBeenCalledTimes(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should call close when open is set to false', async () => {
|
|
82
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
83
|
+
fixture.componentInstance.isOpen.set(true);
|
|
84
|
+
fixture.detectChanges();
|
|
85
|
+
await fixture.whenStable();
|
|
86
|
+
|
|
87
|
+
const drawerEl = getDrawerEl(fixture)!;
|
|
88
|
+
Object.defineProperty(drawerEl, 'open', { value: true, writable: true });
|
|
89
|
+
|
|
90
|
+
fixture.componentInstance.isOpen.set(false);
|
|
91
|
+
fixture.detectChanges();
|
|
92
|
+
await fixture.whenStable();
|
|
93
|
+
await new Promise(r => setTimeout(r, 400));
|
|
94
|
+
|
|
95
|
+
expect(drawerEl.close).toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should project header content via drawer-header slot', () => {
|
|
99
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
100
|
+
fixture.detectChanges();
|
|
101
|
+
|
|
102
|
+
const header = fixture.nativeElement.querySelector('[drawer-header]');
|
|
103
|
+
expect(header?.textContent).toBe('Test Drawer');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should project footer content via drawer-footer slot', () => {
|
|
107
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
108
|
+
fixture.detectChanges();
|
|
109
|
+
|
|
110
|
+
const footer = fixture.nativeElement.querySelector('[drawer-footer]');
|
|
111
|
+
expect(footer?.textContent?.trim()).toBe('Footer text');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should apply side attribute', () => {
|
|
115
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
116
|
+
fixture.componentInstance.side.set('left');
|
|
117
|
+
fixture.detectChanges();
|
|
118
|
+
|
|
119
|
+
const drawerEl = getDrawerEl(fixture)!;
|
|
120
|
+
expect(drawerEl.getAttribute('data-side')).toBe('left');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should apply size attribute', () => {
|
|
124
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
125
|
+
fixture.componentInstance.size.set('lg');
|
|
126
|
+
fixture.detectChanges();
|
|
127
|
+
|
|
128
|
+
const drawerEl = getDrawerEl(fixture)!;
|
|
129
|
+
expect(drawerEl.getAttribute('data-size')).toBe('lg');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should apply data-swipe-direction', () => {
|
|
133
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
134
|
+
fixture.componentInstance.side.set('bottom');
|
|
135
|
+
fixture.detectChanges();
|
|
136
|
+
|
|
137
|
+
const drawerEl = getDrawerEl(fixture)!;
|
|
138
|
+
expect(drawerEl.getAttribute('data-swipe-direction')).toBe('bottom');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should close when close is called', async () => {
|
|
142
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
143
|
+
fixture.componentInstance.isOpen.set(true);
|
|
144
|
+
fixture.detectChanges();
|
|
145
|
+
await fixture.whenStable();
|
|
146
|
+
|
|
147
|
+
const drawer = fixture.debugElement.query(By.directive(ZenDrawer)).componentInstance;
|
|
148
|
+
drawer.close();
|
|
149
|
+
fixture.detectChanges();
|
|
150
|
+
|
|
151
|
+
expect(fixture.componentInstance.isOpen()).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should not close on escape when closeOnEscape is false', async () => {
|
|
155
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
156
|
+
fixture.componentInstance.isOpen.set(true);
|
|
157
|
+
fixture.componentInstance.closeOnEscape.set(false);
|
|
158
|
+
fixture.detectChanges();
|
|
159
|
+
await fixture.whenStable();
|
|
160
|
+
|
|
161
|
+
const drawerEl = getDrawerEl(fixture)!;
|
|
162
|
+
const cancelEvent = new Event('cancel', { bubbles: true, cancelable: true });
|
|
163
|
+
drawerEl.dispatchEvent(cancelEvent);
|
|
164
|
+
fixture.detectChanges();
|
|
165
|
+
|
|
166
|
+
expect(fixture.componentInstance.isOpen()).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should render swipe handle when swipeHandle is true', () => {
|
|
170
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
171
|
+
fixture.detectChanges();
|
|
172
|
+
|
|
173
|
+
const handle = fixture.nativeElement.querySelector('.zen-drawer-swipe-handle');
|
|
174
|
+
expect(handle).toBeTruthy();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should not render swipe handle when swipeHandle is false', () => {
|
|
178
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
179
|
+
fixture.componentInstance.swipeHandle.set(false);
|
|
180
|
+
fixture.detectChanges();
|
|
181
|
+
|
|
182
|
+
const handle = fixture.nativeElement.querySelector('.zen-drawer-swipe-handle');
|
|
183
|
+
expect(handle).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should toggle zen-drawer-open class on body when scaleBackground is true', async () => {
|
|
187
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
188
|
+
fixture.componentInstance.scaleBackground.set(true);
|
|
189
|
+
fixture.detectChanges();
|
|
190
|
+
|
|
191
|
+
fixture.componentInstance.isOpen.set(true);
|
|
192
|
+
fixture.detectChanges();
|
|
193
|
+
await fixture.whenStable();
|
|
194
|
+
|
|
195
|
+
expect(document.body.classList.contains('zen-drawer-open')).toBe(true);
|
|
196
|
+
|
|
197
|
+
const drawerEl = getDrawerEl(fixture)!;
|
|
198
|
+
Object.defineProperty(drawerEl, 'open', { value: true, writable: true });
|
|
199
|
+
fixture.componentInstance.isOpen.set(false);
|
|
200
|
+
fixture.detectChanges();
|
|
201
|
+
|
|
202
|
+
expect(document.body.classList.contains('zen-drawer-open')).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should not toggle zen-drawer-open class on body when scaleBackground is false', async () => {
|
|
206
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
207
|
+
fixture.componentInstance.scaleBackground.set(false);
|
|
208
|
+
fixture.detectChanges();
|
|
209
|
+
|
|
210
|
+
fixture.componentInstance.isOpen.set(true);
|
|
211
|
+
fixture.detectChanges();
|
|
212
|
+
await fixture.whenStable();
|
|
213
|
+
|
|
214
|
+
expect(document.body.classList.contains('zen-drawer-open')).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should close on backdrop click when backdrop is true', async () => {
|
|
218
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
219
|
+
fixture.componentInstance.isOpen.set(true);
|
|
220
|
+
fixture.detectChanges();
|
|
221
|
+
await fixture.whenStable();
|
|
222
|
+
|
|
223
|
+
const drawerEl = getDrawerEl(fixture)!;
|
|
224
|
+
const pdEvent = new PointerEvent('pointerdown', { bubbles: true, cancelable: true, button: 0 });
|
|
225
|
+
Object.defineProperty(pdEvent, 'target', { value: drawerEl });
|
|
226
|
+
drawerEl.dispatchEvent(pdEvent);
|
|
227
|
+
|
|
228
|
+
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
|
|
229
|
+
Object.defineProperty(clickEvent, 'target', { value: drawerEl });
|
|
230
|
+
drawerEl.dispatchEvent(clickEvent);
|
|
231
|
+
fixture.detectChanges();
|
|
232
|
+
|
|
233
|
+
expect(fixture.componentInstance.isOpen()).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should not close on backdrop click when backdrop is false', async () => {
|
|
237
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
238
|
+
fixture.componentInstance.isOpen.set(true);
|
|
239
|
+
fixture.componentInstance.backdrop.set(false);
|
|
240
|
+
fixture.detectChanges();
|
|
241
|
+
await fixture.whenStable();
|
|
242
|
+
|
|
243
|
+
const drawerEl = getDrawerEl(fixture)!;
|
|
244
|
+
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
|
|
245
|
+
Object.defineProperty(clickEvent, 'target', { value: drawerEl });
|
|
246
|
+
drawerEl.dispatchEvent(clickEvent);
|
|
247
|
+
fixture.detectChanges();
|
|
248
|
+
|
|
249
|
+
expect(fixture.componentInstance.isOpen()).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should not close when clicking inside drawer content', async () => {
|
|
253
|
+
const fixture = TestBed.createComponent(DrawerTestComponent);
|
|
254
|
+
fixture.componentInstance.isOpen.set(true);
|
|
255
|
+
fixture.detectChanges();
|
|
256
|
+
await fixture.whenStable();
|
|
257
|
+
|
|
258
|
+
const contentEl = fixture.nativeElement.querySelector('.zen-drawer-content');
|
|
259
|
+
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
|
|
260
|
+
Object.defineProperty(clickEvent, 'target', { value: contentEl });
|
|
261
|
+
contentEl.dispatchEvent(clickEvent);
|
|
262
|
+
fixture.detectChanges();
|
|
263
|
+
|
|
264
|
+
expect(fixture.componentInstance.isOpen()).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { signal } from '@angular/core';
|
|
2
|
+
import { argsToTemplate, Meta, moduleMetadata, StoryObj } from '@storybook/angular';
|
|
3
|
+
|
|
4
|
+
import { ZenButton } from '../button';
|
|
5
|
+
import { ZenDrawer } from './drawer';
|
|
6
|
+
import { DrawerSide, DrawerSize } from './drawer.types';
|
|
7
|
+
|
|
8
|
+
type Story = StoryObj<ZenDrawer>;
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
title: 'UI/Drawer',
|
|
12
|
+
component: ZenDrawer,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
decorators: [
|
|
15
|
+
moduleMetadata({
|
|
16
|
+
imports: [ZenButton, ZenDrawer],
|
|
17
|
+
}),
|
|
18
|
+
],
|
|
19
|
+
argTypes: {
|
|
20
|
+
side: {
|
|
21
|
+
name: 'side',
|
|
22
|
+
control: 'select',
|
|
23
|
+
options: ['left', 'right', 'top', 'bottom'] satisfies DrawerSide[],
|
|
24
|
+
table: {
|
|
25
|
+
category: 'inputs',
|
|
26
|
+
type: { summary: 'string' },
|
|
27
|
+
defaultValue: { summary: 'right' satisfies DrawerSide },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
size: {
|
|
31
|
+
name: 'size',
|
|
32
|
+
control: 'select',
|
|
33
|
+
options: ['sm', 'md', 'lg', 'xl', 'full'] satisfies DrawerSize[],
|
|
34
|
+
table: {
|
|
35
|
+
category: 'inputs',
|
|
36
|
+
type: { summary: 'string' },
|
|
37
|
+
defaultValue: { summary: 'md' satisfies DrawerSize },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
closeOnEscape: { control: 'boolean', table: { category: 'inputs', defaultValue: { summary: 'true' } } },
|
|
41
|
+
backdrop: { control: 'boolean', table: { category: 'inputs', defaultValue: { summary: 'true' } } },
|
|
42
|
+
swipeHandle: { control: 'boolean', table: { category: 'inputs', defaultValue: { summary: 'false' } } },
|
|
43
|
+
handleOnly: { control: 'boolean', table: { category: 'inputs', defaultValue: { summary: 'false' } } },
|
|
44
|
+
scaleBackground: { control: 'boolean', table: { category: 'inputs', defaultValue: { summary: 'false' } } },
|
|
45
|
+
open: { control: 'boolean', table: { readonly: false, category: 'models' } },
|
|
46
|
+
},
|
|
47
|
+
args: {
|
|
48
|
+
side: 'right',
|
|
49
|
+
size: 'md',
|
|
50
|
+
closeOnEscape: true,
|
|
51
|
+
backdrop: true,
|
|
52
|
+
swipeHandle: true,
|
|
53
|
+
handleOnly: false,
|
|
54
|
+
scaleBackground: false,
|
|
55
|
+
open: false,
|
|
56
|
+
},
|
|
57
|
+
} satisfies Meta<ZenDrawer>;
|
|
58
|
+
|
|
59
|
+
export default meta;
|
|
60
|
+
|
|
61
|
+
export const Default: Story = {
|
|
62
|
+
render: ({ open, ...args }) => {
|
|
63
|
+
const mutatedArgs = { ...args, open: signal(open) };
|
|
64
|
+
return {
|
|
65
|
+
props: { ...mutatedArgs },
|
|
66
|
+
template: `
|
|
67
|
+
<button zen-btn (click)="open.set(true)">Open Drawer</button>
|
|
68
|
+
|
|
69
|
+
<zen-drawer #drawer [(open)]="open" ${argsToTemplate(args)}>
|
|
70
|
+
<h2 drawer-header>Drawer Title</h2>
|
|
71
|
+
<p>This is the drawer content. You can put any content here.</p>
|
|
72
|
+
<div drawer-footer>
|
|
73
|
+
<button zen-btn (click)="drawer.close()" variant="filled" color="danger">Close</button>
|
|
74
|
+
</div>
|
|
75
|
+
</zen-drawer>
|
|
76
|
+
`,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const Sides: Story = {
|
|
82
|
+
render: () => ({
|
|
83
|
+
props: {
|
|
84
|
+
openLeft: signal(false),
|
|
85
|
+
openRight: signal(false),
|
|
86
|
+
openTop: signal(false),
|
|
87
|
+
openBottom: signal(false),
|
|
88
|
+
},
|
|
89
|
+
template: `
|
|
90
|
+
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
91
|
+
<button zen-btn (click)="openLeft.set(true)">Left</button>
|
|
92
|
+
<button zen-btn (click)="openRight.set(true)">Right</button>
|
|
93
|
+
<button zen-btn (click)="openTop.set(true)">Top</button>
|
|
94
|
+
<button zen-btn (click)="openBottom.set(true)">Bottom</button>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<zen-drawer [(open)]="openLeft" side="left">
|
|
98
|
+
<h2 drawer-header>Left Drawer</h2>
|
|
99
|
+
<p>Drawer slides in from the left</p>
|
|
100
|
+
</zen-drawer>
|
|
101
|
+
|
|
102
|
+
<zen-drawer [(open)]="openRight" side="right">
|
|
103
|
+
<h2 drawer-header>Right Drawer</h2>
|
|
104
|
+
<p>Drawer slides in from the right</p>
|
|
105
|
+
</zen-drawer>
|
|
106
|
+
|
|
107
|
+
<zen-drawer [(open)]="openTop" side="top">
|
|
108
|
+
<h2 drawer-header>Top Drawer</h2>
|
|
109
|
+
<p>Drawer slides in from the top</p>
|
|
110
|
+
</zen-drawer>
|
|
111
|
+
|
|
112
|
+
<zen-drawer [(open)]="openBottom" side="bottom">
|
|
113
|
+
<h2 drawer-header>Bottom Drawer</h2>
|
|
114
|
+
<p>Drawer slides in from the bottom</p>
|
|
115
|
+
</zen-drawer>
|
|
116
|
+
`,
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const Sizes: Story = {
|
|
121
|
+
render: () => ({
|
|
122
|
+
props: {
|
|
123
|
+
openSm: signal(false),
|
|
124
|
+
openMd: signal(false),
|
|
125
|
+
openLg: signal(false),
|
|
126
|
+
openXl: signal(false),
|
|
127
|
+
openFull: signal(false),
|
|
128
|
+
},
|
|
129
|
+
template: `
|
|
130
|
+
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
131
|
+
<button zen-btn (click)="openSm.set(true)">Small</button>
|
|
132
|
+
<button zen-btn (click)="openMd.set(true)">Medium</button>
|
|
133
|
+
<button zen-btn (click)="openLg.set(true)">Large</button>
|
|
134
|
+
<button zen-btn (click)="openXl.set(true)">Extra Large</button>
|
|
135
|
+
<button zen-btn (click)="openFull.set(true)">Full Screen</button>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<zen-drawer [(open)]="openSm" size="sm">
|
|
139
|
+
<h2 drawer-header>Small Drawer</h2>
|
|
140
|
+
<p>Small drawer content</p>
|
|
141
|
+
</zen-drawer>
|
|
142
|
+
|
|
143
|
+
<zen-drawer [(open)]="openMd" size="md">
|
|
144
|
+
<h2 drawer-header>Medium Drawer</h2>
|
|
145
|
+
<p>Medium drawer content</p>
|
|
146
|
+
</zen-drawer>
|
|
147
|
+
|
|
148
|
+
<zen-drawer [(open)]="openLg" size="lg">
|
|
149
|
+
<h2 drawer-header>Large Drawer</h2>
|
|
150
|
+
<p>Large drawer content with more space for complex layouts.</p>
|
|
151
|
+
</zen-drawer>
|
|
152
|
+
|
|
153
|
+
<zen-drawer [(open)]="openXl" size="xl">
|
|
154
|
+
<h2 drawer-header>Extra Large Drawer</h2>
|
|
155
|
+
<p>Extra large drawer content for data tables, forms, etc.</p>
|
|
156
|
+
</zen-drawer>
|
|
157
|
+
|
|
158
|
+
<zen-drawer [(open)]="openFull" size="full">
|
|
159
|
+
<h2 drawer-header>Full Screen Drawer</h2>
|
|
160
|
+
<p>Full screen drawer content</p>
|
|
161
|
+
</zen-drawer>
|
|
162
|
+
`,
|
|
163
|
+
}),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const HandleOnly: Story = {
|
|
167
|
+
render: () => ({
|
|
168
|
+
props: {
|
|
169
|
+
openHandle: signal(false),
|
|
170
|
+
},
|
|
171
|
+
template: `
|
|
172
|
+
<button zen-btn (click)="openHandle.set(true)">Handle Only</button>
|
|
173
|
+
|
|
174
|
+
<zen-drawer [swipeHandle]="true" [(open)]="openHandle" side="bottom" [handleOnly]="true">
|
|
175
|
+
<h2 drawer-header>Handle Only Drawer</h2>
|
|
176
|
+
<p>Only the grab handle can drag this drawer. Click or drag anywhere else on the content.</p>
|
|
177
|
+
<div style="height: 200px; background: hsl(120deg 80% 95%); border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-top: 1rem;">
|
|
178
|
+
<p>Content that won't drag</p>
|
|
179
|
+
</div>
|
|
180
|
+
</zen-drawer>
|
|
181
|
+
`,
|
|
182
|
+
}),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export const ScaleBackground: Story = {
|
|
186
|
+
render: () => ({
|
|
187
|
+
props: {
|
|
188
|
+
openScale: signal(false),
|
|
189
|
+
},
|
|
190
|
+
template: `
|
|
191
|
+
<style>body.zen-drawer-open { transform: scale(0.98); transition: transform 0.3s ease; }</style>
|
|
192
|
+
<button zen-btn (click)="openScale.set(true)">Scale Background</button>
|
|
193
|
+
|
|
194
|
+
<zen-drawer [(open)]="openScale" side="bottom" [scaleBackground]="true">
|
|
195
|
+
<h2 drawer-header>Scale Drawer</h2>
|
|
196
|
+
<p>The page behind scales down slightly while this drawer is open.</p>
|
|
197
|
+
</zen-drawer>
|
|
198
|
+
`,
|
|
199
|
+
}),
|
|
200
|
+
};
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
computed,
|
|
4
|
+
DestroyRef,
|
|
5
|
+
effect,
|
|
6
|
+
ElementRef,
|
|
7
|
+
inject,
|
|
8
|
+
input,
|
|
9
|
+
model,
|
|
10
|
+
untracked,
|
|
11
|
+
viewChild,
|
|
12
|
+
} from '@angular/core';
|
|
13
|
+
|
|
14
|
+
import { DrawerSide, DrawerSize, GestureCallbacks } from './drawer.types';
|
|
15
|
+
import { DrawerGesture } from './drawer-gesture';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ZenDrawer is a reusable drawer component built on the native HTML `<dialog>` element.
|
|
19
|
+
* It slides in from one of the four sides (`left`, `right`, `top`, `bottom`) and can be
|
|
20
|
+
* dismissed by clicking the backdrop, pressing Escape, or dragging/swiping the panel.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```html
|
|
24
|
+
* <button zen-btn (click)="isOpen.set(true)">Open Drawer</button>
|
|
25
|
+
*
|
|
26
|
+
* <zen-drawer [(open)]="isOpen" side="right" size="md">
|
|
27
|
+
* <h2 drawer-header>Drawer Title</h2>
|
|
28
|
+
* <p>Drawer content</p>
|
|
29
|
+
* <div drawer-footer>
|
|
30
|
+
* <button zen-btn (click)="isOpen.set(false)" variant="filled" color="danger">Close</button>
|
|
31
|
+
* </div>
|
|
32
|
+
* </zen-drawer>
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* ### CSS Custom Properties
|
|
36
|
+
*
|
|
37
|
+
* You can customize the component using CSS custom properties:
|
|
38
|
+
* ```css
|
|
39
|
+
* :root {
|
|
40
|
+
* --zen-drawer-padding: 1rem;
|
|
41
|
+
* --zen-drawer-bg: #fff;
|
|
42
|
+
* --zen-drawer-leading-border-radius: 12px;
|
|
43
|
+
* --zen-drawer-shadow: 0 4px 24px rgb(0 0 0 / 20%);
|
|
44
|
+
* --zen-drawer-backdrop-bg: rgb(0 0 0 / 50%);
|
|
45
|
+
* --zen-drawer-overlay-min-opacity: 0;
|
|
46
|
+
* --zen-drawer-inset: 0px;
|
|
47
|
+
* --zen-drawer-drag: 0px;
|
|
48
|
+
* --zen-drawer-height: auto;
|
|
49
|
+
* --zen-size-inline-sm: clamp(16rem, 25vw, 24rem);
|
|
50
|
+
* --zen-size-inline-md: clamp(22rem, 40vw, 34rem);
|
|
51
|
+
* --zen-size-inline-lg: clamp(30rem, 55vw, 46rem);
|
|
52
|
+
* --zen-size-inline-xl: clamp(38rem, 70vw, 58rem);
|
|
53
|
+
* --zen-size-inline-full: calc(100% - var(--zen-drawer-inset) * 2);
|
|
54
|
+
* --zen-size-block-sm: clamp(14rem, 30dvh, 22rem);
|
|
55
|
+
* --zen-size-block-md: clamp(18rem, 45dvh, 30rem);
|
|
56
|
+
* --zen-size-block-lg: clamp(24rem, 60dvh, 38rem);
|
|
57
|
+
* --zen-size-block-xl: clamp(30rem, 75dvh, 46rem);
|
|
58
|
+
* --zen-size-block-full: calc(100% - var(--zen-drawer-inset) * 2);
|
|
59
|
+
* --zen-transition-duration: 0.3s;
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @author Konrad Stępień
|
|
64
|
+
* @license {@link https://github.com/kstepien3/ng-zen/blob/master/LICENSE|BSD-2-Clause}
|
|
65
|
+
* @see [GitHub](https://github.com/kstepien3/ng-zen)
|
|
66
|
+
* @see [MDN Dialog Element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog)
|
|
67
|
+
*/
|
|
68
|
+
@Component({
|
|
69
|
+
selector: 'zen-drawer',
|
|
70
|
+
standalone: true,
|
|
71
|
+
templateUrl: './drawer.html',
|
|
72
|
+
styleUrl: './drawer.scss',
|
|
73
|
+
host: {
|
|
74
|
+
'(click)': 'onBackdropClick($event)',
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
export class ZenDrawer {
|
|
78
|
+
/** Controls the open state of the drawer. Supports two-way binding via `[(open)]`. When `true`, calls `showModal()`; when `false`, animates out and calls `close()` on the native dialog. */
|
|
79
|
+
readonly open = model<boolean>(false);
|
|
80
|
+
/** Side from which the drawer slides in. Sets the slide direction, drag axis, and which dimension `size` controls (`width` for `left`/`right`, `height` for `top`/`bottom`). */
|
|
81
|
+
readonly side = input<DrawerSide>('right');
|
|
82
|
+
/** Size variant of the drawer. For `left`/`right` sets `width`; for `top`/`bottom` sets `height` via the `data-size` attribute. */
|
|
83
|
+
readonly size = input<DrawerSize>('md');
|
|
84
|
+
/** Whether pressing Escape closes the drawer (bound to the native dialog `cancel` event). */
|
|
85
|
+
readonly closeOnEscape = input(true);
|
|
86
|
+
/** Whether clicking the backdrop closes the drawer. When `false`, backdrop clicks are ignored. */
|
|
87
|
+
readonly backdrop = input(true);
|
|
88
|
+
/** Whether to render a grab handle at the leading edge of the drawer. Also the drag affordance when `handleOnly` is enabled. */
|
|
89
|
+
readonly swipeHandle = input(false);
|
|
90
|
+
/** When `true`, only the swipe handle can initiate a drag-to-dismiss gesture; clicks and drags on the content are ignored. */
|
|
91
|
+
readonly handleOnly = input(false);
|
|
92
|
+
/** When `true`, scales the page behind the drawer down while open (requires styling `body.zen-drawer-open` in the host app). */
|
|
93
|
+
readonly scaleBackground = input(false);
|
|
94
|
+
|
|
95
|
+
private readonly dialogRef = viewChild<ElementRef<HTMLDialogElement>>('drawerDialog');
|
|
96
|
+
private readonly gesture = new DrawerGesture();
|
|
97
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
98
|
+
|
|
99
|
+
private backdropMouseDown = false;
|
|
100
|
+
private wasDragging = false;
|
|
101
|
+
private isClosing = false;
|
|
102
|
+
private closeTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
103
|
+
|
|
104
|
+
protected readonly isVertical = computed(() => this.side() === 'top' || this.side() === 'bottom');
|
|
105
|
+
|
|
106
|
+
constructor() {
|
|
107
|
+
this.destroyRef.onDestroy(() => {
|
|
108
|
+
if (this.closeTimeout) {
|
|
109
|
+
clearTimeout(this.closeTimeout);
|
|
110
|
+
this.closeTimeout = null;
|
|
111
|
+
}
|
|
112
|
+
if (this.scaleBackground()) {
|
|
113
|
+
document.body.classList.remove('zen-drawer-open');
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
effect(() => {
|
|
118
|
+
const isOpen = this.open();
|
|
119
|
+
const dialog = this.dialogRef();
|
|
120
|
+
const shouldScale = this.scaleBackground();
|
|
121
|
+
|
|
122
|
+
untracked(() => {
|
|
123
|
+
if (!dialog) return;
|
|
124
|
+
|
|
125
|
+
const element = dialog.nativeElement;
|
|
126
|
+
|
|
127
|
+
if (isOpen) {
|
|
128
|
+
this.cancelClosing(element);
|
|
129
|
+
|
|
130
|
+
if (!element.open) {
|
|
131
|
+
element.showModal();
|
|
132
|
+
}
|
|
133
|
+
element.style.removeProperty('--zen-drawer-drag');
|
|
134
|
+
element.style.removeProperty('--zen-drawer-width');
|
|
135
|
+
|
|
136
|
+
if (shouldScale) {
|
|
137
|
+
document.body.classList.add('zen-drawer-open');
|
|
138
|
+
}
|
|
139
|
+
} else if (element.open && !this.isClosing) {
|
|
140
|
+
this.animateAndClose(element, shouldScale);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Programmatically close the drawer (sets `open` to `false`). */
|
|
147
|
+
close(): void {
|
|
148
|
+
this.open.set(false);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Handles the native `cancel` event (Escape). Always prevents the default close; closes only when `closeOnEscape` is `true`. */
|
|
152
|
+
protected cancel(event: Event): void {
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
if (this.closeOnEscape()) {
|
|
155
|
+
this.close();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Closes on backdrop click unless a drag just ended or `backdrop` is disabled. */
|
|
160
|
+
protected onBackdropClick(event: MouseEvent): void {
|
|
161
|
+
if (!this.backdrop()) return;
|
|
162
|
+
|
|
163
|
+
if (this.wasDragging) {
|
|
164
|
+
this.wasDragging = false;
|
|
165
|
+
this.backdropMouseDown = false;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!this.backdropMouseDown) return;
|
|
170
|
+
this.backdropMouseDown = false;
|
|
171
|
+
|
|
172
|
+
const dialog = this.dialogRef()?.nativeElement;
|
|
173
|
+
if (!dialog || event.target !== dialog) return;
|
|
174
|
+
|
|
175
|
+
if (this.isClickOutside(dialog, event)) {
|
|
176
|
+
this.close();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Registers a pointer as a potential drag source and records whether the press started on the backdrop. */
|
|
181
|
+
protected pointerDown(event: PointerEvent): void {
|
|
182
|
+
if (event.button !== 0) return;
|
|
183
|
+
|
|
184
|
+
this.wasDragging = false;
|
|
185
|
+
const dialog = this.dialogRef()?.nativeElement;
|
|
186
|
+
|
|
187
|
+
if (dialog && event.target === dialog) {
|
|
188
|
+
this.backdropMouseDown = this.isClickOutside(dialog, event);
|
|
189
|
+
} else {
|
|
190
|
+
this.backdropMouseDown = false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (this.handleOnly() && !(event.target as HTMLElement).closest('.zen-drawer-swipe-handle')) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.gesture.pointerDown(event, this.side(), this.gestureCallbacks());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Routes pointer movement to the gesture engine; marks a completed drag so the trailing click is swallowed. */
|
|
201
|
+
protected pointerMove(event: PointerEvent): void {
|
|
202
|
+
this.gesture.pointerMove(event, this.side(), this.gestureCallbacks());
|
|
203
|
+
if (this.gesture.isLocked) {
|
|
204
|
+
this.wasDragging = true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Ends the drag gesture and lets the engine decide whether to close or snap back. */
|
|
209
|
+
protected pointerUp(event: PointerEvent): void {
|
|
210
|
+
this.gesture.pointerUp(event.pointerId, this.side(), this.gestureCallbacks());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Cancels an in-progress drag gesture (e.g. lost pointer) and resets drag state. */
|
|
214
|
+
protected pointerCancel(event: PointerEvent): void {
|
|
215
|
+
this.wasDragging = false;
|
|
216
|
+
this.backdropMouseDown = false;
|
|
217
|
+
this.gesture.pointerCancel(event.pointerId, this.side(), this.gestureCallbacks());
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private animateAndClose(element: HTMLDialogElement, shouldScale: boolean): void {
|
|
221
|
+
this.isClosing = true;
|
|
222
|
+
|
|
223
|
+
if (shouldScale) {
|
|
224
|
+
document.body.classList.remove('zen-drawer-open');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
element.removeAttribute('data-swiping');
|
|
228
|
+
element.setAttribute('data-closing', 'true');
|
|
229
|
+
element.style.removeProperty('--zen-drawer-drag');
|
|
230
|
+
|
|
231
|
+
const handleTransitionEnd = (event: TransitionEvent): void => {
|
|
232
|
+
if (event.target === element && (event.propertyName === 'transform' || event.propertyName === 'opacity')) {
|
|
233
|
+
element.removeEventListener('transitionend', handleTransitionEnd);
|
|
234
|
+
this.finalizeClose(element);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
element.addEventListener('transitionend', handleTransitionEnd);
|
|
239
|
+
|
|
240
|
+
this.closeTimeout = setTimeout(() => {
|
|
241
|
+
element.removeEventListener('transitionend', handleTransitionEnd);
|
|
242
|
+
this.finalizeClose(element);
|
|
243
|
+
}, 350);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private finalizeClose(element: HTMLDialogElement): void {
|
|
247
|
+
if (this.closeTimeout) {
|
|
248
|
+
clearTimeout(this.closeTimeout);
|
|
249
|
+
this.closeTimeout = null;
|
|
250
|
+
}
|
|
251
|
+
element.removeAttribute('data-closing');
|
|
252
|
+
if (element.open) {
|
|
253
|
+
element.close();
|
|
254
|
+
}
|
|
255
|
+
this.isClosing = false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private cancelClosing(element: HTMLDialogElement): void {
|
|
259
|
+
if (this.closeTimeout) {
|
|
260
|
+
clearTimeout(this.closeTimeout);
|
|
261
|
+
this.closeTimeout = null;
|
|
262
|
+
}
|
|
263
|
+
this.isClosing = false;
|
|
264
|
+
element.removeAttribute('data-closing');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private isClickOutside(dialog: HTMLDialogElement, event: MouseEvent): boolean {
|
|
268
|
+
const rect = dialog.getBoundingClientRect();
|
|
269
|
+
return rect.width > 0 && rect.height > 0
|
|
270
|
+
? event.clientX < rect.left ||
|
|
271
|
+
event.clientX > rect.right ||
|
|
272
|
+
event.clientY < rect.top ||
|
|
273
|
+
event.clientY > rect.bottom
|
|
274
|
+
: true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private gestureCallbacks(): GestureCallbacks {
|
|
278
|
+
const el = this.dialogRef()!.nativeElement;
|
|
279
|
+
const vertical = this.isVertical();
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
onOpenChange: (open: boolean) => this.open.set(open),
|
|
283
|
+
setDragStyle: (cssVar: string, value: string) => el.style.setProperty(cssVar, value),
|
|
284
|
+
removeDragStyle: (cssVar: string) => el.style.removeProperty(cssVar),
|
|
285
|
+
getDimension: () => (vertical ? el.offsetHeight : el.offsetWidth),
|
|
286
|
+
setPointerCapture: (id: number) => el.setPointerCapture(id),
|
|
287
|
+
setAttribute: (name: string, value: string) => el.setAttribute(name, value),
|
|
288
|
+
removeAttribute: (name: string) => el.removeAttribute(name),
|
|
289
|
+
setOverscrollStyle: (sizePx: number) => {
|
|
290
|
+
const prop = vertical ? 'height' : 'width';
|
|
291
|
+
el.style.setProperty(prop, `${sizePx}px`);
|
|
292
|
+
},
|
|
293
|
+
removeOverscrollStyle: () => {
|
|
294
|
+
el.style.removeProperty('width');
|
|
295
|
+
el.style.removeProperty('height');
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type DrawerSide = 'left' | 'right' | 'top' | 'bottom';
|
|
2
|
+
export type DrawerSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
3
|
+
|
|
4
|
+
export interface GestureCallbacks {
|
|
5
|
+
readonly onOpenChange: (open: boolean) => void;
|
|
6
|
+
readonly setDragStyle: (cssVar: string, value: string) => void;
|
|
7
|
+
readonly removeDragStyle: (cssVar: string) => void;
|
|
8
|
+
readonly getDimension: () => number;
|
|
9
|
+
readonly setPointerCapture: (pointerId: number) => void;
|
|
10
|
+
readonly setAttribute: (name: string, value: string) => void;
|
|
11
|
+
readonly removeAttribute: (name: string) => void;
|
|
12
|
+
readonly setOverscrollStyle: (sizePx: number) => void;
|
|
13
|
+
readonly removeOverscrollStyle: () => void;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ZenDrawer } from './drawer';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../../../src/schematics/ui/schema.ts"],"names":[],"mappings":"","sourcesContent":["import { Path } from '@angular-devkit/core';\n\nexport interface GeneratorSchemaBase {\n currentDirectory: Path;\n path?: Path;\n stories: boolean;\n project?: string;\n}\n\nexport type UiType =\n | 'alert'\n | 'avatar'\n | 'badge'\n | 'button'\n | 'card'\n | 'checkbox'\n | 'dialog'\n | 'divider'\n | 'form-control'\n | 'icon'\n | 'input'\n | 'pin'\n | 'popover'\n | 'radio'\n | 'skeleton'\n | 'switch'\n | 'textarea';\n\nexport interface Schema extends GeneratorSchemaBase {\n ui: UiType[];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../../../src/schematics/ui/schema.ts"],"names":[],"mappings":"","sourcesContent":["import { Path } from '@angular-devkit/core';\n\nexport interface GeneratorSchemaBase {\n currentDirectory: Path;\n path?: Path;\n stories: boolean;\n project?: string;\n}\n\nexport type UiType =\n | 'alert'\n | 'avatar'\n | 'badge'\n | 'button'\n | 'card'\n | 'checkbox'\n | 'dialog'\n | 'divider'\n | 'drawer'\n | 'form-control'\n | 'icon'\n | 'input'\n | 'pin'\n | 'popover'\n | 'radio'\n | 'skeleton'\n | 'switch'\n | 'textarea';\n\nexport interface Schema extends GeneratorSchemaBase {\n ui: UiType[];\n}\n"]}
|