@rettangoli/ui 1.7.5 → 1.8.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.
@@ -1,8 +1,34 @@
1
- import { css } from "../common.js";
1
+ import {
2
+ css,
3
+ mediaQueries,
4
+ getResponsiveAttribute,
5
+ permutateBreakpoints,
6
+ } from "../common.js";
2
7
  import { calculatePopoverPosition } from "../common/popover.js";
3
8
 
4
9
  const CONTENT_WRAPPER_ATTR = "data-rtgl-popover-content";
5
10
  const DEFAULT_CONTENT_STYLE = "min-width: 200px; max-width: 400px; box-sizing: border-box;";
11
+ const ACTIVE_OVERLAY_ATTR = "data-rtgl-active-overlay";
12
+ const ACTIVE_PLACE_ATTR = "data-rtgl-active-place";
13
+ const RESPONSIVE_POPOVER_SIZES = ["sm", "md", "lg", "xl"];
14
+ const mediaQueryCondition = (mediaQuery) => mediaQuery.replace(/^@media\s+/, "");
15
+
16
+ const parseResponsiveBooleanValue = (value, fallback = false) => {
17
+ if (value === null || value === undefined) {
18
+ return fallback;
19
+ }
20
+
21
+ if (typeof value === "boolean") {
22
+ return value;
23
+ }
24
+
25
+ const normalized = `${value}`.trim().toLowerCase();
26
+ if (["false", "0", "no", "off"].includes(normalized)) {
27
+ return false;
28
+ }
29
+
30
+ return true;
31
+ };
6
32
 
