@magic-spells/dialog-panel 0.1.0 → 0.2.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.
@@ -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.0",
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,50 @@ 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('transitionend', _.#handleTransitionEnd);
19
+ }
20
+
21
+ // Ensure body scroll is restored if component is removed while open
22
+ document.body.classList.remove('overflow-hidden');
23
+ this.#restoreScroll();
24
+ }
25
+
26
+ /**
27
+ * Saves current scroll position and locks body scrolling
28
+ * @private
29
+ */
30
+ #lockScroll() {
31
+ const _ = this;
32
+ // Save current scroll position
33
+ _.#scrollPosition = window.pageYOffset;
34
+
35
+ // Apply fixed position to body
36
+ document.body.classList.add('overflow-hidden');
37
+ document.body.style.top = `-${_.#scrollPosition}px`;
38
+ }
39
+
40
+ /**
41
+ * Restores scroll position when dialog is closed
42
+ * @private
43
+ */
44
+ #restoreScroll() {
45
+ const _ = this;
46
+ // Remove fixed positioning
47
+ document.body.classList.remove('overflow-hidden');
48
+ document.body.style.removeProperty('top');
49
+
50
+ // Restore scroll position
51
+ window.scrollTo(0, _.#scrollPosition);
52
+ }
9
53
  /**
10
54
  * Initializes the dialog panel, sets up focus trap and overlay
11
55
  */
@@ -20,6 +64,19 @@ class DialogPanel extends HTMLElement {
20
64
  _.contentPanel = _.querySelector('dialog-content');
21
65
  _.focusTrap = document.createElement('focus-trap');
22
66
  _.triggerEl = null;
67
+
68
+ // Create a handler for transition end events
69
+ _.#handleTransitionEnd = (e) => {
70
+ if (e.propertyName === 'opacity' && _.getAttribute('aria-hidden') === 'true') {
71
+ _.contentPanel.classList.add('hidden');
72
+
73
+ // Dispatch afterHide event - dialog has completed its transition
74
+ _.dispatchEvent(new CustomEvent('afterHide', {
75
+ bubbles: true,
76
+ detail: { triggerElement: _.triggerEl }
77
+ }));
78
+ }
79
+ };
23
80
 
24
81
  // Ensure we have labelledby and describedby references
