@magic-spells/dialog-panel 1.0.1 → 1.1.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 +27 -0
- package/dist/dialog-panel.css +84 -2
- package/dist/dialog-panel.min.css +1 -1
- package/package.json +2 -3
- package/src/dialog-panel.css +0 -117
- package/src/dialog-panel.js +0 -363
package/README.md
CHANGED
|
@@ -54,6 +54,33 @@ Or include directly in HTML:
|
|
|
54
54
|
</script>
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
## Positioning
|
|
58
|
+
|
|
59
|
+
By default, the panel is centered and uses a fade/scale animation. Set the
|
|
60
|
+
`position` attribute to slide the panel in from an edge instead:
|
|
61
|
+
|
|
62
|
+
```html
|
|
63
|
+
<dialog-panel position="bottom">...</dialog-panel>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Value | Animation | Typical use |
|
|
67
|
+
|-------|-----------|-------------|
|
|
68
|
+
| `center` (default) | Fade + scale | Standard modal |
|
|
69
|
+
| `top` | Slide down from top | Predictive search, notifications |
|
|
70
|
+
| `bottom` | Slide up from bottom | iOS-style action sheets |
|
|
71
|
+
| `left` | Slide in from left | Mobile nav menu |
|
|
72
|
+
| `right` | Slide in from right | Cart drawer, side panel |
|
|
73
|
+
|
|
74
|
+
Drawer panels default to full width (top/bottom, max-height `85vh`) or full
|
|
75
|
+
height (left/right, `min(24rem, 90vw)` wide). Override with your own CSS to
|
|
76
|
+
customize:
|
|
77
|
+
|
|
78
|
+
```css
|
|
79
|
+
dialog-panel[position='right'] > dialog {
|
|
80
|
+
width: 28rem;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
57
84
|
## How It Works
|
|
58
85
|
|
|
59
86
|
1. Wrap a native `<dialog>` element inside `<dialog-panel>`
|
package/dist/dialog-panel.css
CHANGED
|
@@ -13,13 +13,11 @@ dialog-panel {
|
|
|
13
13
|
/* Base dialog styles with transitions */
|
|
14
14
|
dialog-panel > dialog {
|
|
15
15
|
border: none;
|
|
16
|
-
border-radius: 0.5rem;
|
|
17
16
|
padding: 0;
|
|
18
17
|
max-width: 32rem;
|
|
19
18
|
width: 90vw;
|
|
20
19
|
max-height: 85vh;
|
|
21
20
|
overflow: visible;
|
|
22
|
-
background: white;
|
|
23
21
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
|
24
22
|
|
|
25
23
|
/* Start hidden */
|
|
@@ -115,3 +113,87 @@ dialog-panel[state='hiding'] > dialog-backdrop {
|
|
|
115
113
|
pointer-events: none;
|
|
116
114
|
transition: opacity 0.2s ease-in;
|
|
117
115
|
}
|
|
116
|
+
|
|
117
|
+
/* ============================================================
|
|
118
|
+
Position variants — drawer-style slide-in animations.
|
|
119
|
+
Default (no position attr, or position="center") uses the
|
|
120
|
+
centered fade/scale animation defined above.
|
|
121
|
+
============================================================ */
|
|
122
|
+
|
|
123
|
+
/* ---- Top drawer ---- */
|
|
124
|
+
dialog-panel[position='top'] > dialog {
|
|
125
|
+
margin: 0 auto auto auto;
|
|
126
|
+
width: 100%;
|
|
127
|
+
max-width: 100%;
|
|
128
|
+
max-height: 85vh;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
dialog-panel[position='top'][state='showing'] > dialog,
|
|
132
|
+
dialog-panel[position='top'][state='hiding'] > dialog {
|
|
133
|
+
opacity: 1;
|
|
134
|
+
transform: translateY(-100%);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
dialog-panel[position='top'][state='shown'] > dialog {
|
|
138
|
+
opacity: 1;
|
|
139
|
+
transform: translateY(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* ---- Bottom drawer ---- */
|
|
143
|
+
dialog-panel[position='bottom'] > dialog {
|
|
144
|
+
margin: auto auto 0 auto;
|
|
145
|
+
width: 100%;
|
|
146
|
+
max-width: 100%;
|
|
147
|
+
max-height: 85vh;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
dialog-panel[position='bottom'][state='showing'] > dialog,
|
|
151
|
+
dialog-panel[position='bottom'][state='hiding'] > dialog {
|
|
152
|
+
opacity: 1;
|
|
153
|
+
transform: translateY(100%);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
dialog-panel[position='bottom'][state='shown'] > dialog {
|
|
157
|
+
opacity: 1;
|
|
158
|
+
transform: translateY(0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* ---- Left drawer ---- */
|
|
162
|
+
dialog-panel[position='left'] > dialog {
|
|
163
|
+
margin: auto auto auto 0;
|
|
164
|
+
height: 100dvh;
|
|
165
|
+
max-height: 100dvh;
|
|
166
|
+
width: min(24rem, 90vw);
|
|
167
|
+
max-width: 100%;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
dialog-panel[position='left'][state='showing'] > dialog,
|
|
171
|
+
dialog-panel[position='left'][state='hiding'] > dialog {
|
|
172
|
+
opacity: 1;
|
|
173
|
+
transform: translateX(-100%);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
dialog-panel[position='left'][state='shown'] > dialog {
|
|
177
|
+
opacity: 1;
|
|
178
|
+
transform: translateX(0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* ---- Right drawer ---- */
|
|
182
|
+
dialog-panel[position='right'] > dialog {
|
|
183
|
+
margin: auto 0 auto auto;
|
|
184
|
+
height: 100dvh;
|
|
185
|
+
max-height: 100dvh;
|
|
186
|
+
width: min(24rem, 90vw);
|
|
187
|
+
max-width: 100%;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
dialog-panel[position='right'][state='showing'] > dialog,
|
|
191
|
+
dialog-panel[position='right'][state='hiding'] > dialog {
|
|
192
|
+
opacity: 1;
|
|
193
|
+
transform: translateX(100%);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
dialog-panel[position='right'][state='shown'] > dialog {
|
|
197
|
+
opacity: 1;
|
|
198
|
+
transform: translateX(0);
|
|
199
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
dialog-panel{display:contents}dialog-panel>dialog{
|
|
1
|
+
dialog-panel{display:contents}dialog-panel>dialog{border:none;box-shadow:0 10px 25px rgba(0,0,0,.15);max-height:85vh;max-width:32rem;opacity:0;overflow:visible;padding:0;transform:scale(.95);transition:opacity .3s ease-out,transform .3s ease-out;width:90vw}dialog-panel:has(dialog-backdrop)>dialog::backdrop{background:transparent}dialog-panel:not(:has(dialog-backdrop))>dialog::backdrop{background:rgba(0,0,0,.3);opacity:0;transition:opacity .3s ease-out}dialog-backdrop{background:rgba(0,0,0,.3);height:200dvh;left:50%;opacity:0;pointer-events:none;position:fixed;top:50%;transform:translateY(-50%) translateX(-50%);transition:opacity .3s ease-out;width:200vw;z-index:9999999}dialog-panel[state=showing]>dialog{opacity:0;transform:scale(.95)}dialog-panel:not(:has(dialog-backdrop))[state=showing]>dialog::backdrop{opacity:0}dialog-panel[state=shown]>dialog{opacity:1;transform:scale(1)}dialog-panel:not(:has(dialog-backdrop))[state=shown]>dialog::backdrop{opacity:1}dialog-panel[state=hiding]>dialog{opacity:0;transform:scale(.95);transition:opacity .2s ease-in,transform .2s ease-in}dialog-panel:not(:has(dialog-backdrop))[state=hiding]>dialog::backdrop{opacity:0;transition:opacity .2s ease-in}dialog-panel[state=hidden]>dialog-backdrop,dialog-panel[state=showing]>dialog-backdrop{opacity:0;pointer-events:none}dialog-panel[state=shown]>dialog-backdrop{opacity:1;pointer-events:auto}dialog-panel[state=hiding]>dialog-backdrop{opacity:0;pointer-events:none;transition:opacity .2s ease-in}dialog-panel[position=top]>dialog{margin:0 auto auto;max-height:85vh;max-width:100%;width:100%}dialog-panel[position=top][state=hiding]>dialog,dialog-panel[position=top][state=showing]>dialog{opacity:1;transform:translateY(-100%)}dialog-panel[position=top][state=shown]>dialog{opacity:1;transform:translateY(0)}dialog-panel[position=bottom]>dialog{margin:auto auto 0;max-height:85vh;max-width:100%;width:100%}dialog-panel[position=bottom][state=hiding]>dialog,dialog-panel[position=bottom][state=showing]>dialog{opacity:1;transform:translateY(100%)}dialog-panel[position=bottom][state=shown]>dialog{opacity:1;transform:translateY(0)}dialog-panel[position=left]>dialog{height:100dvh;margin:auto auto auto 0;max-height:100dvh;max-width:100%;width:min(24rem,90vw)}dialog-panel[position=left][state=hiding]>dialog,dialog-panel[position=left][state=showing]>dialog{opacity:1;transform:translateX(-100%)}dialog-panel[position=left][state=shown]>dialog{opacity:1;transform:translateX(0)}dialog-panel[position=right]>dialog{height:100dvh;margin:auto 0 auto auto;max-height:100dvh;max-width:100%;width:min(24rem,90vw)}dialog-panel[position=right][state=hiding]>dialog,dialog-panel[position=right][state=showing]>dialog{opacity:1;transform:translateX(100%)}dialog-panel[position=right][state=shown]>dialog{opacity:1;transform:translateX(0)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magic-spells/dialog-panel",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A lightweight web component wrapper for native <dialog> elements with state-driven animations.",
|
|
5
5
|
"author": "Cory Schulz",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,8 +37,7 @@
|
|
|
37
37
|
"custom-elements"
|
|
38
38
|
],
|
|
39
39
|
"files": [
|
|
40
|
-
"dist/"
|
|
41
|
-
"src/"
|
|
40
|
+
"dist/"
|
|
42
41
|
],
|
|
43
42
|
"scripts": {
|
|
44
43
|
"build": "rollup -c",
|
package/src/dialog-panel.css
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dialog-panel basic centered modal styles
|
|
3
|
-
*
|
|
4
|
-
* This provides a simple fade in/out animation for a centered modal dialog.
|
|
5
|
-
* Developers can customize or replace these styles to match their design.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/* Wrapper doesn't affect layout */
|
|
9
|
-
dialog-panel {
|
|
10
|
-
display: contents;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/* Base dialog styles with transitions */
|
|
14
|
-
dialog-panel > dialog {
|
|
15
|
-
border: none;
|
|
16
|
-
border-radius: 0.5rem;
|
|
17
|
-
padding: 0;
|
|
18
|
-
max-width: 32rem;
|
|
19
|
-
width: 90vw;
|
|
20
|
-
max-height: 85vh;
|
|
21
|
-
overflow: visible;
|
|
22
|
-
background: white;
|
|
23
|
-
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
|
24
|
-
|
|
25
|
-
/* Start hidden */
|
|
26
|
-
opacity: 0;
|
|
27
|
-
transform: scale(0.95);
|
|
28
|
-
|
|
29
|
-
/* Transition always defined on base */
|
|
30
|
-
transition:
|
|
31
|
-
opacity 0.3s ease-out,
|
|
32
|
-
transform 0.3s ease-out;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/* When using dialog-backdrop element, make native ::backdrop transparent
|
|
36
|
-
but keep it clickable (clicks detected via bounding rect check) */
|
|
37
|
-
dialog-panel:has(dialog-backdrop) > dialog::backdrop {
|
|
38
|
-
background: transparent;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/* Native backdrop (when not using dialog-backdrop) */
|
|
42
|
-
dialog-panel:not(:has(dialog-backdrop)) > dialog::backdrop {
|
|
43
|
-
background: rgba(0, 0, 0, 0.3);
|
|
44
|
-
opacity: 0;
|
|
45
|
-
transition: opacity 0.3s ease-out;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/* Custom dialog-backdrop element */
|
|
49
|
-
dialog-backdrop {
|
|
50
|
-
position: fixed;
|
|
51
|
-
background: rgba(0, 0, 0, 0.3);
|
|
52
|
-
opacity: 0;
|
|
53
|
-
transition: opacity 0.3s ease-out;
|
|
54
|
-
pointer-events: none;
|
|
55
|
-
z-index: 9999999;
|
|
56
|
-
top: 50%;
|
|
57
|
-
left: 50%;
|
|
58
|
-
width: 200vw;
|
|
59
|
-
height: 200dvh;
|
|
60
|
-
transform: translateY(-50%) translateX(-50%);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/* Showing state - dialog is open but still at initial values
|
|
64
|
-
(this is the starting point for the fade-in animation) */
|
|
65
|
-
dialog-panel[state='showing'] > dialog {
|
|
66
|
-
opacity: 0;
|
|
67
|
-
transform: scale(0.95);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
dialog-panel:not(:has(dialog-backdrop))[state='showing']
|
|
71
|
-
> dialog::backdrop {
|
|
72
|
-
opacity: 0;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/* Shown state - fully visible (animates from showing → shown) */
|
|
76
|
-
dialog-panel[state='shown'] > dialog {
|
|
77
|
-
opacity: 1;
|
|
78
|
-
transform: scale(1);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
dialog-panel:not(:has(dialog-backdrop))[state='shown']
|
|
82
|
-
> dialog::backdrop {
|
|
83
|
-
opacity: 1;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/* Hiding state - faster exit animation */
|
|
87
|
-
dialog-panel[state='hiding'] > dialog {
|
|
88
|
-
opacity: 0;
|
|
89
|
-
transform: scale(0.95);
|
|
90
|
-
transition:
|
|
91
|
-
opacity 0.2s ease-in,
|
|
92
|
-
transform 0.2s ease-in;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
dialog-panel:not(:has(dialog-backdrop))[state='hiding']
|
|
96
|
-
> dialog::backdrop {
|
|
97
|
-
opacity: 0;
|
|
98
|
-
transition: opacity 0.2s ease-in;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/* dialog-backdrop state animations */
|
|
102
|
-
dialog-panel[state='showing'] > dialog-backdrop,
|
|
103
|
-
dialog-panel[state='hidden'] > dialog-backdrop {
|
|
104
|
-
opacity: 0;
|
|
105
|
-
pointer-events: none;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
dialog-panel[state='shown'] > dialog-backdrop {
|
|
109
|
-
opacity: 1;
|
|
110
|
-
pointer-events: auto;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
dialog-panel[state='hiding'] > dialog-backdrop {
|
|
114
|
-
opacity: 0;
|
|
115
|
-
pointer-events: none;
|
|
116
|
-
transition: opacity 0.2s ease-in;
|
|
117
|
-
}
|
package/src/dialog-panel.js
DELETED
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import './dialog-panel.css';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* DialogPanel - A lightweight web component wrapper for native <dialog> elements
|
|
5
|
-
* with state-driven animations.
|
|
6
|
-
*
|
|
7
|
-
* @extends HTMLElement
|
|
8
|
-
*
|
|
9
|
-
* @property {string} state - Current state: 'hidden' | 'showing' | 'shown' | 'hiding'
|
|
10
|
-
* @property {HTMLDialogElement} dialog - Reference to inner <dialog> element
|
|
11
|
-
* @property {boolean} isOpen - True if state is 'showing' or 'shown'
|
|
12
|
-
* @property {HTMLElement|null} triggerElement - Element that triggered current action
|
|
13
|
-
*
|
|
14
|
-
* @fires beforeShow - Fired before showing starts (cancelable)
|
|
15
|
-
* @fires shown - Fired after show animation completes
|
|
16
|
-
* @fires beforeHide - Fired before hiding starts (cancelable)
|
|
17
|
-
* @fires hidden - Fired after hide animation completes
|
|
18
|
-
*/
|
|
19
|
-
class DialogPanel extends HTMLElement {
|
|
20
|
-
// Private fields
|
|
21
|
-
#state = 'hidden';
|
|
22
|
-
#triggerElement = null;
|
|
23
|
-
#dialog = null;
|
|
24
|
-
#result = null;
|
|
25
|
-
|
|
26
|
-
// Event handler references for cleanup
|
|
27
|
-
#handlers = {
|
|
28
|
-
click: null,
|
|
29
|
-
dialogClick: null,
|
|
30
|
-
cancel: null,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// Animation cleanup references
|
|
34
|
-
#pendingRAF = null;
|
|
35
|
-
#pendingTimeout = null;
|
|
36
|
-
|
|
37
|
-
// Fallback timeout for transitionend (in ms)
|
|
38
|
-
static TRANSITION_FALLBACK_TIMEOUT = 700;
|
|
39
|
-
|
|
40
|
-
connectedCallback() {
|
|
41
|
-
const _ = this;
|
|
42
|
-
|
|
43
|
-
// Find inner dialog element
|
|
44
|
-
_.#dialog = _.querySelector('dialog');
|
|
45
|
-
|
|
46
|
-
if (!_.#dialog) {
|
|
47
|
-
console.warn(
|
|
48
|
-
'DialogPanel: No <dialog> element found inside <dialog-panel>'
|
|
49
|
-
);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Auto-create dialog-backdrop if not present
|
|
54
|
-
if (!_.querySelector('dialog-backdrop')) {
|
|
55
|
-
const backdrop = document.createElement('dialog-backdrop');
|
|
56
|
-
_.insertBefore(backdrop, _.firstChild);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Set initial state attribute
|
|
60
|
-
_.#setState('hidden');
|
|
61
|
-
|
|
62
|
-
// Bind event handlers
|
|
63
|
-
_.#bindEvents();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
disconnectedCallback() {
|
|
67
|
-
const _ = this;
|
|
68
|
-
|
|
69
|
-
// Cancel pending animations
|
|
70
|
-
if (_.#pendingRAF) {
|
|
71
|
-
cancelAnimationFrame(_.#pendingRAF);
|
|
72
|
-
_.#pendingRAF = null;
|
|
73
|
-
}
|
|
74
|
-
if (_.#pendingTimeout) {
|
|
75
|
-
clearTimeout(_.#pendingTimeout);
|
|
76
|
-
_.#pendingTimeout = null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Clean up event listeners
|
|
80
|
-
if (_.#handlers.click) {
|
|
81
|
-
_.removeEventListener('click', _.#handlers.click);
|
|
82
|
-
}
|
|
83
|
-
if (_.#dialog) {
|
|
84
|
-
if (_.#handlers.dialogClick) {
|
|
85
|
-
_.#dialog.removeEventListener(
|
|
86
|
-
'click',
|
|
87
|
-
_.#handlers.dialogClick
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
if (_.#handlers.cancel) {
|
|
91
|
-
_.#dialog.removeEventListener('cancel', _.#handlers.cancel);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Bind event listeners for close buttons, backdrop, and escape key
|
|
98
|
-
* @private
|
|
99
|
-
*/
|
|
100
|
-
#bindEvents() {
|
|
101
|
-
const _ = this;
|
|
102
|
-
|
|
103
|
-
// Handle close buttons with data-action-hide-dialog
|
|
104
|
-
_.#handlers.click = (e) => {
|
|
105
|
-
const trigger = e.target.closest('[data-action-hide-dialog]');
|
|
106
|
-
if (trigger) {
|
|
107
|
-
e.stopPropagation();
|
|
108
|
-
_.hide(trigger);
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
_.addEventListener('click', _.#handlers.click);
|
|
112
|
-
|
|
113
|
-
// Handle backdrop click - detect clicks outside dialog bounds
|
|
114
|
-
// This works because clicks on ::backdrop still fire on the dialog element
|
|
115
|
-
_.#handlers.dialogClick = (e) => {
|
|
116
|
-
const rect = _.#dialog.getBoundingClientRect();
|
|
117
|
-
const clickedOutside =
|
|
118
|
-
e.clientX < rect.left ||
|
|
119
|
-
e.clientX > rect.right ||
|
|
120
|
-
e.clientY < rect.top ||
|
|
121
|
-
e.clientY > rect.bottom;
|
|
122
|
-
if (clickedOutside) {
|
|
123
|
-
e.stopPropagation();
|
|
124
|
-
_.hide();
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
_.#dialog.addEventListener('click', _.#handlers.dialogClick);
|
|
128
|
-
|
|
129
|
-
// Handle escape key - intercept native cancel and animate close
|
|
130
|
-
_.#handlers.cancel = (e) => {
|
|
131
|
-
e.preventDefault();
|
|
132
|
-
e.stopPropagation();
|
|
133
|
-
_.hide();
|
|
134
|
-
};
|
|
135
|
-
_.#dialog.addEventListener('cancel', _.#handlers.cancel);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Show the dialog with animation
|
|
140
|
-
* @param {HTMLElement} [triggerEl=null] - The element that triggered the show
|
|
141
|
-
* @returns {boolean} False if show was prevented via beforeShow event
|
|
142
|
-
*/
|
|
143
|
-
show(triggerEl = null) {
|
|
144
|
-
const _ = this;
|
|
145
|
-
|
|
146
|
-
// Check if already showing/shown
|
|
147
|
-
if (_.#state === 'showing' || _.#state === 'shown') return true;
|
|
148
|
-
|
|
149
|
-
// If currently hiding, ignore (let hide complete first)
|
|
150
|
-
if (_.#state === 'hiding') return false;
|
|
151
|
-
|
|
152
|
-
// Store trigger element for focus return later
|
|
153
|
-
_.#triggerElement = triggerEl || null;
|
|
154
|
-
|
|
155
|
-
// Fire beforeShow (cancelable)
|
|
156
|
-
if (!_.#emit('beforeShow', { cancelable: true })) return false;
|
|
157
|
-
|
|
158
|
-
// Set state to 'showing' and open native dialog
|
|
159
|
-
_.#setState('showing');
|
|
160
|
-
_.#dialog.showModal();
|
|
161
|
-
|
|
162
|
-
// Double RAF ensures browser has painted the 'showing' state
|
|
163
|
-
// before we transition to 'shown'
|
|
164
|
-
_.#pendingRAF = requestAnimationFrame(() => {
|
|
165
|
-
_.#pendingRAF = requestAnimationFrame(() => {
|
|
166
|
-
_.#pendingRAF = null;
|
|
167
|
-
_.#setState('shown');
|
|
168
|
-
|
|
169
|
-
// Wait for transition to complete before firing 'shown' event
|
|
170
|
-
_.#waitForTransition(() => {
|
|
171
|
-
_.#emit('shown');
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Hide the dialog with animation
|
|
181
|
-
* @param {HTMLElement} [triggerEl=null] - The element that triggered the hide
|
|
182
|
-
* @returns {boolean} False if hide was prevented via beforeHide event
|
|
183
|
-
*/
|
|
184
|
-
hide(triggerEl = null) {
|
|
185
|
-
const _ = this;
|
|
186
|
-
|
|
187
|
-
// Check if already hiding/hidden
|
|
188
|
-
if (_.#state === 'hiding' || _.#state === 'hidden') return true;
|
|
189
|
-
|
|
190
|
-
// If currently showing, ignore (let show complete first)
|
|
191
|
-
if (_.#state === 'showing') return false;
|
|
192
|
-
|
|
193
|
-
// Capture result from trigger element
|
|
194
|
-
_.#result = triggerEl?.dataset?.result ?? null;
|
|
195
|
-
|
|
196
|
-
// Fire beforeHide (cancelable)
|
|
197
|
-
if (
|
|
198
|
-
!_.#emit('beforeHide', {
|
|
199
|
-
cancelable: true,
|
|
200
|
-
result: _.#result,
|
|
201
|
-
triggerElement: triggerEl,
|
|
202
|
-
})
|
|
203
|
-
) {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Set state to 'hiding' - this triggers CSS exit animation
|
|
208
|
-
_.#setState('hiding');
|
|
209
|
-
|
|
210
|
-
// Wait for transition to complete
|
|
211
|
-
_.#waitForTransition(() => {
|
|
212
|
-
_.#dialog.close();
|
|
213
|
-
_.#setState('hidden');
|
|
214
|
-
_.#emit('hidden', {
|
|
215
|
-
result: _.#result,
|
|
216
|
-
triggerElement: triggerEl,
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Return focus to trigger element
|
|
220
|
-
if (_.#triggerElement) _.#triggerElement.focus();
|
|
221
|
-
|
|
222
|
-
// Clean up
|
|
223
|
-
_.#triggerElement = null;
|
|
224
|
-
_.#result = null;
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
return true;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Wait for CSS transition to complete with fallback timeout
|
|
232
|
-
* @param {Function} callback - Called when transition completes
|
|
233
|
-
* @private
|
|
234
|
-
*/
|
|
235
|
-
#waitForTransition(callback) {
|
|
236
|
-
const _ = this;
|
|
237
|
-
let called = false;
|
|
238
|
-
|
|
239
|
-
const done = () => {
|
|
240
|
-
if (called) return;
|
|
241
|
-
called = true;
|
|
242
|
-
_.#dialog.removeEventListener('transitionend', onTransitionEnd);
|
|
243
|
-
if (_.#pendingTimeout) {
|
|
244
|
-
clearTimeout(_.#pendingTimeout);
|
|
245
|
-
_.#pendingTimeout = null;
|
|
246
|
-
}
|
|
247
|
-
callback();
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
const onTransitionEnd = (e) => {
|
|
251
|
-
if (e.target === _.#dialog) done();
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
_.#dialog.addEventListener('transitionend', onTransitionEnd);
|
|
255
|
-
|
|
256
|
-
_.#pendingTimeout = setTimeout(
|
|
257
|
-
done,
|
|
258
|
-
DialogPanel.TRANSITION_FALLBACK_TIMEOUT
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Set the component state
|
|
264
|
-
* @param {string} newState - The new state
|
|
265
|
-
* @private
|
|
266
|
-
*/
|
|
267
|
-
#setState(newState) {
|
|
268
|
-
this.#state = newState;
|
|
269
|
-
this.setAttribute('state', newState);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Emit a custom event
|
|
274
|
-
* @param {string} name - Event name
|
|
275
|
-
* @param {Object} options - Event options
|
|
276
|
-
* @returns {boolean} False if event was cancelled
|
|
277
|
-
* @private
|
|
278
|
-
*/
|
|
279
|
-
#emit(name, options = {}) {
|
|
280
|
-
const _ = this;
|
|
281
|
-
const { cancelable = false, ...detail } = options;
|
|
282
|
-
|
|
283
|
-
const event = new CustomEvent(name, {
|
|
284
|
-
bubbles: true,
|
|
285
|
-
composed: true,
|
|
286
|
-
cancelable,
|
|
287
|
-
detail: {
|
|
288
|
-
triggerElement: _.#triggerElement,
|
|
289
|
-
result: _.#result,
|
|
290
|
-
state: _.#state,
|
|
291
|
-
...detail,
|
|
292
|
-
},
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
return _.dispatchEvent(event);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Read-only properties
|
|
299
|
-
get state() {
|
|
300
|
-
return this.#state;
|
|
301
|
-
}
|
|
302
|
-
get dialog() {
|
|
303
|
-
return this.#dialog;
|
|
304
|
-
}
|
|
305
|
-
get isOpen() {
|
|
306
|
-
return this.#state === 'showing' || this.#state === 'shown';
|
|
307
|
-
}
|
|
308
|
-
get triggerElement() {
|
|
309
|
-
return this.#triggerElement;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* DialogBackdrop - A custom backdrop element that animates with the dialog-panel state.
|
|
315
|
-
* Use this instead of native ::backdrop for consistent cross-browser animations.
|
|
316
|
-
*
|
|
317
|
-
* @extends HTMLElement
|
|
318
|
-
*/
|
|
319
|
-
class DialogBackdrop extends HTMLElement {
|
|
320
|
-
#panel = null;
|
|
321
|
-
#handlers = {
|
|
322
|
-
click: null,
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
connectedCallback() {
|
|
326
|
-
const _ = this;
|
|
327
|
-
|
|
328
|
-
// Find parent dialog-panel
|
|
329
|
-
_.#panel = _.closest('dialog-panel');
|
|
330
|
-
|
|
331
|
-
if (!_.#panel) {
|
|
332
|
-
console.warn(
|
|
333
|
-
'DialogBackdrop: Must be inside a <dialog-panel> element'
|
|
334
|
-
);
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Handle click to close
|
|
339
|
-
_.#handlers.click = () => {
|
|
340
|
-
_.#panel.hide();
|
|
341
|
-
};
|
|
342
|
-
_.addEventListener('click', _.#handlers.click);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
disconnectedCallback() {
|
|
346
|
-
const _ = this;
|
|
347
|
-
if (_.#handlers.click) {
|
|
348
|
-
_.removeEventListener('click', _.#handlers.click);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Register the custom elements
|
|
354
|
-
// Note: dialog-backdrop must be defined BEFORE dialog-panel,
|
|
355
|
-
// because dialog-panel's connectedCallback may create dialog-backdrop elements
|
|
356
|
-
if (!customElements.get('dialog-backdrop')) {
|
|
357
|
-
customElements.define('dialog-backdrop', DialogBackdrop);
|
|
358
|
-
}
|
|
359
|
-
if (!customElements.get('dialog-panel')) {
|
|
360
|
-
customElements.define('dialog-panel', DialogPanel);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
export { DialogPanel, DialogBackdrop };
|