@pure-ds/core 0.6.1 → 0.6.3

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * PDS Enhancers - Single Source of Truth
3
- *
3
+ *
4
4
  * This file defines all progressive enhancements for the Pure Design System.
5
5
  * Each enhancer has:
6
6
  * - selector: CSS selector to target elements
@@ -30,24 +30,28 @@ function enhanceAccordion(elem) {
30
30
  if (elem.dataset.enhancedAccordion) return;
31
31
  elem.dataset.enhancedAccordion = "true";
32
32
 
33
- elem.addEventListener("toggle", (event) => {
34
- // Only handle toggle events from direct child details elements
35
- // to avoid closing parent details when nested accordions are used
36
- if (event.target.open && event.target.parentElement === elem) {
37
- elem.querySelectorAll(":scope > details[open]").forEach((details) => {
38
- if (details !== event.target) {
39
- details.open = false;
40
- }
41
- });
42
- }
43
- }, true);
33
+ elem.addEventListener(
34
+ "toggle",
35
+ (event) => {
36
+ // Only handle toggle events from direct child details elements
37
+ // to avoid closing parent details when nested accordions are used
38
+ if (event.target.open && event.target.parentElement === elem) {
39
+ elem.querySelectorAll(":scope > details[open]").forEach((details) => {
40
+ if (details !== event.target) {
41
+ details.open = false;
42
+ }
43
+ });
44
+ }
45
+ },
46
+ true,
47
+ );
44
48
  }
45
49
 