25
82
  if (!_.getAttribute('aria-labelledby')) {
@@ -51,10 +108,12 @@ class DialogPanel extends HTMLElement {
51
108
  * @private
52
109
  */
53
110
  #bindUI() {
111
+ const _ = this;
112
+
54
113
  // Handle trigger buttons
55
114
  document.addEventListener('click', (e) => {
56
115
  const trigger = e.target.closest(
57
- `[aria-controls="${this.id}"]`
116
+ `[aria-controls="${_.id}"]`
58
117
  );
59
118
  if (!trigger) return;
60
119
 
@@ -62,15 +121,17 @@ class DialogPanel extends HTMLElement {
62
121
  e.preventDefault();
63
122
  }
64
123
 
65
- // this.triggerEl = trigger;
66
- this.show(trigger);
124
+ _.show(trigger);
67
125
  });
68
126
 
69
127
  // Handle close buttons
70
- this.addEventListener('click', (e) => {
128
+ _.addEventListener('click', (e) => {
71
129
  if (!e.target.closest('[data-action="hide-dialog"]')) return;
72
- this.hide();
130
+ _.hide();
73
131
  });
132
+
133
+ // Add transition end listener
134
+ _.contentPanel.addEventListener('transitionend', _.#handleTransitionEnd);
74
135
  }
75
136
 
76
137
  /**
@@ -88,50 +149,104 @@ class DialogPanel extends HTMLElement {
88
149
  /**
89
150
  * Shows the dialog and traps focus within it
90
151
  * @param {HTMLElement} [triggerEl=null] - The element that triggered the dialog
152
+ * @fires DialogPanel#beforeShow - Fired before the dialog starts to show
153
+ * @fires DialogPanel#show - Fired when the dialog has been shown
154
+ * @returns {boolean} False if the show was prevented by a beforeShow event handler
91
155
  */
92
156
  show(triggerEl = null) {
93
- this.triggerEl = triggerEl || false;
94
-
95
- // Update ARIA states
96
- this.setAttribute('aria-hidden', 'false');
97
- if (this.triggerEl) {
98
- this.triggerEl.setAttribute('aria-expanded', 'true');
99
- }
157
+ const _ = this;
158
+ _.triggerEl = triggerEl || false;
100
159
 
101
- // prevent body from scrolling
102
- document.body.classList.add('overflow-hidden');
160
+ // Dispatch beforeShow event - allows preventing the dialog from opening
161
+ const beforeShowEvent = new CustomEvent('beforeShow', {
162
+ bubbles: true,
163
+ cancelable: true,
164
+ detail: { triggerElement: _.triggerEl }
165
+ });
166
+
167
+ const showAllowed = _.dispatchEvent(beforeShowEvent);
168
+
169
+ // If event was canceled (preventDefault was called), don't show the dialog
170
+ if (!showAllowed) return false;
103
171
 
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
- }
172
+ // Remove the hidden class first to ensure content is rendered
173
+ _.contentPanel.classList.remove('hidden');
174
+
175
+ // Give the browser a moment to process before starting animation
176
+ requestAnimationFrame(() => {
177
+ // Update ARIA states
178
+ _.setAttribute('aria-hidden', 'false');
179
+ if (_.triggerEl) {
180
+ _.triggerEl.setAttribute('aria-expanded', 'true');
181
+ }
182
+
183
+ // Lock body scrolling and save scroll position
184
+ _.#lockScroll();
185
+
186
+ // Focus management
187
+ const firstFocusable = _.querySelector(
188
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
189
+ );
190
+ if (firstFocusable) {
191
+ requestAnimationFrame(() => {
192
+ firstFocusable.focus();
193
+ });
194
+ }
195
+
196
+ // Dispatch show event - dialog is now visible
197
+ _.dispatchEvent(new CustomEvent('show', {
198
+ bubbles: true,
199
+ detail: { triggerElement: _.triggerEl }
200
+ }));
201
+ });
202
+
203
+ return true;
113
204
  }
114
205
 
115
206
  /**
116
207
  * Hides the dialog and restores focus
208
+ * @fires DialogPanel#beforeHide - Fired before the dialog starts to hide
209
+ * @fires DialogPanel#hide - Fired when the dialog has started hiding (transition begins)
210
+ * @fires DialogPanel#afterHide - Fired when the dialog has completed its hide transition
211
+ * @returns {boolean} False if the hide was prevented by a beforeHide event handler
117
212
  */
118
213
  hide() {
119
- // allow body to scroll
120
- document.body.classList.remove('overflow-hidden');
214
+ const _ = this;
215
+
216
+ // Dispatch beforeHide event - allows preventing the dialog from closing
217
+ const beforeHideEvent = new CustomEvent('beforeHide', {
218
+ bubbles: true,
219
+ cancelable: true,
220
+ detail: { triggerElement: _.triggerEl }
221
+ });
222
+
223
+ const hideAllowed = _.dispatchEvent(beforeHideEvent);
224
+
225
+ // If event was canceled (preventDefault was called), don't hide the dialog
226
+ if (!hideAllowed) return false;
227
+
228
+ // Restore body scroll and scroll position
229
+ _.#restoreScroll();
121
230
 
122
231
  // 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');
232
+ if (_.triggerEl) {
233
+ // remove focus from modal panel first
234
+ _.triggerEl.focus();
235
+ // mark trigger as no longer expanded
236
+ _.triggerEl.setAttribute('aria-expanded', 'false');
129
237
  }
130
238
 
131
- // hide dialog panel
132
- setTimeout(() => {
133
- this.setAttribute('aria-hidden', 'true');
134
- }, 1);
239
+ // Set aria-hidden to start transition
240
+ // The transitionend event handler will add display:none when complete
241
+ _.setAttribute('aria-hidden', 'true');
242
+
243
+ // Dispatch hide event - dialog is now starting to hide
244
+ _.dispatchEvent(new CustomEvent('hide', {
245
+ bubbles: true,
246
+ detail: { triggerElement: _.triggerEl }
247
+ }));
248
+
249
+ return true;
135
250
  }
136
251
  }
137
252
 
@@ -171,4 +286,4 @@ customElements.define('dialog-overlay', DialogOverlay);
171
286
  customElements.define('dialog-content', DialogContent);
172
287
 
173
288
  export { DialogPanel, DialogOverlay, DialogContent };
174
- export default DialogPanel;
289
+ 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
- }