@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/ui",
3
- "version": "1.7.5",
3
+ "version": "1.8.0",
4
4
  "description": "A UI component library for building web interfaces.",
5
5
  "main": "dist/rettangoli-esm.min.js",
6
6
  "type": "module",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "homepage": "https://github.com/yuusoft-org/rettangoli#readme",
64
64
  "dependencies": {
65
- "@rettangoli/fe": "1.1.3",
65
+ "@rettangoli/fe": "1.2.0",
66
66
  "jempl": "1.0.1"
67
67
  }
68
68
  }
@@ -61,6 +61,11 @@ export const calculatePopoverPosition = ({
61
61
  left = x - width - offset;
62
62
  top = y - height;
63
63
  break;
64
+ case "center":
65
+ case "c":
66
+ left = (viewportWidth - width) / 2;
67
+ top = (viewportHeight - height) / 2;
68
+ break;
64
69
  default:
65
70
  left = x;
66
71
  top = y + offset;
@@ -11,6 +11,18 @@ const getItemType = (item = {}) => {
11
11
  return 'item';
12
12
  };
13
13
 
14
+ export const handleOnUpdate = (deps, payload) => {
15
+ const { render, refs } = deps;
16
+ const { oldProps = {}, newProps = {} } = payload;
17
+ const shouldRefreshPopover = oldProps.items !== newProps.items && !!newProps.open;
18
+
19
+ render();
20
+
21
+ if (shouldRefreshPopover) {
22
+ refs?.popover?.refreshContent?.();
23
+ }
24
+ }
25
+
14
26
  export const handleClosePopover = (deps, payload) => {
15
27
  const { dispatchEvent } = deps;
16
28
  dispatchEvent(new CustomEvent('close'));
@@ -44,6 +44,34 @@ propsSchema:
44
44
  type: string
45
45
  place:
46
46
  type: string
47
+ overlay:
48
+ type: boolean
49
+ no-overlay:
50
+ type: boolean
51
+ sm-place:
52
+ type: string
53
+ md-place:
54
+ type: string
55
+ lg-place:
56
+ type: string
57
+ xl-place:
58
+ type: string
59
+ sm-overlay:
60
+ type: boolean
61
+ md-overlay:
62
+ type: boolean
63
+ lg-overlay:
64
+ type: boolean
65
+ xl-overlay:
66
+ type: boolean
67
+ sm-no-overlay:
68
+ type: boolean
69
+ md-no-overlay:
70
+ type: boolean
71
+ lg-no-overlay:
72
+ type: boolean
73
+ xl-no-overlay:
74
+ type: boolean
47
75
  w:
48
76
  type: string
49
77
  h:
@@ -3,6 +3,38 @@ export const createInitialState = () => Object.freeze({
3
3
  });
4
4
 
5
5
  const escapeAttrValue = (value) => `${value}`.replace(/"/g, '"');
6
+ const POPOVER_ATTR_PROPS = [
7
+ ["overlay", "overlay"],
8
+ ["noOverlay", "no-overlay"],
9
+ ["smPlace", "sm-place"],
10
+ ["mdPlace", "md-place"],
11
+ ["lgPlace", "lg-place"],
12
+ ["xlPlace", "xl-place"],
13
+ ["smOverlay", "sm-overlay"],
14
+ ["mdOverlay", "md-overlay"],
15
+ ["lgOverlay", "lg-overlay"],
16
+ ["xlOverlay", "xl-overlay"],
17
+ ["smNoOverlay", "sm-no-overlay"],
18
+ ["mdNoOverlay", "md-no-overlay"],
19
+ ["lgNoOverlay", "lg-no-overlay"],
20
+ ["xlNoOverlay", "xl-no-overlay"],
21
+ ];
22
+
23
+ const stringifyPopoverAttrs = (props = {}) => {
24
+ return POPOVER_ATTR_PROPS
25
+ .filter(([propName]) => props[propName] !== undefined && props[propName] !== null)
26
+ .map(([propName, attrName]) => {
27
+ const value = props[propName];
28
+
29
+ if (value === true) {
30
+ return attrName;
31
+ }
32
+
33
+ return `${attrName}="${escapeAttrValue(value)}"`;
34
+ })
35
+ .join(" ");
36
+ };
37
+
6
38
  const getItemType = (item = {}) => {
7
39
  if (item.type === 'section' || item.type === 'label') {
8
40
  return 'section';
@@ -76,5 +108,6 @@ export const selectViewData = ({ props }) => {
76
108
  w: props.w || '300',
77
109
  h: props.h || '300',
78
110
  place: props.place || 'bs',
111
+ popoverAttrString: stringifyPopoverAttrs(props),
79
112
  };
80
113
  }
@@ -18,7 +18,7 @@ styles:
18
18
  height: 16px
19
19
  flex-shrink: 0
20
20
  template:
21
- - rtgl-popover#popover ?open=${open} x=${x} y=${y} place=${place} content-w=${w} content-h=${h} content-sv=true content-g=xs content-pv=sm:
21
+ - 'rtgl-popover#popover ?open=${open} x=${x} y=${y} place=${place} ${popoverAttrString} content-w=${w} content-h=${h} content-sv=true content-g=xs content-pv=sm':
22
22
  - $for item, i in items:
23
23
  - $if item.isSection:
24
24
  - rtgl-view w=f p=md:
@@ -308,6 +308,9 @@ export const handleShowToast = (deps, payload) => {
308
308
  * @param {number} payload.x - X coordinate position (required)
309
309
  * @param {number} payload.y - Y coordinate position (required)
310
310
  * @param {string} [payload.place] - Dropdown menu place token (default: "bs")
311
+ * @param {string} [payload.mdPlace] - Responsive place token, for example "center" on mobile
312
+ * @param {boolean} [payload.overlay] - Whether to show a dialog-style dim overlay
313
+ * @param {boolean} [payload.mdOverlay] - Responsive overlay flag for mobile breakpoints
311
314
  * @returns {Promise<Object|null>} Promise that resolves with clicked item info or null if closed without selection
312
315
  * @returns {Object} [result.index] - Index of the clicked item
313
316
  * @returns {Object} [result.item] - The clicked item object
@@ -3,6 +3,22 @@ const VALID_TOAST_SIZES = new Set(["sm", "md", "lg"]);
3
3
  const VALID_TOAST_PHASES = new Set(["active", "exiting"]);
4
4
  const VALID_TOAST_POSITIONS = new Set(["top", "bottom"]);
5
5
  const VALID_COMPONENT_DIALOG_ROLES = new Set(["confirm", "cancel"]);
6
+ const DROPDOWN_POPOVER_ATTR_PROPS = [
7
+ ["overlay", "overlay"],
8
+ ["noOverlay", "no-overlay"],
9
+ ["smPlace", "sm-place"],
10
+ ["mdPlace", "md-place"],
11
+ ["lgPlace", "lg-place"],
12
+ ["xlPlace", "xl-place"],
13
+ ["smOverlay", "sm-overlay"],
14
+ ["mdOverlay", "md-overlay"],
15
+ ["lgOverlay", "lg-overlay"],
16
+ ["xlOverlay", "xl-overlay"],
17
+ ["smNoOverlay", "sm-no-overlay"],
18
+ ["mdNoOverlay", "md-no-overlay"],
19
+ ["lgNoOverlay", "lg-no-overlay"],
20
+ ["xlNoOverlay", "xl-no-overlay"],
21
+ ];
6
22
 
7
23
  const DEFAULT_COMPONENT_DIALOG_BUTTONS = Object.freeze([
8
24
  {
@@ -28,6 +44,43 @@ const normalizeObject = (value) => {
28
44
  return value;
29
45
  };
30
46
 
47
+ const escapeAttrValue = (value) => `${value}`.replace(/"/g, '&quot;');
48
+
49
+ const readResponsiveOptionValue = (source = {}, propName, attrName) => {
50
+ if (source[propName] !== undefined) {
51
+ return source[propName];
52
+ }
53
+
54
+ return source[attrName];
55
+ };
56
+
57
+ const collectDropdownPopoverOptions = (source = {}) => {
58
+ return DROPDOWN_POPOVER_ATTR_PROPS.reduce((acc, [propName, attrName]) => {
59
+ const value = readResponsiveOptionValue(source, propName, attrName);
60
+
61
+ if (value !== undefined && value !== null) {
62
+ acc[propName] = value;
63
+ }
64
+
65
+ return acc;
66
+ }, {});
67
+ };
68
+
69
+ const stringifyDropdownPopoverAttrs = (source = {}) => {
70
+ return DROPDOWN_POPOVER_ATTR_PROPS
71
+ .filter(([propName]) => source[propName] !== undefined && source[propName] !== null)
72
+ .map(([propName, attrName]) => {
73
+ const value = source[propName];
74
+
75
+ if (value === true) {
76
+ return attrName;
77
+ }
78
+
79
+ return `${attrName}="${escapeAttrValue(value)}"`;
80
+ })
81
+ .join(" ");
82
+ };
83
+
31
84
  const normalizeDialogSize = (value, fallback = "md") => {
32
85
  return VALID_DIALOG_SIZES.has(value) ? value : fallback;
33
86
  };
@@ -172,6 +225,7 @@ export const setDropdownConfig = ({ state }, options = {}) => {
172
225
  x: options.x ?? 0,
173
226
  y: options.y ?? 0,
174
227
  place: options.place ?? "bs",
228
+ ...collectDropdownPopoverOptions(options),
175
229
  };
176
230
  state.uiType = "dropdown";
177
231
  state.isOpen = true;
@@ -301,6 +355,7 @@ export const selectViewData = ({ state }) => {
301
355
  x: state.dropdownConfig?.x ?? 0,
302
356
  y: state.dropdownConfig?.y ?? 0,
303
357
  place: state.dropdownConfig?.place ?? "bs",
358
+ popoverAttrString: stringifyDropdownPopoverAttrs(state.dropdownConfig),
304
359
  },
305
360
  formDialogConfig: {
306
361
  form: state.formDialogConfig?.form ?? { fields: [], actions: { buttons: [] } },
@@ -74,6 +74,9 @@ styles:
74
74
  .toast-card:
75
75
  animation: none
76
76
  transition: none
77
+ '@media only screen and (max-width: 768px)':
78
+ .toast-card:
79
+ width: calc(100vw - 2 * var(--spacing-lg))
77
80
  template:
78
81
  - $if topToasts.length > 0:
79
82
  - rtgl-view class="toast-layer toast-layer-top" pos=fix edge=t z=2100 w=f ah=c g=sm ph=md pt=xl:
@@ -117,4 +120,4 @@ template:
117
120
  - $if config.mode == 'confirm':
118
121
  - rtgl-button#cancelButton v=se: ${config.cancelText}
119
122
  - rtgl-button#confirmButton v=pr: ${config.confirmText}
120
- - rtgl-dropdown-menu#dropdownMenu ?open=${isDropdownOpen} x=${dropdownConfig.x} y=${dropdownConfig.y} place=${dropdownConfig.place} :items=${dropdownConfig.items} key=dropdown-${isDropdownOpen}: null
123
+ - 'rtgl-dropdown-menu#dropdownMenu ?open=${isDropdownOpen} x=${dropdownConfig.x} y=${dropdownConfig.y} place=${dropdownConfig.place} ${dropdownConfig.popoverAttrString} :items=${dropdownConfig.items} key=dropdown-${isDropdownOpen}': null
@@ -149,6 +149,9 @@ const createGlobalUI = (globalUIElement) => {
149
149
  * @param {number} options.x - X coordinate position (required)
150
150
  * @param {number} options.y - Y coordinate position (required)
151
151
  * @param {string} [options.place] - Dropdown menu place token (default: "bs")
152
+ * @param {string} [options.mdPlace] - Responsive place token, for example "center" on mobile
153
+ * @param {boolean} [options.overlay] - Whether to show a dialog-style dim overlay
154
+ * @param {boolean} [options.mdOverlay] - Responsive overlay flag for mobile breakpoints
152
155
  * @returns {Promise<Object|null>} Promise that resolves with clicked item info or null if closed without selection
153
156
  * @returns {Object} [result.index] - Index of the clicked item
154
157
  * @returns {Object} [result.item] - The clicked item object
@@ -1,7 +1,47 @@
1
- import { css, mediaQueries } from "../common.js";
1
+ import {
2
+ css,
3
+ mediaQueries,
4
+ getResponsiveAttribute,
5
+ permutateBreakpoints,
6
+ } from "../common.js";
2
7
 
3
8
  const MIN_MARGIN_PX = 40;
4
9
  const MAX_LAYOUT_RETRIES = 6;
10
+ const RESPONSIVE_LAYOUT_SIZES = ["sm", "md", "lg", "xl"];
11
+ const CLOSE_BUTTON_SIZE_PX = 32;
12
+ const CLOSE_BUTTON_OFFSET_PX = 8;
13
+ const ACTIVE_LAYOUT_ATTR = "data-rtgl-active-layout";
14
+ const FIXED_LAYOUT_SELECTOR = `:host([${ACTIVE_LAYOUT_ATTR}="fixed"])`;
15
+
16
+ const mediaQueryCondition = (mediaQuery) => mediaQuery.replace(/^@media\s+/, "");
17
+ const fixedLayoutStyle = (selector) => css`
18
+ ${selector} dialog {
19
+ width: 100vw !important;
20
+ max-width: 100vw !important;
21
+ height: 100vh !important;
22
+ height: 100dvh !important;
23
+ max-height: 100vh !important;
24
+ max-height: 100dvh !important;
25
+ margin: 0 !important;
26
+ overflow: hidden !important;
27
+ }
28
+
29
+ ${selector} slot[name="content"] {
30
+ box-sizing: border-box;
31
+ width: 100vw !important;
32
+ max-width: 100vw !important;
33
+ height: 100vh !important;
34
+ height: 100dvh !important;
35
+ max-height: 100vh !important;
36
+ max-height: 100dvh !important;
37
+ margin: 0 !important;
38
+ overflow-y: auto !important;
39
+ overscroll-behavior: contain;
40
+ border-radius: 0;
41
+ padding-top: max(var(--spacing-lg), env(safe-area-inset-top));
42
+ padding-bottom: max(var(--spacing-lg), env(safe-area-inset-bottom));
43
+ }
44
+ `;
5
45
 
6
46
  class RettangoliDialogElement extends HTMLElement {
7
47
  static styleSheet = null;
@@ -15,6 +55,7 @@ class RettangoliDialogElement extends HTMLElement {
15
55
  }
16
56
 
17
57
  dialog {
58
+ position: relative;
18
59
  padding: 0;
19
60
  border: none;
20
61
  background: transparent;
@@ -47,6 +88,64 @@ class RettangoliDialogElement extends HTMLElement {
47
88
  margin-bottom: 40px;
48
89
  }
49
90
 
91
+ .close-button {
92
+ align-items: center;
93
+ appearance: none;
94
+ background: transparent;
95
+ border: none;
96
+ border-radius: var(--border-radius-sm);
97
+ box-shadow: none;
98
+ color: var(--muted-foreground);
99
+ cursor: pointer;
100
+ display: none;
101
+ height: ${CLOSE_BUTTON_SIZE_PX}px;
102
+ justify-content: center;
103
+ left: var(--rtgl-dialog-close-left, auto);
104
+ padding: 0;
105
+ position: absolute;
106
+ top: var(--rtgl-dialog-close-top, ${CLOSE_BUTTON_OFFSET_PX}px);
107
+ width: ${CLOSE_BUTTON_SIZE_PX}px;
108
+ z-index: 1;
109
+ -webkit-appearance: none;
110
+ }
111
+
112
+ :host([close-button]) .close-button {
113
+ display: flex;
114
+ }
115
+
116
+ .close-button[hidden] {
117
+ display: none;
118
+ }
119
+
120
+ .close-button:hover {
121
+ background: var(--accent);
122
+ color: var(--foreground);
123
+ }
124
+
125
+ .close-button:focus-visible {
126
+ background: var(--accent);
127
+ color: var(--foreground);
128
+ outline: none;
129
+ }
130
+
131
+ .close-button::before,
132
+ .close-button::after {
133
+ background: currentColor;
134
+ border-radius: 999px;
135
+ content: "";
136
+ height: 16px;
137
+ position: absolute;
138
+ width: 2px;
139
+ }
140
+
141
+ .close-button::before {
142
+ transform: rotate(45deg);
143
+ }
144
+
145
+ .close-button::after {
146
+ transform: rotate(-45deg);
147
+ }
148
+
50
149
  /* Size attribute styles */
51
150
  :host([s="sm"]) slot[name="content"] {
52
151
  width: 33vw;
@@ -91,11 +190,25 @@ class RettangoliDialogElement extends HTMLElement {
91
190
  animation: dialog-in 150ms cubic-bezier(0.16, 1, 0.3, 1);
92
191
  }
93
192
 
193
+ dialog[open] .close-button {
194
+ animation: dialog-in 150ms cubic-bezier(0.16, 1, 0.3, 1);
195
+ }
196
+
94
197
  @media (prefers-reduced-motion: reduce) {
95
- dialog[open] slot[name="content"] {
198
+ dialog[open] slot[name="content"],
199
+ dialog[open] .close-button {
96
200
  animation: none;
97
201
  }
98
202
  }
203
+
204
+ ${fixedLayoutStyle(FIXED_LAYOUT_SELECTOR)}
205
+
206
+ :host([no-padding]) slot[name="content"] {
207
+ margin-left: 0;
208
+ margin-right: 0;
209
+ max-width: 100vw;
210
+ padding: 0;
211
+ }
99
212
  `);
100
213
  }
101
214
  }
@@ -108,10 +221,12 @@ class RettangoliDialogElement extends HTMLElement {
108
221
 
109
222
  // Create dialog element
110
223
  this._dialogElement = document.createElement('dialog');
224
+ this._dialogElement.tabIndex = -1;
111
225
  this.shadow.appendChild(this._dialogElement);
112
226
 
113
227
  // Store reference for content slot
114
228
  this._slotElement = null;
229
+ this._closeButtonElement = null;
115
230
  this._isConnected = false;
116
231
  this._adaptiveFrameId = null;
117
232
  this._layoutRetryCount = 0;
@@ -133,8 +248,17 @@ class RettangoliDialogElement extends HTMLElement {
133
248
  this._scheduleAdaptiveCentering({ resetRetries: true });
134
249
  };
135
250
  this._onWindowResize = () => {
251
+ this._updateActiveLayoutAttribute();
136
252
  this._scheduleAdaptiveCentering({ resetRetries: true });
137
253
  };
254
+ this._onDialogScroll = () => {
255
+ this._updateCloseButtonPosition();
256
+ };
257
+ this._onCloseButtonClick = (e) => {
258
+ e.preventDefault();
259
+ e.stopPropagation();
260
+ this._attemptClose();
261
+ };
138
262
 
139
263
  // Track if mouse down occurred inside dialog content
140
264
  this._mouseDownInContent = false;
@@ -178,7 +302,13 @@ class RettangoliDialogElement extends HTMLElement {
178
302
  }
179
303
 
180
304
  static get observedAttributes() {
181
- return ["open", "w", "s"];
305
+ return [
306
+ "open",
307
+ "w",
308
+ "s",
309
+ "close-button",
310
+ ...permutateBreakpoints(["layout"]),
311
+ ];
182
312
  }
183
313
 
184
314
  connectedCallback() {
@@ -209,16 +339,22 @@ class RettangoliDialogElement extends HTMLElement {
209
339
  } else if (newValue === null && this._dialogElement.open) {
210
340
  this._hideModal();
211
341
  }
212
- } else if (name === 's') {
342
+ } else if (name === 's' || name.endsWith('layout')) {
213
343
  // Size is handled via CSS :host() selectors.
344
+ this._updateActiveLayoutAttribute();
214
345
  this._scheduleAdaptiveCentering({ resetRetries: true });
215
346
  } else if (name === 'w') {
216
347
  this._updateWidth();
348
+ } else if (name === 'close-button') {
349
+ this._updateCloseButton();
350
+ this._updateCloseButtonPosition();
217
351
  }
218
352
  }
219
353
 
220
354
  _updateDialog() {
221
355
  this._updateWidth();
356
+ this._updateCloseButton();
357
+ this._updateActiveLayoutAttribute();
222
358
  }
223
359
 
224
360
  _updateWidth() {
@@ -230,6 +366,22 @@ class RettangoliDialogElement extends HTMLElement {
230
366
  }
231
367
  }
232
368
 
369
+ _updateCloseButton() {
370
+ if (!this._closeButtonElement) {
371
+ return;
372
+ }
373
+ this._closeButtonElement.setAttribute("aria-label", "Close dialog");
374
+ this._closeButtonElement.hidden = !this.hasAttribute("close-button");
375
+ }
376
+
377
+ _createCloseButtonElement() {
378
+ const closeButtonElement = document.createElement("button");
379
+ closeButtonElement.type = "button";
380
+ closeButtonElement.className = "close-button";
381
+ closeButtonElement.addEventListener("click", this._onCloseButtonClick);
382
+ return closeButtonElement;
383
+ }
384
+
233
385
  _clearManagedLongTokenWrapping() {
234
386
  for (const textElement of this._managedLongTokenTextElements) {
235
387
  if (textElement?.isConnected) {
@@ -292,13 +444,23 @@ class RettangoliDialogElement extends HTMLElement {
292
444
  this._slotElement.addEventListener('slotchange', this._onSlotChange);
293
445
  this._dialogElement.appendChild(this._slotElement);
294
446
  }
447
+ if (!this._closeButtonElement) {
448
+ this._closeButtonElement = this._createCloseButtonElement();
449
+ this._dialogElement.appendChild(this._closeButtonElement);
450
+ }
451
+ this._updateCloseButton();
295
452
 
453
+ this._updateActiveLayoutAttribute();
296
454
  this._dialogElement.showModal();
455
+ if (this.shadow.activeElement === this._closeButtonElement) {
456
+ this._dialogElement.focus({ preventScroll: true });
457
+ }
297
458
 
298
459
  // Reset scroll position
299
460
  this._dialogElement.scrollTop = 0;
300
461
 
301
462
  window.addEventListener("resize", this._onWindowResize);
463
+ this._dialogElement.addEventListener("scroll", this._onDialogScroll, { passive: true });
302
464
  this._syncLongTokenWrapping();
303
465
  this._observeAssignedContent();
304
466
  this._layoutRetryCount = 0;
@@ -324,9 +486,16 @@ class RettangoliDialogElement extends HTMLElement {
324
486
  this._dialogElement.removeChild(this._slotElement);
325
487
  this._slotElement = null;
326
488
  }
489
+ if (this._closeButtonElement) {
490
+ this._closeButtonElement.removeEventListener("click", this._onCloseButtonClick);
491
+ this._dialogElement.removeChild(this._closeButtonElement);
492
+ this._closeButtonElement = null;
493
+ }
327
494
 
328
495
  // Reset dialog height
329
496
  this._dialogElement.style.height = '';
497
+ this._dialogElement.style.removeProperty("--rtgl-dialog-close-top");
498
+ this._dialogElement.style.removeProperty("--rtgl-dialog-close-left");
330
499
 
331
500
  // Don't emit any event when programmatically closed via attribute
332
501
  }
@@ -339,6 +508,7 @@ class RettangoliDialogElement extends HTMLElement {
339
508
  }
340
509
  this._layoutRetryCount = 0;
341
510
  window.removeEventListener("resize", this._onWindowResize);
511
+ this._dialogElement.removeEventListener("scroll", this._onDialogScroll);
342
512
  if (this._resizeObserver && this._observedContentElement) {
343
513
  this._resizeObserver.unobserve(this._observedContentElement);
344
514
  }
@@ -405,11 +575,84 @@ class RettangoliDialogElement extends HTMLElement {
405
575
  });
406
576
  }
407
577
 
578
+ _getActiveResponsiveSize() {
579
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
580
+ return "default";
581
+ }
582
+
583
+ for (const size of RESPONSIVE_LAYOUT_SIZES) {
584
+ const query = mediaQueries[size];
585
+ if (query && window.matchMedia(mediaQueryCondition(query)).matches) {
586
+ return size;
587
+ }
588
+ }
589
+
590
+ return "default";
591
+ }
592
+
593
+ _getActiveLayout() {
594
+ return getResponsiveAttribute({
595
+ element: this,
596
+ size: this._getActiveResponsiveSize(),
597
+ attr: "layout",
598
+ }) || "centered";
599
+ }
600
+
601
+ _isFixedLayoutActive() {
602
+ return this._updateActiveLayoutAttribute() === "fixed";
603
+ }
604
+
605
+ _updateActiveLayoutAttribute() {
606
+ const activeLayout = this._getActiveLayout();
607
+ if (activeLayout === "fixed") {
608
+ if (this.getAttribute(ACTIVE_LAYOUT_ATTR) !== "fixed") {
609
+ this.setAttribute(ACTIVE_LAYOUT_ATTR, "fixed");
610
+ }
611
+ } else if (this.hasAttribute(ACTIVE_LAYOUT_ATTR)) {
612
+ this.removeAttribute(ACTIVE_LAYOUT_ATTR);
613
+ }
614
+ return activeLayout;
615
+ }
616
+
617
+ _updateCloseButtonPosition() {
618
+ if (!this._slotElement || !this._closeButtonElement || !this._dialogElement.open) {
619
+ return;
620
+ }
621
+
622
+ const slotWidth = Math.round(this._slotElement.getBoundingClientRect().width);
623
+ const scrollTop = Math.round(this._dialogElement.scrollTop);
624
+ const scrollLeft = Math.round(this._dialogElement.scrollLeft);
625
+ const top = Math.max(
626
+ 0,
627
+ Math.round(this._slotElement.offsetTop) + scrollTop + CLOSE_BUTTON_OFFSET_PX,
628
+ );
629
+ const left = Math.max(
630
+ 0,
631
+ Math.round(this._slotElement.offsetLeft) +
632
+ scrollLeft +
633
+ slotWidth -
634
+ CLOSE_BUTTON_SIZE_PX -
635
+ CLOSE_BUTTON_OFFSET_PX,
636
+ );
637
+
638
+ this._dialogElement.style.setProperty("--rtgl-dialog-close-top", `${top}px`);
639
+ this._dialogElement.style.setProperty("--rtgl-dialog-close-left", `${left}px`);
640
+ }
641
+
408
642
  _applyAdaptiveCentering() {
409
643
  if (!this._slotElement || !this._dialogElement.open) {
410
644
  return;
411
645
  }
412
646
 
647
+ if (this._isFixedLayoutActive()) {
648
+ this._slotElement.style.marginTop = '';
649
+ this._slotElement.style.marginBottom = '';
650
+ this._dialogElement.style.height = '';
651
+ this._updateCloseButtonPosition();
652
+ this._layoutRetryCount = 0;
653
+ return;
654
+ }
655
+
413
656
  this._observeAssignedContent();
414
657
  const contentElement = this._getAssignedContentElement();
415
658
  const contentHeight = contentElement
@@ -431,6 +674,7 @@ class RettangoliDialogElement extends HTMLElement {
431
674
  this._slotElement.style.marginTop = `${MIN_MARGIN_PX}px`;
432
675
  this._slotElement.style.marginBottom = `${MIN_MARGIN_PX}px`;
433
676
  this._dialogElement.style.height = '100vh';
677
+ this._updateCloseButtonPosition();
434
678
  return;
435
679
  }
436
680
 
@@ -439,6 +683,7 @@ class RettangoliDialogElement extends HTMLElement {
439
683
  this._slotElement.style.marginTop = `${margin}px`;
440
684
  this._slotElement.style.marginBottom = `${margin}px`;
441
685
  this._dialogElement.style.height = 'auto';
686
+ this._updateCloseButtonPosition();
442
687
  }
443
688
 
444
689