@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/dist/rettangoli-iife-layout.min.js +164 -60
- package/dist/rettangoli-iife-ui.min.js +186 -82
- package/package.json +2 -2
- package/src/common/popover.js +5 -0
- package/src/components/dropdownMenu/dropdownMenu.handlers.js +12 -0
- package/src/components/dropdownMenu/dropdownMenu.schema.yaml +28 -0
- package/src/components/dropdownMenu/dropdownMenu.store.js +33 -0
- package/src/components/dropdownMenu/dropdownMenu.view.yaml +1 -1
- package/src/components/globalUi/globalUi.handlers.js +3 -0
- package/src/components/globalUi/globalUi.store.js +55 -0
- package/src/components/globalUi/globalUi.view.yaml +4 -1
- package/src/deps/createGlobalUI.js +3 -0
- package/src/primitives/dialog.js +249 -4
- package/src/primitives/popover.js +155 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rettangoli/ui",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
65
|
+
"@rettangoli/fe": "1.2.0",
|
|
66
66
|
"jempl": "1.0.1"
|
|
67
67
|
}
|
|
68
68
|
}
|
package/src/common/popover.js
CHANGED
|
@@ -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, '"');
|
|
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
|
package/src/primitives/dialog.js
CHANGED
|
@@ -1,7 +1,47 @@
|
|
|
1
|
-
import {
|
|
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 [
|
|
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
|
|