@rettangoli/ui 1.7.3 → 1.7.5
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/dist/rettangoli-iife-layout.min.js +54 -44
- package/dist/rettangoli-iife-ui.min.js +72 -62
- package/package.json +1 -1
- package/src/common/popover.js +74 -0
- package/src/components/form/form.view.yaml +1 -1
- package/src/components/select/select.handlers.js +11 -1
- package/src/components/tagSelect/tagSelect.handlers.js +1 -1
- package/src/components/tagSelect/tagSelect.schema.yaml +2 -0
- package/src/components/tagSelect/tagSelect.store.js +2 -1
- package/src/primitives/dialog.js +14 -3
- package/src/primitives/popover.js +117 -81
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const calculatePopoverPosition = ({
|
|
2
|
+
x,
|
|
3
|
+
y,
|
|
4
|
+
width,
|
|
5
|
+
height,
|
|
6
|
+
place,
|
|
7
|
+
viewportWidth,
|
|
8
|
+
viewportHeight,
|
|
9
|
+
offset = 8,
|
|
10
|
+
padding = 8,
|
|
11
|
+
}) => {
|
|
12
|
+
let left = x;
|
|
13
|
+
let top = y;
|
|
14
|
+
|
|
15
|
+
switch (place) {
|
|
16
|
+
case "t":
|
|
17
|
+
left = x - width / 2;
|
|
18
|
+
top = y - height - offset;
|
|
19
|
+
break;
|
|
20
|
+
case "ts":
|
|
21
|
+
left = x;
|
|
22
|
+
top = y - height - offset;
|
|
23
|
+
break;
|
|
24
|
+
case "te":
|
|
25
|
+
left = x - width;
|
|
26
|
+
top = y - height - offset;
|
|
27
|
+
break;
|
|
28
|
+
case "r":
|
|
29
|
+
left = x + offset;
|
|
30
|
+
top = y - height / 2;
|
|
31
|
+
break;
|
|
32
|
+
case "rs":
|
|
33
|
+
left = x + offset;
|
|
34
|
+
top = y;
|
|
35
|
+
break;
|
|
36
|
+
case "re":
|
|
37
|
+
left = x + offset;
|
|
38
|
+
top = y - height;
|
|
39
|
+
break;
|
|
40
|
+
case "b":
|
|
41
|
+
left = x - width / 2;
|
|
42
|
+
top = y + offset;
|
|
43
|
+
break;
|
|
44
|
+
case "bs":
|
|
45
|
+
left = x;
|
|
46
|
+
top = y + offset;
|
|
47
|
+
break;
|
|
48
|
+
case "be":
|
|
49
|
+
left = x - width;
|
|
50
|
+
top = y + offset;
|
|
51
|
+
break;
|
|
52
|
+
case "l":
|
|
53
|
+
left = x - width - offset;
|
|
54
|
+
top = y - height / 2;
|
|
55
|
+
break;
|
|
56
|
+
case "ls":
|
|
57
|
+
left = x - width - offset;
|
|
58
|
+
top = y;
|
|
59
|
+
break;
|
|
60
|
+
case "le":
|
|
61
|
+
left = x - width - offset;
|
|
62
|
+
top = y - height;
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
left = x;
|
|
66
|
+
top = y + offset;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
left: Math.max(padding, Math.min(left, viewportWidth - width - padding)),
|
|
72
|
+
top: Math.max(padding, Math.min(top, viewportHeight - height - padding)),
|
|
73
|
+
};
|
|
74
|
+
};
|
|
@@ -69,7 +69,7 @@ template:
|
|
|
69
69
|
- $if field.type == "select":
|
|
70
70
|
- rtgl-select#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} ?no-clear=${field.noClear} :selectedValue=${field._selectedValue} :placeholder=${field.placeholder} ?disabled=${field._disabled}: null
|
|
71
71
|
- $if field.type == "tag-select":
|
|
72
|
-
- rtgl-tag-select#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} :selectedValues=${field._selectedValues} :placeholder=${field.placeholder} :addOption=${field.addOption} ?disabled=${field._disabled}: null
|
|
72
|
+
- rtgl-tag-select#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} :selectedValues=${field._selectedValues} :placeholder=${field.placeholder} :addOption=${field.addOption} ?no-add=${field.noAdd} ?disabled=${field._disabled}: null
|
|
73
73
|
- $if field.type == "segmented-control":
|
|
74
74
|
- rtgl-segmented-control#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} ?no-clear=${field.noClear} :selectedValue=${field._selectedValue} :placeholder=${field.placeholder} ?disabled=${field._disabled}: null
|
|
75
75
|
- $if field.type == "color-picker":
|
|
@@ -28,8 +28,9 @@ export const handleBeforeMount = (deps) => {
|
|
|
28
28
|
|
|
29
29
|
export const handleOnUpdate = (deps, payload) => {
|
|
30
30
|
const { oldProps, newProps } = payload;
|
|
31
|
-
const { store, render } = deps;
|
|
31
|
+
const { store, render, refs } = deps;
|
|
32
32
|
let shouldRender = false;
|
|
33
|
+
let shouldRefreshPopover = false;
|
|
33
34
|
|
|
34
35
|
if (!!newProps?.disabled && !oldProps?.disabled) {
|
|
35
36
|
store.closeOptionsPopover({});
|
|
@@ -41,8 +42,17 @@ export const handleOnUpdate = (deps, payload) => {
|
|
|
41
42
|
shouldRender = true;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
if (oldProps.options !== newProps.options) {
|
|
46
|
+
shouldRender = true;
|
|
47
|
+
shouldRefreshPopover = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
if (shouldRender) {
|
|
45
51
|
render();
|
|
52
|
+
|
|
53
|
+
if (shouldRefreshPopover && store.selectState?.().isOpen) {
|
|
54
|
+
refs?.popover?.refreshContent?.();
|
|
55
|
+
}
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
|
|
@@ -341,7 +341,7 @@ export const handleSubmitClick = (deps, payload) => {
|
|
|
341
341
|
|
|
342
342
|
export const handleAddOptionClick = (deps, payload) => {
|
|
343
343
|
const { props, dispatchEvent } = deps;
|
|
344
|
-
if (props.disabled) return;
|
|
344
|
+
if (props.disabled || props.noAdd) return;
|
|
345
345
|
|
|
346
346
|
const event = payload._event;
|
|
347
347
|
event.stopPropagation();
|
|
@@ -12,6 +12,7 @@ const blacklistedProps = [
|
|
|
12
12
|
"draftSelectedValues",
|
|
13
13
|
"onChange",
|
|
14
14
|
"addOption",
|
|
15
|
+
"noAdd",
|
|
15
16
|
"disabled",
|
|
16
17
|
];
|
|
17
18
|
|
|
@@ -231,7 +232,7 @@ export const selectViewData = ({ state, props }) => {
|
|
|
231
232
|
triggerTags,
|
|
232
233
|
triggerCursor: isDisabled ? "not-allowed" : "pointer",
|
|
233
234
|
triggerTabIndex: isDisabled ? -1 : 0,
|
|
234
|
-
showAddOption:
|
|
235
|
+
showAddOption: !isDisabled && !props.noAdd,
|
|
235
236
|
addOptionLabel: props.addOption?.label || "Add tag",
|
|
236
237
|
hasDraftChanges,
|
|
237
238
|
submitDisabled: isDisabled,
|
package/src/primitives/dialog.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { css } from "../common.js";
|
|
1
|
+
import { css, mediaQueries } from "../common.js";
|
|
2
2
|
|
|
3
3
|
const MIN_MARGIN_PX = 40;
|
|
4
4
|
const MAX_LAYOUT_RETRIES = 6;
|
|
@@ -66,6 +66,16 @@ class RettangoliDialogElement extends HTMLElement {
|
|
|
66
66
|
margin-right: 0;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
${mediaQueries.md} {
|
|
70
|
+
:host(:not([w])[s="sm"]) slot[name="content"],
|
|
71
|
+
:host(:not([w])[s="md"]) slot[name="content"],
|
|
72
|
+
:host(:not([w])[s="lg"]) slot[name="content"] {
|
|
73
|
+
box-sizing: border-box;
|
|
74
|
+
width: calc(100vw - 2 * var(--spacing-lg));
|
|
75
|
+
max-width: calc(100vw - 2 * var(--spacing-lg));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
@keyframes dialog-in {
|
|
70
80
|
from {
|
|
71
81
|
opacity: 0;
|
|
@@ -199,10 +209,11 @@ class RettangoliDialogElement extends HTMLElement {
|
|
|
199
209
|
} else if (newValue === null && this._dialogElement.open) {
|
|
200
210
|
this._hideModal();
|
|
201
211
|
}
|
|
212
|
+
} else if (name === 's') {
|
|
213
|
+
// Size is handled via CSS :host() selectors.
|
|
214
|
+
this._scheduleAdaptiveCentering({ resetRetries: true });
|
|
202
215
|
} else if (name === 'w') {
|
|
203
216
|
this._updateWidth();
|
|
204
|
-
} else if (name === 's') {
|
|
205
|
-
// Size is handled via CSS :host() selectors
|
|
206
217
|
}
|
|
207
218
|
}
|
|
208
219
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { css } from "../common.js";
|
|
2
|
+
import { calculatePopoverPosition } from "../common/popover.js";
|
|
2
3
|
|
|
3
4
|
const CONTENT_WRAPPER_ATTR = "data-rtgl-popover-content";
|
|
4
5
|
const DEFAULT_CONTENT_STYLE = "min-width: 200px; max-width: 400px; box-sizing: border-box;";
|
|
@@ -115,6 +116,18 @@ class RettangoliPopoverElement extends HTMLElement {
|
|
|
115
116
|
|
|
116
117
|
// Track if we're open
|
|
117
118
|
this._isOpen = false;
|
|
119
|
+
this._positionFrameId = null;
|
|
120
|
+
this._revealFrameId = null;
|
|
121
|
+
this._positionVersion = 0;
|
|
122
|
+
this._isObservingResize = false;
|
|
123
|
+
this._observedContentWrapper = null;
|
|
124
|
+
this._resizeObserver = typeof ResizeObserver === "function"
|
|
125
|
+
? new ResizeObserver(() => {
|
|
126
|
+
if (this._isOpen) {
|
|
127
|
+
this._schedulePositionUpdate();
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
: null;
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
_emitClose() {
|
|
@@ -153,6 +166,9 @@ class RettangoliPopoverElement extends HTMLElement {
|
|
|
153
166
|
}
|
|
154
167
|
|
|
155
168
|
disconnectedCallback() {
|
|
169
|
+
this._cancelScheduledPositionUpdate();
|
|
170
|
+
this._stopResizeObserver();
|
|
171
|
+
|
|
156
172
|
// Clean up dialog if it's open
|
|
157
173
|
if (this._isOpen && this._dialogElement.open) {
|
|
158
174
|
this._dialogElement.close();
|
|
@@ -170,7 +186,7 @@ class RettangoliPopoverElement extends HTMLElement {
|
|
|
170
186
|
this._hide();
|
|
171
187
|
}
|
|
172
188
|
} else if ((name === 'x' || name === 'y' || name === 'place') && this._isOpen) {
|
|
173
|
-
this.
|
|
189
|
+
this._schedulePositionUpdate();
|
|
174
190
|
} else if (name === 'no-overlay' && oldValue !== newValue && this._isOpen) {
|
|
175
191
|
this._hide();
|
|
176
192
|
this._show();
|
|
@@ -265,7 +281,7 @@ class RettangoliPopoverElement extends HTMLElement {
|
|
|
265
281
|
}
|
|
266
282
|
|
|
267
283
|
if (reposition && this._isOpen) {
|
|
268
|
-
this.
|
|
284
|
+
this._schedulePositionUpdate();
|
|
269
285
|
}
|
|
270
286
|
}
|
|
271
287
|
|
|
@@ -281,6 +297,7 @@ class RettangoliPopoverElement extends HTMLElement {
|
|
|
281
297
|
}
|
|
282
298
|
|
|
283
299
|
this._isOpen = true;
|
|
300
|
+
this._startResizeObserver();
|
|
284
301
|
|
|
285
302
|
// Show the dialog using setTimeout to ensure it's in the DOM
|
|
286
303
|
if (!this._dialogElement.open) {
|
|
@@ -292,19 +309,20 @@ class RettangoliPopoverElement extends HTMLElement {
|
|
|
292
309
|
this._dialogElement.showModal();
|
|
293
310
|
}
|
|
294
311
|
}
|
|
312
|
+
|
|
313
|
+
this._schedulePositionUpdate();
|
|
295
314
|
}, 0);
|
|
315
|
+
} else {
|
|
316
|
+
this._schedulePositionUpdate();
|
|
296
317
|
}
|
|
297
|
-
|
|
298
|
-
// Update position after dialog is shown
|
|
299
|
-
requestAnimationFrame(() => {
|
|
300
|
-
this._updatePosition();
|
|
301
|
-
});
|
|
302
318
|
}
|
|
303
319
|
}
|
|
304
320
|
|
|
305
321
|
_hide() {
|
|
306
322
|
if (this._isOpen) {
|
|
307
323
|
this._isOpen = false;
|
|
324
|
+
this._cancelScheduledPositionUpdate();
|
|
325
|
+
this._stopResizeObserver();
|
|
308
326
|
|
|
309
327
|
// Close the dialog
|
|
310
328
|
if (this._dialogElement.open) {
|
|
@@ -319,18 +337,83 @@ class RettangoliPopoverElement extends HTMLElement {
|
|
|
319
337
|
}
|
|
320
338
|
}
|
|
321
339
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
340
|
+
_startResizeObserver() {
|
|
341
|
+
if (!this._resizeObserver) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!this._isObservingResize) {
|
|
346
|
+
this._resizeObserver.observe(this._popoverContainer);
|
|
347
|
+
this._isObservingResize = true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (this._contentWrapper && this._observedContentWrapper !== this._contentWrapper) {
|
|
351
|
+
if (this._observedContentWrapper) {
|
|
352
|
+
this._resizeObserver.unobserve(this._observedContentWrapper);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
this._resizeObserver.observe(this._contentWrapper);
|
|
356
|
+
this._observedContentWrapper = this._contentWrapper;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
_stopResizeObserver() {
|
|
361
|
+
this._resizeObserver?.disconnect();
|
|
362
|
+
this._isObservingResize = false;
|
|
363
|
+
this._observedContentWrapper = null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
_cancelScheduledPositionUpdate() {
|
|
367
|
+
if (this._positionFrameId !== null) {
|
|
368
|
+
cancelAnimationFrame(this._positionFrameId);
|
|
369
|
+
this._positionFrameId = null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (this._revealFrameId !== null) {
|
|
373
|
+
cancelAnimationFrame(this._revealFrameId);
|
|
374
|
+
this._revealFrameId = null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
this._positionVersion += 1;
|
|
378
|
+
this.removeAttribute('positioned');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
_readCoordinateAttr(name) {
|
|
382
|
+
const value = parseFloat(this.getAttribute(name) || '0');
|
|
383
|
+
return Number.isFinite(value) ? value : 0;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
_schedulePositionUpdate() {
|
|
387
|
+
if (!this._isOpen) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
326
390
|
|
|
327
391
|
// Remove positioned attribute to hide during repositioning
|
|
328
392
|
this.removeAttribute('positioned');
|
|
393
|
+
this._positionVersion += 1;
|
|
394
|
+
|
|
395
|
+
if (this._positionFrameId !== null) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
this._positionFrameId = requestAnimationFrame(() => {
|
|
400
|
+
this._positionFrameId = null;
|
|
401
|
+
|
|
402
|
+
if (!this._isOpen) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!this._dialogElement.open) {
|
|
407
|
+
this._schedulePositionUpdate();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
329
410
|
|
|
330
|
-
// Calculate position based on place
|
|
331
|
-
// We'll position after the popover is rendered to get its dimensions
|
|
332
|
-
requestAnimationFrame(() => {
|
|
333
411
|
this._syncContentWrapper({ reposition: false });
|
|
412
|
+
this._startResizeObserver();
|
|
413
|
+
|
|
414
|
+
const x = this._readCoordinateAttr('x');
|
|
415
|
+
const y = this._readCoordinateAttr('y');
|
|
416
|
+
const place = this.getAttribute('place') || 'bs';
|
|
334
417
|
const rect = this._popoverContainer.getBoundingClientRect();
|
|
335
418
|
const { left, top } = this._calculatePosition(x, y, rect.width, rect.height, place);
|
|
336
419
|
|
|
@@ -339,78 +422,27 @@ class RettangoliPopoverElement extends HTMLElement {
|
|
|
339
422
|
this._popoverContainer.style.top = `${top}px`;
|
|
340
423
|
|
|
341
424
|
// Then make visible in next frame to prevent flicker
|
|
342
|
-
|
|
343
|
-
|
|
425
|
+
const revealVersion = this._positionVersion;
|
|
426
|
+
this._revealFrameId = requestAnimationFrame(() => {
|
|
427
|
+
this._revealFrameId = null;
|
|
428
|
+
|
|
429
|
+
if (this._isOpen && this._positionVersion === revealVersion) {
|
|
430
|
+
this.setAttribute('positioned', '');
|
|
431
|
+
}
|
|
344
432
|
});
|
|
345
433
|
});
|
|
346
434
|
}
|
|
347
435
|
|
|
348
436
|
_calculatePosition(x, y, width, height, place) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
case 'ts':
|
|
359
|
-
left = x;
|
|
360
|
-
top = y - height - offset;
|
|
361
|
-
break;
|
|
362
|
-
case 'te':
|
|
363
|
-
left = x - width;
|
|
364
|
-
top = y - height - offset;
|
|
365
|
-
break;
|
|
366
|
-
case 'r':
|
|
367
|
-
left = x + offset;
|
|
368
|
-
top = y - height / 2;
|
|
369
|
-
break;
|
|
370
|
-
case 'rs':
|
|
371
|
-
left = x + offset;
|
|
372
|
-
top = y;
|
|
373
|
-
break;
|
|
374
|
-
case 're':
|
|
375
|
-
left = x + offset;
|
|
376
|
-
top = y - height;
|
|
377
|
-
break;
|
|
378
|
-
case 'b':
|
|
379
|
-
left = x - width / 2;
|
|
380
|
-
top = y + offset;
|
|
381
|
-
break;
|
|
382
|
-
case 'bs':
|
|
383
|
-
left = x;
|
|
384
|
-
top = y + offset;
|
|
385
|
-
break;
|
|
386
|
-
case 'be':
|
|
387
|
-
left = x - width;
|
|
388
|
-
top = y + offset;
|
|
389
|
-
break;
|
|
390
|
-
case 'l':
|
|
391
|
-
left = x - width - offset;
|
|
392
|
-
top = y - height / 2;
|
|
393
|
-
break;
|
|
394
|
-
case 'ls':
|
|
395
|
-
left = x - width - offset;
|
|
396
|
-
top = y;
|
|
397
|
-
break;
|
|
398
|
-
case 'le':
|
|
399
|
-
left = x - width - offset;
|
|
400
|
-
top = y - height;
|
|
401
|
-
break;
|
|
402
|
-
default:
|
|
403
|
-
left = x;
|
|
404
|
-
top = y + offset;
|
|
405
|
-
break;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Ensure popover stays within viewport
|
|
409
|
-
const padding = 8;
|
|
410
|
-
left = Math.max(padding, Math.min(left, window.innerWidth - width - padding));
|
|
411
|
-
top = Math.max(padding, Math.min(top, window.innerHeight - height - padding));
|
|
412
|
-
|
|
413
|
-
return { left, top };
|
|
437
|
+
return calculatePopoverPosition({
|
|
438
|
+
x,
|
|
439
|
+
y,
|
|
440
|
+
width,
|
|
441
|
+
height,
|
|
442
|
+
place,
|
|
443
|
+
viewportWidth: window.innerWidth,
|
|
444
|
+
viewportHeight: window.innerHeight,
|
|
445
|
+
});
|
|
414
446
|
}
|
|
415
447
|
|
|
416
448
|
|
|
@@ -422,6 +454,10 @@ class RettangoliPopoverElement extends HTMLElement {
|
|
|
422
454
|
get content() {
|
|
423
455
|
return this._contentWrapper;
|
|
424
456
|
}
|
|
457
|
+
|
|
458
|
+
refreshContent() {
|
|
459
|
+
this._syncContentWrapper();
|
|
460
|
+
}
|
|
425
461
|
}
|
|
426
462
|
|
|
427
463
|
// Export factory function to maintain API compatibility
|