@magic-spells/dialog-panel 0.1.0 → 0.2.2

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.
@@ -0,0 +1,73 @@
1
+ // Import variables using the modern @use rule
2
+ @use 'variables' as vars;
3
+
4
+ dialog-panel {
5
+ /* Make it take no space and be invisible in the document flow */
6
+ display: contents;
7
+
8
+ &[aria-hidden='false'] {
9
+ dialog-overlay,
10
+ dialog-content {
11
+ pointer-events: auto;
12
+ opacity: 1;
13
+ transform: scale(1);
14
+ filter: blur(0px);
15
+ }
16
+ }
17
+ }
18
+
19
+ /* Overlay background */
20
+ dialog-overlay {
21
+ position: fixed;
22
+ top: 0;
23
+ left: 0;
24
+ width: 100vw;
25
+ height: 100vh;
26
+ opacity: 0;
27
+ pointer-events: none;
28
+ z-index: var(--dp-overlay-z-index, 1000);
29
+ transition: var(--dp-overlay-transition, all 300ms ease-out);
30
+ background-color: var(
31
+ --dp-overlay-background,
32
+ rgba(20, 23, 26, 0.4)
33
+ );
34
+ backdrop-filter: var(
35
+ --dp-overlay-backdrop-filter,
36
+ blur(2px) saturate(120%)
37
+ );
38
+ }
39
+
40
+ dialog-content {
41
+ position: fixed;
42
+ top: 50%;
43
+ left: 50%;
44
+ transform: translate(-50%, -50%) scale(0.95);
45
+ max-width: 90vw;
46
+ max-height: 85vh;
47
+ display: var(--dp-content-display, block);
48
+ opacity: 0;
49
+ background: var(--dp-content-background, white);
50
+ pointer-events: none;
51
+ z-index: var(--dp-content-z-index, 1001);
52
+ box-shadow: var(
53
+ --dp-content-shadow,
54
+ 0 10px 25px rgba(0, 0, 0, 0.15)
55
+ );
56
+ border-radius: var(--dp-content-border-radius, 8px);
57
+ overflow: auto;
58
+ transition:
59
+ opacity var(--dp-transition-duration, 300ms)
60
+ var(--dp-transition-timing, ease-out),
61
+ transform var(--dp-transition-duration, 300ms)
62
+ var(--dp-transition-timing, ease-out);
63
+
64
+ /* When shown, reset transform to center */
65
+ dialog-panel[aria-hidden='false'] & {
66
+ transform: translate(-50%, -50%) scale(1);
67
+ }
68
+
69
+ /* When explicitly hidden, remove from layout */
70
+ &.hidden {
71
+ display: none;
72
+ }
73
+ }
@@ -0,0 +1,54 @@
1
+ // Dialog Panel variables
2
+ // SCSS variables for internal component use and customization
3
+ // These variables can be customized by the user
4
+
5
+ // Layout
6
+ $panel-top: 0 !default;
7
+ $panel-left: 0 !default;
8
+ $panel-width: 100vw !default;
9
+ $panel-height: 100vh !default;
10
+ $panel-z-index: 10 !default;
11
+
12
+ // Overlay
13
+ $overlay-z-index: 1000 !default;
14
+ $overlay-background: rgba(20, 23, 26, 0.4) !default;
15
+ $overlay-backdrop-filter: blur(2px) saturate(120%) !default;
16
+ $overlay-transition: all 400ms ease-out !default;
17
+
18
+ // Content
19
+ $content-display: block !default;
20
+ $content-background: white !default;
21
+ $content-z-index: 1001 !default;
22
+ $content-shadow: 0 10px 25px rgba(0, 0, 0, 0.15) !default;
23
+ $content-border-radius: 8px !default;
24
+
25
+ // Animation
26
+ $transition-duration: 400ms !default;
27
+ $transition-timing: ease-out !default;
28
+
29
+ // Define CSS Custom Properties using SCSS values
30
+ :root {
31
+ // Layout
32
+ --dp-panel-top: #{$panel-top};
33
+ --dp-panel-left: #{$panel-left};
34
+ --dp-panel-width: #{$panel-width};
35
+ --dp-panel-height: #{$panel-height};
36
+ --dp-panel-z-index: #{$panel-z-index};
37
+
38
+ // Overlay
39
+ --dp-overlay-z-index: #{$overlay-z-index};
40
+ --dp-overlay-background: #{$overlay-background};
41
+ --dp-overlay-backdrop-filter: #{$overlay-backdrop-filter};
42
+ --dp-overlay-transition: #{$overlay-transition};
43
+
44
+ // Content
45
+ --dp-content-display: #{$content-display};
46
+ --dp-content-background: #{$content-background};
47
+ --dp-content-z-index: #{$content-z-index};
48
+ --dp-content-shadow: #{$content-shadow};
49
+ --dp-content-border-radius: #{$content-border-radius};
50
+
51
+ // Animation
52
+ --dp-transition-duration: #{$transition-duration};
53
+ --dp-transition-timing: #{$transition-timing};
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-spells/dialog-panel",
3
- "version": "0.1.0",
3
+ "version": "0.2.2",
4
4
  "description": "A lightweight, customizable Dialog Panel web component for creating accessible and responsive modal dialogs.",
5
5
  "author": "Cory Schulz",
6
6
  "license": "MIT",
@@ -9,13 +9,17 @@
9
9
  "module": "dist/dialog-panel.esm.js",
10
10
  "unpkg": "dist/dialog-panel.min.js",
11
11
  "style": "dist/dialog-panel.min.css",
12
+ "sass": "dist/dialog-panel.scss",
12
13
  "exports": {
13
14
  ".": {
14
15
  "import": "./dist/dialog-panel.esm.js",
15
16
  "require": "./dist/dialog-panel.cjs.js",
16
17
  "default": "./dist/dialog-panel.esm.js"
17
18
  },
18
- "./style.css": "./dist/dialog-panel.min.css"
19
+ "./css": "./dist/dialog-panel.css",
20
+ "./css/min": "./dist/dialog-panel.min.css",
21
+ "./scss": "./dist/dialog-panel.scss",
22
+ "./scss/*": "./dist/scss/*"
19
23
  },
20
24
  "sideEffects": true,
21
25
  "repository": {
@@ -46,7 +50,8 @@
46
50
  "lint": "eslint src/ rollup.config.mjs",
47
51
  "format": "prettier --write .",
48
52
  "prepublishOnly": "npm run build",
49
- "serve": "rollup -c --watch"
53
+ "serve": "rollup -c --watch",
54
+ "dev": "rollup -c --watch"
50
55
  },
51
56
  "publishConfig": {
52
57
  "access": "public",
@@ -67,7 +72,8 @@
67
72
  "rollup": "^3.0.0",
68
73
  "rollup-plugin-copy": "^3.5.0",
69
74
  "rollup-plugin-postcss": "^4.0.2",
70
- "rollup-plugin-serve": "^1.1.1"
75
+ "rollup-plugin-serve": "^1.1.1",
76
+ "sass": "^1.86.3"
71
77
  },
72
78
  "dependencies": {
73
79
  "@magic-spells/focus-trap": "^1.0.6"
@@ -1,4 +1,4 @@
1
- import './dialog-panel.css';
1
+ import './index.scss';
2
2
  import '@magic-spells/focus-trap';
3
3
 
4
4
  /**
@@ -6,6 +6,53 @@ import '@magic-spells/focus-trap';
6
6
  * @extends HTMLElement
7
7
  */
8
8
  class DialogPanel extends HTMLElement {
9
+ #handleTransitionEnd;
10
+ #scrollPosition = 0;
11
+
12
+ /**
13
+ * Clean up event listeners when component is removed from DOM
14
+ */
15
+ disconnectedCallback() {
16
+ const _ = this;
17
+ if (_.contentPanel) {
18
+ _.contentPanel.removeEventListener(
19
+ 'transitionend',
20
+ _.#handleTransitionEnd
21
+ );
22
+ }
23
+
24
+ // Ensure body scroll is restored if component is removed while open
25
+ document.body.classList.remove('overflow-hidden');
26
+ this.#restoreScroll();
27
+ }
28
+
29
+ /**
30
+ * Saves current scroll position and locks body scrolling
31
+ * @private
32
+ */
33
+ #lockScroll() {
34
+ const _ = this;
35
+ // Save current scroll position
36
+ _.#scrollPosition = window.pageYOffset;
37
+
38
+ // Apply fixed position to body
39
+ document.body.classList.add('overflow-hidden');
40
+ document.body.style.top = `-${_.#scrollPosition}px`;
41
+ }
42
+
43
+ /**
44
+ * Restores scroll position when dialog is closed
45
+ * @private
46
+ */
47
+ #restoreScroll() {
48
+ const _ = this;
49
+ // Remove fixed positioning
50
+ document.body.classList.remove('overflow-hidden');
51
+ document.body.style.removeProperty('top');
52
+
53
+ // Restore scroll position
54
+ window.scrollTo(0, _.#scrollPosition);
55
+ }
9
56
  /**
10
57
  * Initializes the dialog panel, sets up focus trap and overlay
11
58
  */
@@ -21,6 +68,24 @@ class DialogPanel extends HTMLElement {
21
68
  _.focusTrap = document.createElement('focus-trap');
22
69
  _.triggerEl = null;
23
70
 
71
+ // Create a handler for transition end events
72
+ _.#handleTransitionEnd = (e) => {
73
+ if (
74
+ e.propertyName === 'opacity' &&
75
+ _.getAttribute('aria-hidden') === 'true'
76
+ ) {
77
+ _.contentPanel.classList.add('hidden');
78
+
79
+ // Dispatch afterHide event - dialog has completed its transition
80
+ _.dispatchEvent(
81
+ new CustomEvent('afterHide', {
82
+ bubbles: true,
83
+ detail: { triggerElement: _.triggerEl },
84
+ })
85
+ );
86
+ }
87
+ };
88
+
24
89
  // Ensure we have labelledby and describedby references
25
90
  if (!_.getAttribute('aria-labelledby')) {
26
91
  const heading = _.querySelector('h1, h2, h3');
@@ -51,26 +116,31 @@ class DialogPanel extends HTMLElement {
51
116
  * @private
52
117
  */
53
118
  #bindUI() {
119
+ const _ = this;
120
+
54
121
  // Handle trigger buttons
55
122
  document.addEventListener('click', (e) => {
56
- const trigger = e.target.closest(
57
- `[aria-controls="${this.id}"]`
58
- );
123
+ const trigger = e.target.closest(`[aria-controls="${_.id}"]`);
59
124
  if (!trigger) return;
60
125
 
61
126
  if (trigger.getAttribute('data-prevent-default') === 'true') {
62
127
  e.preventDefault();
63
128
  }
64
129
 
65
- // this.triggerEl = trigger;
66
- this.show(trigger);
130
+ _.show(trigger);
67
131
  });
68
132
 
69
133
  // Handle close buttons
70
- this.addEventListener('click', (e) => {
134
+ _.addEventListener('click', (e) => {
71
135
  if (!e.target.closest('[data-action="hide-dialog"]')) return;
72
- this.hide();
136
+ _.hide();
73
137
  });
138
+
139
+ // Add transition end listener
140
+ _.contentPanel.addEventListener(
141
+ 'transitionend',
142
+ _.#handleTransitionEnd
143
+ );
74
144
  }
75
145
 
76
146
  /**
@@ -88,50 +158,108 @@ class DialogPanel extends HTMLElement {
88
158
  /**
89
159
  * Shows the dialog and traps focus within it
90
160
  * @param {HTMLElement} [triggerEl=null] - The element that triggered the dialog
161
+ * @fires DialogPanel#beforeShow - Fired before the dialog starts to show
162
+ * @fires DialogPanel#show - Fired when the dialog has been shown
163
+ * @returns {boolean} False if the show was prevented by a beforeShow event handler
91
164
  */
92
165
  show(triggerEl = null) {
93
- this.triggerEl = triggerEl || false;
166
+ const _ = this;
167
+ _.triggerEl = triggerEl || false;
94
168
 
95
- // Update ARIA states
96
- this.setAttribute('aria-hidden', 'false');
97
- if (this.triggerEl) {
98
- this.triggerEl.setAttribute('aria-expanded', 'true');
99
- }
169
+ // Dispatch beforeShow event - allows preventing the dialog from opening
170
+ const beforeShowEvent = new CustomEvent('beforeShow', {
171
+ bubbles: true,
172
+ cancelable: true,
173
+ detail: { triggerElement: _.triggerEl },
174
+ });
100
175
 
101
- // prevent body from scrolling
102
- document.body.classList.add('overflow-hidden');
176
+ const showAllowed = _.dispatchEvent(beforeShowEvent);
103
177
 
104
- // Focus management
105
- const firstFocusable = this.querySelector(
106
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
107
- );
108
- if (firstFocusable) {
109
- requestAnimationFrame(() => {
110
- firstFocusable.focus();
111
- });
112
- }
178
+ // If event was canceled (preventDefault was called), don't show the dialog
179
+ if (!showAllowed) return false;
180
+
181
+ // Remove the hidden class first to ensure content is rendered
182
+ _.contentPanel.classList.remove('hidden');
183
+
184
+ // Give the browser a moment to process before starting animation
185
+ requestAnimationFrame(() => {
186
+ // Update ARIA states
187
+ _.setAttribute('aria-hidden', 'false');
188
+ if (_.triggerEl) {
189
+ _.triggerEl.setAttribute('aria-expanded', 'true');
190
+ }
191
+
192
+ // Lock body scrolling and save scroll position
193
+ _.#lockScroll();
194
+
195
+ // Focus management
196
+ const firstFocusable = _.querySelector(
197
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
198
+ );
199
+ if (firstFocusable) {
200
+ requestAnimationFrame(() => {
201
+ firstFocusable.focus();
202
+ });
203
+ }
204
+
205
+ // Dispatch show event - dialog is now visible
206
+ _.dispatchEvent(
207
+ new CustomEvent('show', {
208
+ bubbles: true,
209
+ detail: { triggerElement: _.triggerEl },
210
+ })
211
+ );
212
+ });
213
+
214
+ return true;
113
215
  }
114
216
 
115
217
  /**
116
218
  * Hides the dialog and restores focus
219
+ * @fires DialogPanel#beforeHide - Fired before the dialog starts to hide
220
+ * @fires DialogPanel#hide - Fired when the dialog has started hiding (transition begins)
221
+ * @fires DialogPanel#afterHide - Fired when the dialog has completed its hide transition
222
+ * @returns {boolean} False if the hide was prevented by a beforeHide event handler
117
223
  */
118
224
  hide() {
119
- // allow body to scroll
120
- document.body.classList.remove('overflow-hidden');
225
+ const _ = this;
226
+
227
+ // Dispatch beforeHide event - allows preventing the dialog from closing
228
+ const beforeHideEvent = new CustomEvent('beforeHide', {
229
+ bubbles: true,
230
+ cancelable: true,
231
+ detail: { triggerElement: _.triggerEl },
232
+ });
233
+
234
+ const hideAllowed = _.dispatchEvent(beforeHideEvent);
235
+
236
+ // If event was canceled (preventDefault was called), don't hide the dialog
237
+ if (!hideAllowed) return false;
238
+
239
+ // Restore body scroll and scroll position
240
+ _.#restoreScroll();
121
241
 
122
242
  // Update ARIA states
123
- if (this.triggerEl) {
124
- this.triggerEl.setAttribute('aria-expanded', 'false');
125
- // Restore focus to trigger element
126
- this.triggerEl.focus();
127
- } else {
128
- console.log('we need to blur focus');
243
+ if (_.triggerEl) {
244
+ // remove focus from modal panel first
245
+ _.triggerEl.focus();
246
+ // mark trigger as no longer expanded
247
+ _.triggerEl.setAttribute('aria-expanded', 'false');
129
248
  }
130
249
 
131
- // hide dialog panel
132
- setTimeout(() => {
133
- this.setAttribute('aria-hidden', 'true');
134
- }, 1);
250
+ // Set aria-hidden to start transition
251
+ // The transitionend event handler will add display:none when complete
252
+ _.setAttribute('aria-hidden', 'true');
253
+
254
+ // Dispatch hide event - dialog is now starting to hide
255
+ _.dispatchEvent(
256
+ new CustomEvent('hide', {
257
+ bubbles: true,
258
+ detail: { triggerElement: _.triggerEl },
259
+ })
260
+ );
261
+
262
+ return true;
135
263
  }
136
264
  }
137
265
 
@@ -166,9 +294,15 @@ class DialogContent extends HTMLElement {
166
294
  }
167
295
  }
168
296
 
169
- customElements.define('dialog-panel', DialogPanel);
170
- customElements.define('dialog-overlay', DialogOverlay);
171
- customElements.define('dialog-content', DialogContent);
297
+ if (!customElements.get('dialog-panel')) {
298
+ customElements.define('dialog-panel', DialogPanel);
299
+ }
300
+ if (!customElements.get('dialog-overlay')) {
301
+ customElements.define('dialog-overlay', DialogOverlay);
302
+ }
303
+ if (!customElements.get('dialog-content')) {
304
+ customElements.define('dialog-content', DialogContent);
305
+ }
172
306
 
173
307
  export { DialogPanel, DialogOverlay, DialogContent };
174
308
  export default DialogPanel;
package/src/index.scss ADDED
@@ -0,0 +1,2 @@
1
+ @forward "scss/variables";
2
+ @forward "scss/dialog-panel";
@@ -0,0 +1,73 @@
1
+ // Import variables using the modern @use rule
2
+ @use 'variables' as vars;
3
+
4
+ dialog-panel {
5
+ /* Make it take no space and be invisible in the document flow */
6
+ display: contents;
7
+
8
+ &[aria-hidden='false'] {
9
+ dialog-overlay,
10
+ dialog-content {
11
+ pointer-events: auto;
12
+ opacity: 1;
13
+ transform: scale(1);
14
+ filter: blur(0px);
15
+ }
16
+ }
17
+ }
18
+
19
+ /* Overlay background */
20
+ dialog-overlay {
21
+ position: fixed;
22
+ top: 0;
23
+ left: 0;
24
+ width: 100vw;
25
+ height: 100vh;
26
+ opacity: 0;
27
+ pointer-events: none;
28
+ z-index: var(--dp-overlay-z-index, 1000);
29
+ transition: var(--dp-overlay-transition, all 300ms ease-out);
30
+ background-color: var(
31
+ --dp-overlay-background,
32
+ rgba(20, 23, 26, 0.4)
33
+ );
34
+ backdrop-filter: var(
35
+ --dp-overlay-backdrop-filter,
36
+ blur(2px) saturate(120%)
37
+ );
38
+ }
39
+
40
+ dialog-content {
41
+ position: fixed;
42
+ top: 50%;
43
+ left: 50%;
44
+ transform: translate(-50%, -50%) scale(0.95);
45
+ max-width: 90vw;
46
+ max-height: 85vh;
47
+ display: var(--dp-content-display, block);
48
+ opacity: 0;
49
+ background: var(--dp-content-background, white);
50
+ pointer-events: none;
51
+ z-index: var(--dp-content-z-index, 1001);
52
+ box-shadow: var(
53
+ --dp-content-shadow,
54
+ 0 10px 25px rgba(0, 0, 0, 0.15)
55
+ );
56
+ border-radius: var(--dp-content-border-radius, 8px);
57
+ overflow: auto;
58
+ transition:
59
+ opacity var(--dp-transition-duration, 300ms)
60
+ var(--dp-transition-timing, ease-out),
61
+ transform var(--dp-transition-duration, 300ms)
62
+ var(--dp-transition-timing, ease-out);
63
+
64
+ /* When shown, reset transform to center */
65
+ dialog-panel[aria-hidden='false'] & {
66
+ transform: translate(-50%, -50%) scale(1);
67
+ }
68
+
69
+ /* When explicitly hidden, remove from layout */
70
+ &.hidden {
71
+ display: none;
72
+ }
73
+ }
@@ -0,0 +1,54 @@
1
+ // Dialog Panel variables
2
+ // SCSS variables for internal component use and customization
3
+ // These variables can be customized by the user
4
+
5
+ // Layout
6
+ $panel-top: 0 !default;
7
+ $panel-left: 0 !default;
8
+ $panel-width: 100vw !default;
9
+ $panel-height: 100vh !default;
10
+ $panel-z-index: 10 !default;
11
+
12
+ // Overlay
13
+ $overlay-z-index: 1000 !default;
14
+ $overlay-background: rgba(20, 23, 26, 0.4) !default;
15
+ $overlay-backdrop-filter: blur(2px) saturate(120%) !default;
16
+ $overlay-transition: all 400ms ease-out !default;
17
+
18
+ // Content
19
+ $content-display: block !default;
20
+ $content-background: white !default;
21
+ $content-z-index: 1001 !default;
22
+ $content-shadow: 0 10px 25px rgba(0, 0, 0, 0.15) !default;
23
+ $content-border-radius: 8px !default;
24
+
25
+ // Animation
26
+ $transition-duration: 400ms !default;
27
+ $transition-timing: ease-out !default;
28
+
29
+ // Define CSS Custom Properties using SCSS values
30
+ :root {
31
+ // Layout
32
+ --dp-panel-top: #{$panel-top};
33
+ --dp-panel-left: #{$panel-left};
34
+ --dp-panel-width: #{$panel-width};
35
+ --dp-panel-height: #{$panel-height};
36
+ --dp-panel-z-index: #{$panel-z-index};
37
+
38
+ // Overlay
39
+ --dp-overlay-z-index: #{$overlay-z-index};
40
+ --dp-overlay-background: #{$overlay-background};
41
+ --dp-overlay-backdrop-filter: #{$overlay-backdrop-filter};
42
+ --dp-overlay-transition: #{$overlay-transition};
43
+
44
+ // Content
45
+ --dp-content-display: #{$content-display};
46
+ --dp-content-background: #{$content-background};
47
+ --dp-content-z-index: #{$content-z-index};
48
+ --dp-content-shadow: #{$content-shadow};
49
+ --dp-content-border-radius: #{$content-border-radius};
50
+
51
+ // Animation
52
+ --dp-transition-duration: #{$transition-duration};
53
+ --dp-transition-timing: #{$transition-timing};
54
+ }
@@ -1,47 +0,0 @@
1
- dialog-panel {
2
- position: fixed;
3
- top: 0;
4
- left: 0;
5
- width: 100vw;
6
- height: 100vh;
7
- z-index: 10;
8
- pointer-events: none;
9
- }
10
-
11
- /* transparent background */
12
- dialog-overlay {
13
- position: absolute;
14
- top: 0;
15
- left: 0;
16
- width: 100vw;
17
- height: 100vh;
18
- opacity: 0;
19
- pointer-events: none;
20
- transition: all 300ms ease-out;
21
- background-color: rgba(20, 23, 26, 0.4);
22
- backdrop-filter: blur(2px) saturate(120%);
23
- }
24
-
25
- dialog-content {
26
- display: block;
27
- opacity: 0;
28
- background: white;
29
- }
30
-
31
- dialog-panel[aria-hidden='false'] {
32
- pointer-events: all;
33
- }
34
-
35
- dialog-panel[aria-hidden='false'] dialog-overlay {
36
- pointer-events: all;
37
- opacity: 1;
38
- transform: scale(1);
39
- filter: blur(0px);
40
- }
41
-
42
- dialog-panel[aria-hidden='false'] dialog-content {
43
- opacity: 1;
44
- z-index: 10;
45
- transform: scale(1);
46
- filter: blur(0px);
47
- }