7
33
  class RettangoliPopoverElement extends HTMLElement {
8
34
  static styleSheet = null;
@@ -40,6 +66,10 @@ class RettangoliPopoverElement extends HTMLElement {
40
66
  pointer-events: auto;
41
67
  }
42
68
 
69
+ :host([${ACTIVE_OVERLAY_ATTR}="true"]) dialog::backdrop {
70
+ background-color: rgba(0, 0, 0, 0.5);
71
+ }
72
+
43
73
  .popover-container {
44
74
  position: fixed;
45
75
  z-index: inherit;
@@ -120,7 +150,13 @@ class RettangoliPopoverElement extends HTMLElement {
120
150
  this._revealFrameId = null;
121
151
  this._positionVersion = 0;
122
152
  this._isObservingResize = false;
153
+ this._isModalOpen = false;
123
154
  this._observedContentWrapper = null;
155
+ this._onWindowResize = () => {
156
+ this._updateActiveStateAttributes();
157
+ this._syncDialogMode();
158
+ this._schedulePositionUpdate();
159
+ };
124
160
  this._resizeObserver = typeof ResizeObserver === "function"
125
161
  ? new ResizeObserver(() => {
126
162
  if (this._isOpen) {
@@ -142,8 +178,11 @@ class RettangoliPopoverElement extends HTMLElement {
142
178
  "open",
143
179
  "x",
144
180
  "y",
145
- "place",
146
- "no-overlay",
181
+ ...permutateBreakpoints([
182
+ "place",
183
+ "overlay",
184
+ "no-overlay",
185
+ ]),
147
186
  "content-w",
148
187
  "content-h",
149
188
  "content-wh",
@@ -158,6 +197,7 @@ class RettangoliPopoverElement extends HTMLElement {
158
197
 
159
198
  connectedCallback() {
160
199
  this._syncContentWrapper({ reposition: false });
200
+ this._updateActiveStateAttributes();
161
201
 
162
202
  // Check initial open attribute
163
203
  if (this.hasAttribute('open')) {
@@ -168,11 +208,13 @@ class RettangoliPopoverElement extends HTMLElement {
168
208
  disconnectedCallback() {
169
209
  this._cancelScheduledPositionUpdate();
170
210
  this._stopResizeObserver();
211
+ window.removeEventListener("resize", this._onWindowResize);
171
212
 
172
213
  // Clean up dialog if it's open
173
214
  if (this._isOpen && this._dialogElement.open) {
174
215
  this._dialogElement.close();
175
216
  }
217
+ this._isModalOpen = false;
176
218
  }
177
219
 
178
220
  attributeChangedCallback(name, oldValue, newValue) {
@@ -185,16 +227,113 @@ class RettangoliPopoverElement extends HTMLElement {
185
227
  } else if (newValue === null && this._isOpen) {
186
228
  this._hide();
187
229
  }
188
- } else if ((name === 'x' || name === 'y' || name === 'place') && this._isOpen) {
189
- this._schedulePositionUpdate();
190
- } else if (name === 'no-overlay' && oldValue !== newValue && this._isOpen) {
191
- this._hide();
192
- this._show();
230
+ } else if (name === 'x' || name === 'y' || name.endsWith('place')) {
231
+ this._updateActiveStateAttributes();
232
+ if (this._isOpen) {
233
+ this._schedulePositionUpdate();
234
+ }
235
+ } else if (name.endsWith('overlay')) {
236
+ this._updateActiveStateAttributes();
237
+ if (this._isOpen) {
238
+ this._syncDialogMode();
239
+ this._schedulePositionUpdate();
240
+ }
193
241
  } else if (name.startsWith("content-")) {
194
242
  this._syncContentWrapper();
195
243
  }
196
244
  }
197
245
 
246
+ _getActiveResponsiveSize() {
247
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
248
+ return "default";
249
+ }
250
+
251
+ for (const size of RESPONSIVE_POPOVER_SIZES) {
252
+ const query = mediaQueries[size];
253
+ if (query && window.matchMedia(mediaQueryCondition(query)).matches) {
254
+ return size;
255
+ }
256
+ }
257
+
258
+ return "default";
259
+ }
260
+
261
+ _getActiveResponsiveAttribute(attr) {
262
+ return getResponsiveAttribute({
263
+ element: this,
264
+ size: this._getActiveResponsiveSize(),
265
+ attr,
266
+ });
267
+ }
268
+
269
+ _getActivePlace() {
270
+ return this._getActiveResponsiveAttribute("place") || "bs";
271
+ }
272
+
273
+ _isOverlayActive() {
274
+ return parseResponsiveBooleanValue(
275
+ this._getActiveResponsiveAttribute("overlay"),
276
+ false,
277
+ );
278
+ }
279
+
280
+ _isNoOverlayActive() {
281
+ return parseResponsiveBooleanValue(
282
+ this._getActiveResponsiveAttribute("no-overlay"),
283
+ false,
284
+ );
285
+ }
286
+
287
+ _shouldUseModalDialog() {
288
+ return this._isOverlayActive() || !this._isNoOverlayActive();
289
+ }
290
+
291
+ _updateActiveStateAttributes() {
292
+ const activePlace = this._getActivePlace();
293
+ const overlayActive = this._isOverlayActive();
294
+
295
+ if (this.getAttribute(ACTIVE_PLACE_ATTR) !== activePlace) {
296
+ this.setAttribute(ACTIVE_PLACE_ATTR, activePlace);
297
+ }
298
+
299
+ if (overlayActive) {
300
+ if (this.getAttribute(ACTIVE_OVERLAY_ATTR) !== "true") {
301
+ this.setAttribute(ACTIVE_OVERLAY_ATTR, "true");
302
+ }
303
+ } else if (this.hasAttribute(ACTIVE_OVERLAY_ATTR)) {
304
+ this.removeAttribute(ACTIVE_OVERLAY_ATTR);
305
+ }
306
+ }
307
+
308
+ _openDialogElement() {
309
+ if (this._dialogElement.open) {
310
+ return;
311
+ }
312
+
313
+ if (this._shouldUseModalDialog()) {
314
+ this._dialogElement.showModal();
315
+ this._isModalOpen = true;
316
+ } else {
317
+ this._dialogElement.show();
318
+ this._isModalOpen = false;
319
+ }
320
+ }
321
+
322
+ _syncDialogMode() {
323
+ if (!this._isOpen || !this._dialogElement.open) {
324
+ return;
325
+ }
326
+
327
+ const shouldUseModal = this._shouldUseModalDialog();
328
+ if (shouldUseModal === this._isModalOpen) {
329
+ return;
330
+ }
331
+
332
+ this._dialogElement.close();
333
+ this._isModalOpen = false;
334
+ this._openDialogElement();
335
+ }
336
+
198
337
  _isIgnorableTextNode(node) {
199
338
  return node?.nodeType === Node.TEXT_NODE && node.textContent?.trim() === "";
200
339
  }
@@ -288,6 +427,7 @@ class RettangoliPopoverElement extends HTMLElement {
288
427
  _show() {
289
428
  if (!this._isOpen) {
290
429
  this._syncContentWrapper({ reposition: false });
430
+ this._updateActiveStateAttributes();
291
431
 
292
432
  // Create and append slot for content only if it doesn't exist
293
433
  if (!this._slotElement) {
@@ -298,16 +438,13 @@ class RettangoliPopoverElement extends HTMLElement {
298
438
 
299
439
  this._isOpen = true;
300
440
  this._startResizeObserver();
441
+ window.addEventListener("resize", this._onWindowResize);
301
442
 
302
443
  // Show the dialog using setTimeout to ensure it's in the DOM
303
444
  if (!this._dialogElement.open) {
304
445
  setTimeout(() => {
305
- if (this._dialogElement && !this._dialogElement.open) {
306
- if (this.hasAttribute('no-overlay')) {
307
- this._dialogElement.show();
308
- } else {
309
- this._dialogElement.showModal();
310
- }
446
+ if (this._isOpen && this._dialogElement && !this._dialogElement.open) {
447
+ this._openDialogElement();
311
448
  }
312
449
 
313
450
  this._schedulePositionUpdate();
@@ -323,11 +460,13 @@ class RettangoliPopoverElement extends HTMLElement {
323
460
  this._isOpen = false;
324
461
  this._cancelScheduledPositionUpdate();
325
462
  this._stopResizeObserver();
463
+ window.removeEventListener("resize", this._onWindowResize);
326
464
 
327
465
  // Close the dialog
328
466
  if (this._dialogElement.open) {
329
467
  this._dialogElement.close();
330
468
  }
469
+ this._isModalOpen = false;
331
470
 
332
471
  // Remove slot to unmount content
333
472
  if (this._slotElement) {
@@ -413,7 +552,8 @@ class RettangoliPopoverElement extends HTMLElement {
413
552
 
414
553
  const x = this._readCoordinateAttr('x');
415
554
  const y = this._readCoordinateAttr('y');
416
- const place = this.getAttribute('place') || 'bs';
555
+ const place = this._getActivePlace();
556
+ this._updateActiveStateAttributes();
417
557
  const rect = this._popoverContainer.getBoundingClientRect();
418
558
  const { left, top } = this._calculatePosition(x, y, rect.width, rect.height, place);
419
559