46
50
  function enhanceDropdown(elem) {
47
51
  if (elem.dataset.enhancedDropdown) return;
48
52
  elem.dataset.enhancedDropdown = "true";
49
- const menu = elem.lastElementChild
50
-
53
+ const menu = elem.lastElementChild;
54
+
51
55
  if (!menu) return;
52
56
 
53
57
  const trigger =
@@ -90,7 +94,7 @@ function enhanceDropdown(elem) {
90
94
  menu?.offsetHeight || 0,
91
95
  menu?.scrollHeight || 0,
92
96
  menuRect.height || 0,
93
- 200
97
+ 200,
94
98
  );
95
99
  const spaceBelow = Math.max(0, window.innerHeight - rect.bottom);
96
100
  const spaceAbove = Math.max(0, rect.top);
@@ -105,7 +109,12 @@ function enhanceDropdown(elem) {
105
109
  elem.getAttribute("data-dropdown-align") ||
106
110
  "auto"
107
111
  ).toLowerCase();
108
- if (align === "left" || align === "right" || align === "start" || align === "end") {
112
+ if (
113
+ align === "left" ||
114
+ align === "right" ||
115
+ align === "start" ||
116
+ align === "end"
117
+ ) {
109
118
  return align === "start" ? "left" : align === "end" ? "right" : align;
110
119
  }
111
120
  const rect = elem.getBoundingClientRect();
@@ -114,7 +123,7 @@ function enhanceDropdown(elem) {
114
123
  menu?.offsetWidth || 0,
115
124
  menu?.scrollWidth || 0,
116
125
  menuRect.width || 0,
117
- 240
126
+ 240,
118
127
  );
119
128
  const spaceRight = Math.max(0, window.innerWidth - rect.left);
120
129
  const spaceLeft = Math.max(0, rect.right);
@@ -123,16 +132,42 @@ function enhanceDropdown(elem) {
123
132
  return spaceLeft > spaceRight ? "right" : "left";
124
133
  };
125
134
 
135
+ // Store click handler reference for cleanup
136
+ let clickHandler = null;
137
+
126
138
  const openMenu = () => {
127
139
  elem.dataset.dropdownDirection = resolveDirection();
128
140
  elem.dataset.dropdownAlign = resolveAlign();
129
141
  menu.setAttribute("aria-hidden", "false");
130
142
  trigger?.setAttribute("aria-expanded", "true");
143
+
144
+ // Add click-outside handler when opening
145
+ if (!clickHandler) {
146
+ clickHandler = (event) => {
147
+ // Use composedPath() to handle Shadow DOM
148
+ const path = event.composedPath ? event.composedPath() : [event.target];
149
+ const clickedInside = path.some((node) => node === elem);
150
+
151
+ if (!clickedInside) {
152
+ closeMenu();
153
+ }
154
+ };
155
+ // Use a slight delay to avoid closing immediately if this was triggered by a click
156
+ setTimeout(() => {
157
+ document.addEventListener("click", clickHandler);
158
+ }, 0);
159
+ }
131
160
  };
132
161
 
133
162
  const closeMenu = () => {
134
163
  menu.setAttribute("aria-hidden", "true");
135
164
  trigger?.setAttribute("aria-expanded", "false");
165
+
166
+ // Remove click-outside handler when closing
167
+ if (clickHandler) {
168
+ document.removeEventListener("click", clickHandler);
169
+ clickHandler = null;
170
+ }
136
171
  };
137
172
 
138
173
  const toggleMenu = () => {
@@ -149,12 +184,6 @@ function enhanceDropdown(elem) {
149
184
  toggleMenu();
150
185
  });
151
186
 
152
- document.addEventListener("click", (event) => {
153
- if (!elem.contains(event.target)) {
154
- closeMenu();
155
- }
156
- });
157
-
158
187
  elem.addEventListener("keydown", (event) => {
159
188
  if (event.key === "Escape") {
160
189
  closeMenu();
@@ -163,8 +192,15 @@ function enhanceDropdown(elem) {
163
192
  });
164
193
 
165
194
  elem.addEventListener("focusout", (event) => {
166
- if (!event.relatedTarget || !elem.contains(event.relatedTarget)) {
167
- closeMenu();
195
+ // Only close if focus is explicitly moving to an element outside the dropdown
196
+ // Don't close if relatedTarget is null (which happens when clicking non-focusable elements inside)
197
+ // Use composedPath() to handle Shadow DOM properly
198
+ if (event.relatedTarget) {
199
+ const path = event.composedPath ? event.composedPath() : [event.relatedTarget];
200
+ const focusedInside = path.some((node) => node === elem);
201
+ if (!focusedInside) {
202
+ closeMenu();
203
+ }
168
204
  }
169
205
  });
170
206
  }
@@ -298,20 +334,18 @@ function enhanceRange(elem) {
298
334
  }
299
335
 
300
336
  function enhanceRequired(elem) {
301
-
302
- if (elem.dataset.enhancedRequired) return;
303
- elem.dataset.enhancedRequired = "true";
304
-
337
+ if (elem.dataset.enhancedRequired) return;
338
+ elem.dataset.enhancedRequired = "true";
339
+
305
340
  const enhanceRequiredField = (input) => {
306
341
  let label;
307
- if(input.closest("[role$=group]")) { // Handles both radiogroup and group
342
+ if (input.closest("[role$=group]")) {
343
+ // Handles both radiogroup and group
308
344
  label = input.closest("[role$=group]").querySelector("legend");
309
- }
310
- else{
345
+ } else {
311
346
  label = input.closest("label");
312
347
  }
313
348
  if (!label) return;
314
-
315
349
 
316
350
  if (label.querySelector(".required-asterisk")) return;
317
351
 
@@ -339,15 +373,14 @@ function enhanceRequired(elem) {
339
373
  legend.textContent = "* Required fields";
340
374
  form.insertBefore(
341
375
  legend,
342
- form.querySelector(".form-actions") || form.lastElementChild
376
+ form.querySelector(".form-actions") || form.lastElementChild,
343
377
  );
344
378
  }
345
- }
379
+ };
346
380
 
347
381
  elem.querySelectorAll("[required]").forEach((input) => {
348
382
  enhanceRequiredField(input);
349
383
  });
350
-
351
384
  }
352
385
 
353
386
  function enhanceOpenGroup(elem) {
@@ -362,7 +395,7 @@ function enhanceOpenGroup(elem) {
362
395
  addInput.classList.add("input-text", "input-sm");
363
396
  addInput.style.width = "auto";
364
397
  const firstInput = elem.querySelector(
365
- 'input[type="radio"], input[type="checkbox"]'
398
+ 'input[type="radio"], input[type="checkbox"]',
366
399
  );
367
400
 
368
401
  elem.appendChild(addInput);
@@ -373,9 +406,7 @@ function enhanceOpenGroup(elem) {
373
406
  event.preventDefault();
374
407
 
375
408
  const type = firstInput.type === "radio" ? "radio" : "checkbox";
376
- const id = `open-group-${Math.random()
377
- .toString(36)
378
- .substring(2, 11)}`;
409
+ const id = `open-group-${Math.random().toString(36).substring(2, 11)}`;
379
410
  const label = document.createElement("label");
380
411
 
381
412
  const span = document.createElement("span");
@@ -510,7 +541,7 @@ const enhancerRunners = new Map([
510
541
  /**
511
542
  * Complete enhancers with runtime functions.
512
543
  * Used by PDS.enhancer() and AutoDefiner at runtime.
513
- *
544
+ *
514
545
  * This is the canonical runtime array of enhancer objects.
515
546
  */
516
547
  export const defaultPDSEnhancers = enhancerDefinitions.map((meta) => ({