@vaadin/master-detail-layout 25.2.0-alpha4 → 25.2.0-alpha6
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/custom-elements.json +220 -0
- package/package.json +8 -8
- package/src/styles/vaadin-master-detail-layout-base-styles.js +22 -16
- package/src/vaadin-master-detail-layout-helpers.js +173 -0
- package/src/vaadin-master-detail-layout.js +95 -258
- package/web-types.json +1 -1
- package/web-types.lit.json +1 -1
package/custom-elements.json
CHANGED
|
@@ -17,6 +17,226 @@
|
|
|
17
17
|
}
|
|
18
18
|
]
|
|
19
19
|
},
|
|
20
|
+
{
|
|
21
|
+
"kind": "javascript-module",
|
|
22
|
+
"path": "src/vaadin-master-detail-layout-helpers.js",
|
|
23
|
+
"declarations": [
|
|
24
|
+
{
|
|
25
|
+
"kind": "function",
|
|
26
|
+
"name": "getCurrentAnimation",
|
|
27
|
+
"parameters": [
|
|
28
|
+
{
|
|
29
|
+
"name": "element",
|
|
30
|
+
"type": {
|
|
31
|
+
"text": "HTMLElement"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"description": "Returns the currently running master-detail-layout animation on the\nelement, if any. Matches by the shared animation ID and `'running'`\nplay state.",
|
|
36
|
+
"return": {
|
|
37
|
+
"type": {
|
|
38
|
+
"text": "Animation | undefined"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"kind": "function",
|
|
44
|
+
"name": "getCurrentAnimationProgress",
|
|
45
|
+
"parameters": [
|
|
46
|
+
{
|
|
47
|
+
"name": "element",
|
|
48
|
+
"type": {
|
|
49
|
+
"text": "HTMLElement"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"description": "Returns the overall progress (0–1) of the current animation on the\nelement, computed as `currentTime / duration`. Returns 0 when no\nanimation is running.",
|
|
54
|
+
"return": {
|
|
55
|
+
"type": {
|
|
56
|
+
"text": "number"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"kind": "function",
|
|
62
|
+
"name": "animateIn",
|
|
63
|
+
"parameters": [
|
|
64
|
+
{
|
|
65
|
+
"name": "element",
|
|
66
|
+
"type": {
|
|
67
|
+
"text": "HTMLElement"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "effects",
|
|
72
|
+
"type": {
|
|
73
|
+
"text": "Array<'fade' | 'slide'>"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "progress",
|
|
78
|
+
"description": "starting progress (0–1) for interrupted resumption",
|
|
79
|
+
"type": {
|
|
80
|
+
"text": "number"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"description": "Runs an enter animation on the element.",
|
|
85
|
+
"return": {
|
|
86
|
+
"type": {
|
|
87
|
+
"text": "Promise<void>"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"kind": "function",
|
|
93
|
+
"name": "animateOut",
|
|
94
|
+
"parameters": [
|
|
95
|
+
{
|
|
96
|
+
"name": "element",
|
|
97
|
+
"type": {
|
|
98
|
+
"text": "HTMLElement"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "effects",
|
|
103
|
+
"type": {
|
|
104
|
+
"text": "Array<'fade' | 'slide'>"
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "progress",
|
|
109
|
+
"description": "starting progress (0–1) for interrupted resumption",
|
|
110
|
+
"type": {
|
|
111
|
+
"text": "number"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"description": "Runs an exit animation on the element.",
|
|
116
|
+
"return": {
|
|
117
|
+
"type": {
|
|
118
|
+
"text": "Promise<void>"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"kind": "function",
|
|
124
|
+
"name": "cancelAnimations",
|
|
125
|
+
"parameters": [
|
|
126
|
+
{
|
|
127
|
+
"name": "element",
|
|
128
|
+
"type": {
|
|
129
|
+
"text": "HTMLElement"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
"description": "Cancels all running animations on the element that match the shared animation ID."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"kind": "function",
|
|
137
|
+
"name": "parseTrackSizes",
|
|
138
|
+
"parameters": [
|
|
139
|
+
{
|
|
140
|
+
"name": "gridTemplate",
|
|
141
|
+
"description": "computed grid template string (e.g. `\"200px [gap] 10px 400px\"`)",
|
|
142
|
+
"type": {
|
|
143
|
+
"text": "string"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"description": "Parses a computed `gridTemplateColumns` / `gridTemplateRows` value\ninto an array of track sizes in pixels. Line names (e.g. `[name]`)\nare stripped before parsing.",
|
|
148
|
+
"return": {
|
|
149
|
+
"type": {
|
|
150
|
+
"text": "number[]"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"kind": "function",
|
|
156
|
+
"name": "detectOverflow",
|
|
157
|
+
"parameters": [
|
|
158
|
+
{
|
|
159
|
+
"name": "hostSize",
|
|
160
|
+
"description": "the host element's width or height in pixels",
|
|
161
|
+
"type": {
|
|
162
|
+
"text": "number"
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"name": "trackSizes",
|
|
167
|
+
"description": "[masterSize, masterExtra, detailSize] in pixels",
|
|
168
|
+
"type": {
|
|
169
|
+
"text": "number[]"
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
],
|
|
173
|
+
"description": "Determines whether the detail area overflows the host element,\nmeaning it should be shown as an overlay instead of side-by-side.\n\nReturns `false` when all tracks fit within the host, or when the\nmaster's extra space (flexible portion) is large enough to absorb\nthe detail column.",
|
|
174
|
+
"return": {
|
|
175
|
+
"type": {
|
|
176
|
+
"text": "boolean"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
],
|
|
181
|
+
"exports": [
|
|
182
|
+
{
|
|
183
|
+
"kind": "js",
|
|
184
|
+
"name": "getCurrentAnimation",
|
|
185
|
+
"declaration": {
|
|
186
|
+
"name": "getCurrentAnimation",
|
|
187
|
+
"module": "src/vaadin-master-detail-layout-helpers.js"
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"kind": "js",
|
|
192
|
+
"name": "getCurrentAnimationProgress",
|
|
193
|
+
"declaration": {
|
|
194
|
+
"name": "getCurrentAnimationProgress",
|
|
195
|
+
"module": "src/vaadin-master-detail-layout-helpers.js"
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"kind": "js",
|
|
200
|
+
"name": "animateIn",
|
|
201
|
+
"declaration": {
|
|
202
|
+
"name": "animateIn",
|
|
203
|
+
"module": "src/vaadin-master-detail-layout-helpers.js"
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"kind": "js",
|
|
208
|
+
"name": "animateOut",
|
|
209
|
+
"declaration": {
|
|
210
|
+
"name": "animateOut",
|
|
211
|
+
"module": "src/vaadin-master-detail-layout-helpers.js"
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"kind": "js",
|
|
216
|
+
"name": "cancelAnimations",
|
|
217
|
+
"declaration": {
|
|
218
|
+
"name": "cancelAnimations",
|
|
219
|
+
"module": "src/vaadin-master-detail-layout-helpers.js"
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"kind": "js",
|
|
224
|
+
"name": "parseTrackSizes",
|
|
225
|
+
"declaration": {
|
|
226
|
+
"name": "parseTrackSizes",
|
|
227
|
+
"module": "src/vaadin-master-detail-layout-helpers.js"
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"kind": "js",
|
|
232
|
+
"name": "detectOverflow",
|
|
233
|
+
"declaration": {
|
|
234
|
+
"name": "detectOverflow",
|
|
235
|
+
"module": "src/vaadin-master-detail-layout-helpers.js"
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
},
|
|
20
240
|
{
|
|
21
241
|
"kind": "javascript-module",
|
|
22
242
|
"path": "src/vaadin-master-detail-layout.js",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/master-detail-layout",
|
|
3
|
-
"version": "25.2.0-
|
|
3
|
+
"version": "25.2.0-alpha6",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -34,16 +34,16 @@
|
|
|
34
34
|
"web-component"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@vaadin/a11y-base": "25.2.0-
|
|
38
|
-
"@vaadin/component-base": "25.2.0-
|
|
39
|
-
"@vaadin/vaadin-themable-mixin": "25.2.0-
|
|
37
|
+
"@vaadin/a11y-base": "25.2.0-alpha6",
|
|
38
|
+
"@vaadin/component-base": "25.2.0-alpha6",
|
|
39
|
+
"@vaadin/vaadin-themable-mixin": "25.2.0-alpha6",
|
|
40
40
|
"lit": "^3.0.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@vaadin/aura": "25.2.0-
|
|
44
|
-
"@vaadin/chai-plugins": "25.2.0-
|
|
43
|
+
"@vaadin/aura": "25.2.0-alpha6",
|
|
44
|
+
"@vaadin/chai-plugins": "25.2.0-alpha6",
|
|
45
45
|
"@vaadin/testing-helpers": "^2.0.0",
|
|
46
|
-
"@vaadin/vaadin-lumo-styles": "25.2.0-
|
|
46
|
+
"@vaadin/vaadin-lumo-styles": "25.2.0-alpha6",
|
|
47
47
|
"sinon": "^21.0.2"
|
|
48
48
|
},
|
|
49
49
|
"customElements": "custom-elements.json",
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
"web-types.json",
|
|
52
52
|
"web-types.lit.json"
|
|
53
53
|
],
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "30f23c65765f27616f2db292406d5759a7e987c3"
|
|
55
55
|
}
|
|
@@ -14,10 +14,10 @@ export const masterDetailLayoutStyles = css`
|
|
|
14
14
|
--_detail-extra: 0px;
|
|
15
15
|
--_detail-cached-size: min-content;
|
|
16
16
|
|
|
17
|
+
--_rtl-multiplier: 1;
|
|
17
18
|
--_transition-duration: 0s;
|
|
18
19
|
--_transition-easing: cubic-bezier(0.78, 0, 0.22, 1);
|
|
19
|
-
--_rtl-multiplier
|
|
20
|
-
--_detail-offscreen: calc(30px * var(--_rtl-multiplier));
|
|
20
|
+
--_transition-offset: calc(30px * var(--_rtl-multiplier));
|
|
21
21
|
|
|
22
22
|
display: grid;
|
|
23
23
|
box-sizing: border-box;
|
|
@@ -42,7 +42,7 @@ export const masterDetailLayoutStyles = css`
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
:host([orientation='vertical']) {
|
|
45
|
-
--
|
|
45
|
+
--_transition-offset: 0 30px;
|
|
46
46
|
|
|
47
47
|
grid-template-columns: 100%;
|
|
48
48
|
grid-template-rows:
|
|
@@ -56,11 +56,14 @@ export const masterDetailLayoutStyles = css`
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
#detail-placeholder {
|
|
59
|
-
|
|
59
|
+
z-index: 1;
|
|
60
|
+
opacity: 0;
|
|
61
|
+
pointer-events: none;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
:host([has-detail-placeholder]:not([has-detail], [overlay])) #detail-placeholder {
|
|
63
|
-
|
|
65
|
+
opacity: 1;
|
|
66
|
+
pointer-events: auto;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
#master {
|
|
@@ -84,9 +87,11 @@ export const masterDetailLayoutStyles = css`
|
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
#backdrop {
|
|
90
|
+
--_transition-easing: linear;
|
|
91
|
+
|
|
87
92
|
position: absolute;
|
|
88
93
|
inset: 0;
|
|
89
|
-
z-index:
|
|
94
|
+
z-index: 2;
|
|
90
95
|
opacity: 0;
|
|
91
96
|
pointer-events: none;
|
|
92
97
|
background: var(--vaadin-overlay-backdrop-background, rgba(0, 0, 0, 0.2));
|
|
@@ -125,32 +130,33 @@ export const masterDetailLayoutStyles = css`
|
|
|
125
130
|
var(--vaadin-master-detail-layout-border-color, var(--vaadin-border-color-secondary));
|
|
126
131
|
}
|
|
127
132
|
|
|
133
|
+
#outgoing {
|
|
134
|
+
position: absolute;
|
|
135
|
+
z-index: 3;
|
|
136
|
+
}
|
|
137
|
+
|
|
128
138
|
/* Detail transition: off-screen by default, on-screen when has-detail */
|
|
129
139
|
#detail {
|
|
130
|
-
translate: var(--
|
|
131
|
-
|
|
140
|
+
translate: var(--_transition-offset);
|
|
141
|
+
opacity: 0;
|
|
142
|
+
z-index: 4;
|
|
132
143
|
}
|
|
133
144
|
|
|
134
145
|
:host([has-detail]) #detail {
|
|
135
146
|
translate: none;
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
#outgoing:not([hidden]) {
|
|
140
|
-
z-index: 1;
|
|
147
|
+
opacity: 1;
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
:host([overlay]) {
|
|
144
|
-
--
|
|
151
|
+
--_transition-offset: calc((100% + 30px) * var(--_rtl-multiplier));
|
|
145
152
|
}
|
|
146
153
|
|
|
147
154
|
:host([overlay][orientation='vertical']) {
|
|
148
|
-
--
|
|
155
|
+
--_transition-offset: 0 calc(100% + 30px);
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
:host([has-detail][overlay]) :is(#detail, #outgoing) {
|
|
152
159
|
position: absolute;
|
|
153
|
-
z-index: 2;
|
|
154
160
|
background: var(--vaadin-master-detail-layout-detail-background, var(--vaadin-background-color));
|
|
155
161
|
box-shadow: var(--vaadin-master-detail-layout-detail-shadow, 0 0 20px 0 rgba(0, 0, 0, 0.3));
|
|
156
162
|
grid-column: none;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2025 - 2026 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const ANIMATION_ID = 'vaadin-master-detail-layout';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reads CSS custom properties from the element that control
|
|
11
|
+
* animation keyframes and timing.
|
|
12
|
+
*
|
|
13
|
+
* @param {HTMLElement} element
|
|
14
|
+
* @return {{ offset: string, easing: string, duration: number }}
|
|
15
|
+
*/
|
|
16
|
+
function getAnimationParams(element) {
|
|
17
|
+
const computedStyle = getComputedStyle(element);
|
|
18
|
+
const offset = computedStyle.getPropertyValue('--_transition-offset');
|
|
19
|
+
const easing = computedStyle.getPropertyValue('--_transition-easing');
|
|
20
|
+
const durationStr = computedStyle.getPropertyValue('--_transition-duration');
|
|
21
|
+
const duration = durationStr.endsWith('ms') ? parseFloat(durationStr) : parseFloat(durationStr) * 1000;
|
|
22
|
+
return { offset, easing, duration };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns the currently running master-detail-layout animation on the
|
|
27
|
+
* element, if any. Matches by the shared animation ID and `'running'`
|
|
28
|
+
* play state.
|
|
29
|
+
*
|
|
30
|
+
* @param {HTMLElement} element
|
|
31
|
+
* @return {Animation | undefined}
|
|
32
|
+
*/
|
|
33
|
+
export function getCurrentAnimation(element) {
|
|
34
|
+
return element
|
|
35
|
+
.getAnimations()
|
|
36
|
+
.find((animation) => animation.id === ANIMATION_ID && animation.playState !== 'finished');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns the overall progress (0–1) of the current animation on the
|
|
41
|
+
* element, computed as `currentTime / duration`. Returns 0 when no
|
|
42
|
+
* animation is running.
|
|
43
|
+
*
|
|
44
|
+
* @param {HTMLElement} element
|
|
45
|
+
* @return {number}
|
|
46
|
+
*/
|
|
47
|
+
export function getCurrentAnimationProgress(element) {
|
|
48
|
+
const animation = getCurrentAnimation(element);
|
|
49
|
+
if (!animation) {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
const currentTime = animation.currentTime;
|
|
53
|
+
if (currentTime == null) {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
return currentTime / animation.effect.getTiming().duration;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Animates the element using the Web Animations API. Cancels any
|
|
61
|
+
* previous animation and resumes from the given progress for a
|
|
62
|
+
* smooth handoff. No-op when CSS params are missing or progress is 1.
|
|
63
|
+
*
|
|
64
|
+
* @param {HTMLElement} element
|
|
65
|
+
* @param {'in' | 'out'} direction
|
|
66
|
+
* @param {Array<'fade' | 'slide'>} effects
|
|
67
|
+
* @param {number} progress starting progress (0–1) for interrupted resumption
|
|
68
|
+
* @return {Promise<void>} resolves when the animation finishes
|
|
69
|
+
*/
|
|
70
|
+
function animate(element, direction, effects, progress) {
|
|
71
|
+
const { offset, easing, duration } = getAnimationParams(element);
|
|
72
|
+
if (!offset || !duration || progress === 1) {
|
|
73
|
+
return Promise.resolve();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const oldAnimation = getCurrentAnimation(element);
|
|
77
|
+
if (oldAnimation) {
|
|
78
|
+
oldAnimation.cancel();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const keyframes = {};
|
|
82
|
+
if (effects.includes('fade')) {
|
|
83
|
+
keyframes.opacity = [0, 1];
|
|
84
|
+
}
|
|
85
|
+
if (effects.includes('slide')) {
|
|
86
|
+
keyframes.translate = [offset, 0];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const newAnimation = element.animate(keyframes, { id: ANIMATION_ID, easing, duration });
|
|
90
|
+
newAnimation.pause();
|
|
91
|
+
newAnimation.currentTime = duration * progress;
|
|
92
|
+
newAnimation.playbackRate = direction === 'in' ? 1 : -1;
|
|
93
|
+
newAnimation.play();
|
|
94
|
+
return newAnimation.finished;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Runs an enter animation on the element.
|
|
99
|
+
*
|
|
100
|
+
* @param {HTMLElement} element
|
|
101
|
+
* @param {Array<'fade' | 'slide'>} effects
|
|
102
|
+
* @param {number} progress starting progress (0–1) for interrupted resumption
|
|
103
|
+
* @return {Promise<void>} resolves when the animation finishes
|
|
104
|
+
*/
|
|
105
|
+
export function animateIn(element, effects, progress) {
|
|
106
|
+
return animate(element, 'in', effects, progress);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Runs an exit animation on the element.
|
|
111
|
+
*
|
|
112
|
+
* @param {HTMLElement} element
|
|
113
|
+
* @param {Array<'fade' | 'slide'>} effects
|
|
114
|
+
* @param {number} progress starting progress (0–1) for interrupted resumption
|
|
115
|
+
* @return {Promise<void>} resolves when the animation finishes
|
|
116
|
+
*/
|
|
117
|
+
export function animateOut(element, effects, progress) {
|
|
118
|
+
return animate(element, 'out', effects, progress);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Cancels all running animations on the element that match the shared animation ID.
|
|
123
|
+
*
|
|
124
|
+
* @param {HTMLElement} element
|
|
125
|
+
*/
|
|
126
|
+
export function cancelAnimations(element) {
|
|
127
|
+
element.getAnimations({ subtree: true }).forEach((animation) => {
|
|
128
|
+
if (animation.id === ANIMATION_ID) {
|
|
129
|
+
animation.cancel();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Parses a computed `gridTemplateColumns` / `gridTemplateRows` value
|
|
136
|
+
* into an array of track sizes in pixels. Line names (e.g. `[name]`)
|
|
137
|
+
* are stripped before parsing.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} gridTemplate computed grid template string (e.g. `"200px [gap] 10px 400px"`)
|
|
140
|
+
* @return {number[]} track sizes in pixels
|
|
141
|
+
*/
|
|
142
|
+
export function parseTrackSizes(gridTemplate) {
|
|
143
|
+
return gridTemplate
|
|
144
|
+
.replace(/\[[^\]]+\]/gu, '')
|
|
145
|
+
.replace(/\s+/gu, ' ')
|
|
146
|
+
.trim()
|
|
147
|
+
.split(' ')
|
|
148
|
+
.map(parseFloat);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Determines whether the detail area overflows the host element,
|
|
153
|
+
* meaning it should be shown as an overlay instead of side-by-side.
|
|
154
|
+
*
|
|
155
|
+
* Returns `false` when all tracks fit within the host, or when the
|
|
156
|
+
* master's extra space (flexible portion) is large enough to absorb
|
|
157
|
+
* the detail column.
|
|
158
|
+
*
|
|
159
|
+
* @param {number} hostSize the host element's width or height in pixels
|
|
160
|
+
* @param {number[]} trackSizes [masterSize, masterExtra, detailSize] in pixels
|
|
161
|
+
* @return {boolean} `true` if the detail overflows and should be overlaid
|
|
162
|
+
*/
|
|
163
|
+
export function detectOverflow(hostSize, trackSizes) {
|
|
164
|
+
const [masterSize, masterExtra, detailSize] = trackSizes;
|
|
165
|
+
|
|
166
|
+
if (Math.floor(masterSize + masterExtra + detailSize) <= Math.floor(hostSize)) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (Math.floor(masterExtra) >= Math.floor(detailSize)) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
@@ -11,27 +11,14 @@ import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
|
11
11
|
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
|
|
12
12
|
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
13
13
|
import { masterDetailLayoutStyles } from './styles/vaadin-master-detail-layout-base-styles.js';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function detectOverflow(hostSize, trackSizes) {
|
|
25
|
-
const [masterSize, masterExtra, detailSize] = trackSizes;
|
|
26
|
-
|
|
27
|
-
if (Math.floor(masterSize + masterExtra + detailSize) <= Math.floor(hostSize)) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
if (Math.floor(masterExtra) >= Math.floor(detailSize)) {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
14
|
+
import {
|
|
15
|
+
animateIn,
|
|
16
|
+
animateOut,
|
|
17
|
+
cancelAnimations,
|
|
18
|
+
detectOverflow,
|
|
19
|
+
getCurrentAnimationProgress,
|
|
20
|
+
parseTrackSizes,
|
|
21
|
+
} from './vaadin-master-detail-layout-helpers.js';
|
|
35
22
|
|
|
36
23
|
/**
|
|
37
24
|
* `<vaadin-master-detail-layout>` is a web component for building UIs with a master
|
|
@@ -274,7 +261,7 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
274
261
|
super.disconnectedCallback();
|
|
275
262
|
this.__resizeObserver.disconnect();
|
|
276
263
|
cancelAnimationFrame(this.__resizeRaf);
|
|
277
|
-
this
|
|
264
|
+
cancelAnimations(this);
|
|
278
265
|
}
|
|
279
266
|
|
|
280
267
|
/** @private */
|
|
@@ -507,282 +494,130 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
507
494
|
* @return {Promise<void>}
|
|
508
495
|
* @protected
|
|
509
496
|
*/
|
|
510
|
-
_setDetail(
|
|
497
|
+
async _setDetail(newDetail, skipTransition) {
|
|
511
498
|
// Don't start a transition if detail didn't change
|
|
512
|
-
const
|
|
513
|
-
if ((
|
|
514
|
-
return
|
|
499
|
+
const oldDetail = this.querySelector('[slot="detail"]');
|
|
500
|
+
if (oldDetail === (newDetail || null)) {
|
|
501
|
+
return;
|
|
515
502
|
}
|
|
516
503
|
|
|
517
|
-
const updateSlot = () => {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
// Add new content
|
|
521
|
-
if (element) {
|
|
522
|
-
element.setAttribute('slot', 'detail');
|
|
523
|
-
this.appendChild(element);
|
|
504
|
+
const updateSlot = async () => {
|
|
505
|
+
if (oldDetail && oldDetail.slot === 'detail') {
|
|
506
|
+
oldDetail.remove();
|
|
524
507
|
}
|
|
525
|
-
};
|
|
526
508
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
509
|
+
if (newDetail) {
|
|
510
|
+
newDetail.setAttribute('slot', 'detail');
|
|
511
|
+
this.appendChild(newDetail);
|
|
512
|
+
}
|
|
532
513
|
|
|
533
|
-
|
|
514
|
+
// Wait for Lit elements to render
|
|
515
|
+
await Promise.resolve();
|
|
534
516
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
updateSlot();
|
|
538
|
-
// Finish the transition
|
|
539
|
-
this._finishTransition();
|
|
540
|
-
});
|
|
541
|
-
}
|
|
517
|
+
this.recalculateLayout();
|
|
518
|
+
};
|
|
542
519
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
* Returns 'replace' in two cases:
|
|
547
|
-
* - Swapping one detail for another (standard replace).
|
|
548
|
-
* - Swapping between placeholder and detail in split mode,
|
|
549
|
-
* so the swap appears instant (replace has 0ms duration in split).
|
|
550
|
-
* In overlay mode, placeholder doesn't participate in transitions,
|
|
551
|
-
* so standard 'add'/'remove' are used instead.
|
|
552
|
-
*
|
|
553
|
-
* @param {Element | null} currentDetail
|
|
554
|
-
* @param {Element | null} newDetail
|
|
555
|
-
* @return {string}
|
|
556
|
-
* @private
|
|
557
|
-
*/
|
|
558
|
-
__getTransitionType(currentDetail, newDetail) {
|
|
559
|
-
if (currentDetail && newDetail) {
|
|
560
|
-
return 'replace';
|
|
520
|
+
if (skipTransition || this.noAnimation) {
|
|
521
|
+
await updateSlot();
|
|
522
|
+
return;
|
|
561
523
|
}
|
|
562
524
|
|
|
563
525
|
const hasPlaceholder = !!this.querySelector('[slot="detail-placeholder"]');
|
|
564
|
-
if (hasPlaceholder && !this.hasAttribute('overlay')) {
|
|
565
|
-
|
|
526
|
+
if ((oldDetail && newDetail) || (hasPlaceholder && !this.hasAttribute('overlay'))) {
|
|
527
|
+
await this._startTransition('replace', updateSlot);
|
|
528
|
+
} else if (!oldDetail && newDetail) {
|
|
529
|
+
await this._startTransition('add', updateSlot);
|
|
530
|
+
} else if (oldDetail && !newDetail) {
|
|
531
|
+
await this._startTransition('remove', updateSlot);
|
|
566
532
|
}
|
|
567
|
-
|
|
568
|
-
return currentDetail ? 'remove' : 'add';
|
|
569
533
|
}
|
|
570
534
|
|
|
571
535
|
/**
|
|
572
536
|
* Starts an animated transition for adding, replacing or removing the
|
|
573
537
|
* detail area using the Web Animations API.
|
|
574
538
|
*
|
|
575
|
-
* For '
|
|
576
|
-
*
|
|
577
|
-
*
|
|
539
|
+
* For 'add'/'replace': DOM is updated immediately, then animation
|
|
540
|
+
* starts after a microtask (so Lit elements render and layout is
|
|
541
|
+
* recalculated before animation params are read).
|
|
578
542
|
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
543
|
+
* For 'remove': animation plays first, then DOM is updated after
|
|
544
|
+
* the slide-out completes.
|
|
545
|
+
*
|
|
546
|
+
* Interruptible: a new transition cancels any in-progress animation
|
|
547
|
+
* and picks up from the interrupted position.
|
|
582
548
|
*
|
|
583
549
|
* @param transitionType
|
|
584
|
-
* @param
|
|
550
|
+
* @param updateSlot
|
|
585
551
|
* @return {Promise<void>}
|
|
586
552
|
* @protected
|
|
587
553
|
*/
|
|
588
|
-
_startTransition(transitionType,
|
|
554
|
+
async _startTransition(transitionType, updateSlot) {
|
|
589
555
|
if (this.noAnimation) {
|
|
590
|
-
|
|
591
|
-
return
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Capture mid-flight state before cancelling active animations
|
|
595
|
-
const interrupted = this.__captureDetailState();
|
|
596
|
-
|
|
597
|
-
this.__endTransition();
|
|
598
|
-
|
|
599
|
-
if (transitionType === 'replace') {
|
|
600
|
-
this.__snapshotOutgoing();
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
this.setAttribute('transition', transitionType);
|
|
604
|
-
|
|
605
|
-
if (transitionType !== 'remove') {
|
|
606
|
-
updateCallback();
|
|
556
|
+
await updateSlot();
|
|
557
|
+
return;
|
|
607
558
|
}
|
|
608
559
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
* @param {string} transitionType
|
|
623
|
-
* @param {{ offscreen: string, duration: number, easing: string, interrupted?: { translate: string, opacity: string }, overlay?: boolean }} opts
|
|
624
|
-
* @param {Function} updateCallback
|
|
625
|
-
* @return {Promise<void>}
|
|
626
|
-
* @private
|
|
627
|
-
*/
|
|
628
|
-
__animateTransition(transitionType, opts, updateCallback) {
|
|
629
|
-
const version = (this.__transitionVersion = (this.__transitionVersion || 0) + 1);
|
|
630
|
-
|
|
631
|
-
return new Promise((resolve) => {
|
|
632
|
-
this.__transitionResolve = resolve;
|
|
633
|
-
|
|
634
|
-
const onFinish = (callback) => {
|
|
635
|
-
if (this.__transitionVersion === version) {
|
|
636
|
-
if (callback) {
|
|
637
|
-
callback();
|
|
638
|
-
}
|
|
639
|
-
this.__endTransition();
|
|
640
|
-
}
|
|
641
|
-
};
|
|
642
|
-
|
|
643
|
-
if (transitionType === 'remove') {
|
|
644
|
-
this.__slide(this.$.detail, false, opts).then(() => onFinish(updateCallback));
|
|
645
|
-
} else if (transitionType === 'replace') {
|
|
646
|
-
// Outgoing slides out on top (z-index), revealing incoming underneath.
|
|
647
|
-
// In overlay mode, the incoming also slides in simultaneously.
|
|
648
|
-
this.__slide(this.$.outgoing, false, opts).then(() => onFinish());
|
|
649
|
-
if (opts.overlay) {
|
|
650
|
-
this.__slide(this.$.detail, true, { ...opts, interrupted: null });
|
|
651
|
-
}
|
|
652
|
-
} else {
|
|
653
|
-
this.__slide(this.$.detail, true, opts).then(() => onFinish());
|
|
560
|
+
try {
|
|
561
|
+
this.setAttribute('transition', transitionType);
|
|
562
|
+
|
|
563
|
+
switch (transitionType) {
|
|
564
|
+
case 'add':
|
|
565
|
+
await this.__addTransition(updateSlot);
|
|
566
|
+
break;
|
|
567
|
+
case 'remove':
|
|
568
|
+
await this.__removeTransition(updateSlot);
|
|
569
|
+
break;
|
|
570
|
+
default:
|
|
571
|
+
await this.__replaceTransition(updateSlot);
|
|
572
|
+
break;
|
|
654
573
|
}
|
|
655
574
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
duration: opts.duration,
|
|
661
|
-
easing: 'linear',
|
|
662
|
-
});
|
|
575
|
+
this.removeAttribute('transition');
|
|
576
|
+
} catch (e) {
|
|
577
|
+
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
578
|
+
return; // Animation was cancelled
|
|
663
579
|
}
|
|
664
|
-
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* Finishes the current transition by detecting and applying the layout
|
|
669
|
-
* state. This method should be called after the DOM has been updated.
|
|
670
|
-
*
|
|
671
|
-
* @protected
|
|
672
|
-
*/
|
|
673
|
-
_finishTransition() {
|
|
674
|
-
queueMicrotask(() => this.recalculateLayout());
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Captures the detail panel's current animated state (translate and
|
|
679
|
-
* opacity). Must be called BEFORE `animation.cancel()`, because
|
|
680
|
-
* cancel removes the animation effect and the element reverts to
|
|
681
|
-
* its CSS resting state.
|
|
682
|
-
*
|
|
683
|
-
* Returns null when there is no active animation.
|
|
684
|
-
*
|
|
685
|
-
* @return {{ translate: string, opacity: string } | null}
|
|
686
|
-
* @private
|
|
687
|
-
*/
|
|
688
|
-
__captureDetailState() {
|
|
689
|
-
if (!this.__activeAnimations || this.__activeAnimations.length === 0) {
|
|
690
|
-
return null;
|
|
580
|
+
throw e;
|
|
691
581
|
}
|
|
692
|
-
const { translate, opacity } = getComputedStyle(this.$.detail);
|
|
693
|
-
return { translate, opacity };
|
|
694
582
|
}
|
|
695
583
|
|
|
696
|
-
/**
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
*
|
|
700
|
-
* @return {{ offscreen: string, duration: number, easing: string }}
|
|
701
|
-
* @private
|
|
702
|
-
*/
|
|
703
|
-
__getAnimationParams() {
|
|
704
|
-
const cs = getComputedStyle(this);
|
|
705
|
-
const offscreen = cs.getPropertyValue('--_detail-offscreen').trim();
|
|
706
|
-
const durationStr = cs.getPropertyValue('--_transition-duration').trim();
|
|
707
|
-
const duration = durationStr.endsWith('ms') ? parseFloat(durationStr) : parseFloat(durationStr) * 1000;
|
|
708
|
-
const easing = cs.getPropertyValue('--_transition-easing').trim();
|
|
709
|
-
return { offscreen, duration, easing };
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
/**
|
|
713
|
-
* Creates a slide animation on the element's `translate` property
|
|
714
|
-
* using the Web Animations API. Returns a promise that resolves when
|
|
715
|
-
* the animation finishes, or immediately if the duration is 0.
|
|
716
|
-
*
|
|
717
|
-
* @param {HTMLElement} element - The element to animate
|
|
718
|
-
* @param {boolean} slideIn - If true, slide in (off-screen → on-screen);
|
|
719
|
-
* otherwise slide out (on-screen → off-screen)
|
|
720
|
-
* @param {{ offscreen: string, duration: number, easing: string, interrupted?: { translate: string, opacity: string }, overlay?: boolean }} opts
|
|
721
|
-
* Animation parameters. `interrupted` overrides the default starting
|
|
722
|
-
* keyframe for interrupted animations (captured mid-flight before cancel).
|
|
723
|
-
* @return {Promise<void>}
|
|
724
|
-
* @private
|
|
725
|
-
*/
|
|
726
|
-
__slide(element, slideIn, { offscreen, duration, easing, interrupted, overlay }) {
|
|
727
|
-
if (!offscreen || duration <= 0) {
|
|
728
|
-
return Promise.resolve();
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
const defaultTranslate = slideIn ? offscreen : 'none';
|
|
732
|
-
const defaultOpacity = !overlay && slideIn ? 0 : 1;
|
|
733
|
-
|
|
734
|
-
const start = interrupted ? interrupted.translate : defaultTranslate;
|
|
735
|
-
const end = slideIn ? 'none' : offscreen;
|
|
736
|
-
|
|
737
|
-
const opacityStart = interrupted ? Number(interrupted.opacity) : defaultOpacity;
|
|
738
|
-
const opacityEnd = !overlay && !slideIn ? 0 : 1;
|
|
584
|
+
/** @private */
|
|
585
|
+
async __addTransition(updateSlot) {
|
|
586
|
+
await updateSlot();
|
|
739
587
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
[
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
],
|
|
746
|
-
{ duration, easing },
|
|
747
|
-
);
|
|
588
|
+
const progress = getCurrentAnimationProgress(this.$.detail);
|
|
589
|
+
await Promise.all([
|
|
590
|
+
animateIn(this.$.detail, ['fade', 'slide'], progress),
|
|
591
|
+
animateIn(this.$.backdrop, ['fade'], this.hasAttribute('overlay') ? progress : 1),
|
|
592
|
+
]);
|
|
748
593
|
}
|
|
749
594
|
|
|
750
|
-
/**
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
*
|
|
755
|
-
* @param {HTMLElement} element
|
|
756
|
-
* @param {Keyframe[]} keyframes
|
|
757
|
-
* @param {KeyframeAnimationOptions} options
|
|
758
|
-
* @return {Promise<void>}
|
|
759
|
-
* @private
|
|
760
|
-
*/
|
|
761
|
-
__animate(element, keyframes, options) {
|
|
762
|
-
const animation = element.animate(keyframes, { ...options, fill: 'forwards' });
|
|
595
|
+
/** @private */
|
|
596
|
+
async __replaceTransition(updateSlot) {
|
|
597
|
+
try {
|
|
598
|
+
this.__snapshotOutgoing();
|
|
763
599
|
|
|
764
|
-
|
|
765
|
-
this.__activeAnimations.push(animation);
|
|
600
|
+
await updateSlot();
|
|
766
601
|
|
|
767
|
-
|
|
602
|
+
const progress = getCurrentAnimationProgress(this.$.detail);
|
|
603
|
+
await Promise.all([
|
|
604
|
+
animateIn(this.$.detail, ['fade', 'slide'], progress),
|
|
605
|
+
animateOut(this.$.outgoing, ['fade', 'slide'], progress),
|
|
606
|
+
]);
|
|
607
|
+
} finally {
|
|
608
|
+
this.__clearOutgoing();
|
|
609
|
+
}
|
|
768
610
|
}
|
|
769
611
|
|
|
770
|
-
/**
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}
|
|
780
|
-
this.removeAttribute('transition');
|
|
781
|
-
this.__clearOutgoing();
|
|
782
|
-
if (this.__transitionResolve) {
|
|
783
|
-
this.__transitionResolve();
|
|
784
|
-
this.__transitionResolve = null;
|
|
785
|
-
}
|
|
612
|
+
/** @private */
|
|
613
|
+
async __removeTransition(updateSlot) {
|
|
614
|
+
const progress = getCurrentAnimationProgress(this.$.detail);
|
|
615
|
+
await Promise.all([
|
|
616
|
+
animateOut(this.$.detail, ['fade', 'slide'], progress),
|
|
617
|
+
animateOut(this.$.backdrop, ['fade'], this.hasAttribute('overlay') ? progress : 1),
|
|
618
|
+
]);
|
|
619
|
+
|
|
620
|
+
await updateSlot();
|
|
786
621
|
}
|
|
787
622
|
|
|
788
623
|
/**
|
|
@@ -797,6 +632,7 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
797
632
|
return;
|
|
798
633
|
}
|
|
799
634
|
currentDetail.setAttribute('slot', 'detail-outgoing');
|
|
635
|
+
this.$.outgoing.style.width = this.__detailCachedSize;
|
|
800
636
|
this.__replacing = true;
|
|
801
637
|
}
|
|
802
638
|
|
|
@@ -806,6 +642,7 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
806
642
|
*/
|
|
807
643
|
__clearOutgoing() {
|
|
808
644
|
this.querySelectorAll('[slot="detail-outgoing"]').forEach((el) => el.remove());
|
|
645
|
+
this.$.outgoing.style.width = '';
|
|
809
646
|
this.__replacing = false;
|
|
810
647
|
}
|
|
811
648
|
|
package/web-types.json
CHANGED
package/web-types.lit.json
CHANGED