@vaadin/master-detail-layout 25.2.0-alpha5 → 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 +8 -6
- package/src/vaadin-master-detail-layout-helpers.js +173 -0
- package/src/vaadin-master-detail-layout.js +85 -248
- 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:
|
|
@@ -87,6 +87,8 @@ export const masterDetailLayoutStyles = css`
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
#backdrop {
|
|
90
|
+
--_transition-easing: linear;
|
|
91
|
+
|
|
90
92
|
position: absolute;
|
|
91
93
|
inset: 0;
|
|
92
94
|
z-index: 2;
|
|
@@ -135,7 +137,7 @@ export const masterDetailLayoutStyles = css`
|
|
|
135
137
|
|
|
136
138
|
/* Detail transition: off-screen by default, on-screen when has-detail */
|
|
137
139
|
#detail {
|
|
138
|
-
translate: var(--
|
|
140
|
+
translate: var(--_transition-offset);
|
|
139
141
|
opacity: 0;
|
|
140
142
|
z-index: 4;
|
|
141
143
|
}
|
|
@@ -146,11 +148,11 @@ export const masterDetailLayoutStyles = css`
|
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
:host([overlay]) {
|
|
149
|
-
--
|
|
151
|
+
--_transition-offset: calc((100% + 30px) * var(--_rtl-multiplier));
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
:host([overlay][orientation='vertical']) {
|
|
153
|
-
--
|
|
155
|
+
--_transition-offset: 0 calc(100% + 30px);
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
:host([has-detail][overlay]) :is(#detail, #outgoing) {
|
|
@@ -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,65 +494,42 @@ 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
|
/**
|
|
@@ -583,204 +547,77 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
583
547
|
* and picks up from the interrupted position.
|
|
584
548
|
*
|
|
585
549
|
* @param transitionType
|
|
586
|
-
* @param
|
|
550
|
+
* @param updateSlot
|
|
587
551
|
* @return {Promise<void>}
|
|
588
552
|
* @protected
|
|
589
553
|
*/
|
|
590
|
-
async _startTransition(transitionType,
|
|
554
|
+
async _startTransition(transitionType, updateSlot) {
|
|
591
555
|
if (this.noAnimation) {
|
|
592
|
-
|
|
556
|
+
await updateSlot();
|
|
593
557
|
return;
|
|
594
558
|
}
|
|
595
559
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if (transitionType !== 'remove') {
|
|
610
|
-
// Add/Replace: update DOM, wait for Lit rendering + recalculateLayout
|
|
611
|
-
updateCallback();
|
|
612
|
-
await Promise.resolve();
|
|
613
|
-
if (this.__transitionVersion !== version) return;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const opts = this.__getAnimationParams();
|
|
617
|
-
opts.interrupted = interrupted;
|
|
618
|
-
opts.overlay = this.hasAttribute('overlay');
|
|
619
|
-
|
|
620
|
-
// Run animations and wait for the detail slide to finish
|
|
621
|
-
await this.__runAnimations(transitionType, opts);
|
|
622
|
-
if (this.__transitionVersion !== version) return;
|
|
623
|
-
|
|
624
|
-
if (transitionType === 'remove') {
|
|
625
|
-
// Remove: deferred DOM update after slide-out completes
|
|
626
|
-
updateCallback();
|
|
627
|
-
await Promise.resolve();
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
this.__endTransition();
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Starts slide animation(s) for the given transition type and returns
|
|
635
|
-
* a promise that resolves when the detail slide completes.
|
|
636
|
-
*
|
|
637
|
-
* @param {string} transitionType
|
|
638
|
-
* @param {{ offscreen: string, duration: number, easing: string, interrupted?: { translate: string, opacity: string }, overlay?: boolean }} opts
|
|
639
|
-
* @return {Promise<void>}
|
|
640
|
-
* @private
|
|
641
|
-
*/
|
|
642
|
-
__runAnimations(transitionType, opts) {
|
|
643
|
-
let slide;
|
|
644
|
-
|
|
645
|
-
if (transitionType === 'remove') {
|
|
646
|
-
slide = this.__slide(this.$.detail, false, opts);
|
|
647
|
-
} else if (transitionType === 'replace') {
|
|
648
|
-
// Outgoing slides out while incoming is revealed underneath.
|
|
649
|
-
// In overlay mode, the incoming also slides in simultaneously.
|
|
650
|
-
slide = this.__slide(this.$.outgoing, false, opts);
|
|
651
|
-
if (opts.overlay) {
|
|
652
|
-
this.__slide(this.$.detail, true, { ...opts, interrupted: null });
|
|
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;
|
|
653
573
|
}
|
|
654
|
-
} else {
|
|
655
|
-
slide = this.__slide(this.$.detail, true, opts);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Fade backdrop in/out for overlay add/remove (not replace — backdrop stays visible)
|
|
659
|
-
if (opts.overlay && transitionType !== 'replace') {
|
|
660
|
-
const fadeIn = transitionType !== 'remove';
|
|
661
|
-
this.__animate(this.$.backdrop, [{ opacity: fadeIn ? 0 : 1 }, { opacity: fadeIn ? 1 : 0 }], {
|
|
662
|
-
duration: opts.duration,
|
|
663
|
-
easing: 'linear',
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
return slide;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
/**
|
|
671
|
-
* Finishes the current transition by detecting and applying the layout
|
|
672
|
-
* state. This method should be called after the DOM has been updated.
|
|
673
|
-
*
|
|
674
|
-
* @protected
|
|
675
|
-
*/
|
|
676
|
-
_finishTransition() {
|
|
677
|
-
queueMicrotask(() => this.recalculateLayout());
|
|
678
|
-
}
|
|
679
574
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
* Returns null when there is no active animation.
|
|
687
|
-
*
|
|
688
|
-
* @return {{ translate: string, opacity: string } | null}
|
|
689
|
-
* @private
|
|
690
|
-
*/
|
|
691
|
-
__captureDetailState() {
|
|
692
|
-
if (!this.__activeAnimations || this.__activeAnimations.length === 0) {
|
|
693
|
-
return null;
|
|
575
|
+
this.removeAttribute('transition');
|
|
576
|
+
} catch (e) {
|
|
577
|
+
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
578
|
+
return; // Animation was cancelled
|
|
579
|
+
}
|
|
580
|
+
throw e;
|
|
694
581
|
}
|
|
695
|
-
const { translate, opacity } = getComputedStyle(this.$.detail);
|
|
696
|
-
return { translate, opacity };
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* Reads animation parameters from CSS custom properties. Called once
|
|
701
|
-
* per transition so that animating stays free of layout reads.
|
|
702
|
-
*
|
|
703
|
-
* @return {{ offscreen: string, duration: number, easing: string }}
|
|
704
|
-
* @private
|
|
705
|
-
*/
|
|
706
|
-
__getAnimationParams() {
|
|
707
|
-
const cs = getComputedStyle(this);
|
|
708
|
-
const offscreen = cs.getPropertyValue('--_detail-offscreen').trim();
|
|
709
|
-
const durationStr = cs.getPropertyValue('--_transition-duration').trim();
|
|
710
|
-
const duration = durationStr.endsWith('ms') ? parseFloat(durationStr) : parseFloat(durationStr) * 1000;
|
|
711
|
-
const easing = cs.getPropertyValue('--_transition-easing').trim();
|
|
712
|
-
return { offscreen, duration, easing };
|
|
713
582
|
}
|
|
714
583
|
|
|
715
|
-
/**
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
* the animation finishes, or immediately if the duration is 0.
|
|
719
|
-
*
|
|
720
|
-
* @param {HTMLElement} element - The element to animate
|
|
721
|
-
* @param {boolean} slideIn - If true, slide in (off-screen → on-screen);
|
|
722
|
-
* otherwise slide out (on-screen → off-screen)
|
|
723
|
-
* @param {{ offscreen: string, duration: number, easing: string, interrupted?: { translate: string, opacity: string }, overlay?: boolean }} opts
|
|
724
|
-
* Animation parameters. `interrupted` overrides the default starting
|
|
725
|
-
* keyframe for interrupted animations (captured mid-flight before cancel).
|
|
726
|
-
* @return {Promise<void>}
|
|
727
|
-
* @private
|
|
728
|
-
*/
|
|
729
|
-
__slide(element, slideIn, { offscreen, duration, easing, interrupted, overlay }) {
|
|
730
|
-
if (!offscreen || duration <= 0) {
|
|
731
|
-
return Promise.resolve();
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const defaultTranslate = slideIn ? offscreen : 'none';
|
|
735
|
-
const defaultOpacity = !overlay && slideIn ? 0 : 1;
|
|
736
|
-
|
|
737
|
-
const start = interrupted ? interrupted.translate : defaultTranslate;
|
|
738
|
-
const end = slideIn ? 'none' : offscreen;
|
|
739
|
-
|
|
740
|
-
const opacityStart = interrupted ? Number(interrupted.opacity) : defaultOpacity;
|
|
741
|
-
const opacityEnd = !overlay && !slideIn ? 0 : 1;
|
|
584
|
+
/** @private */
|
|
585
|
+
async __addTransition(updateSlot) {
|
|
586
|
+
await updateSlot();
|
|
742
587
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
[
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
],
|
|
749
|
-
{ duration, easing },
|
|
750
|
-
);
|
|
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
|
+
]);
|
|
751
593
|
}
|
|
752
594
|
|
|
753
|
-
/**
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
*
|
|
758
|
-
* @param {HTMLElement} element
|
|
759
|
-
* @param {Keyframe[]} keyframes
|
|
760
|
-
* @param {KeyframeAnimationOptions} options
|
|
761
|
-
* @return {Promise<void>}
|
|
762
|
-
* @private
|
|
763
|
-
*/
|
|
764
|
-
__animate(element, keyframes, options) {
|
|
765
|
-
const animation = element.animate(keyframes, { ...options, fill: 'forwards' });
|
|
595
|
+
/** @private */
|
|
596
|
+
async __replaceTransition(updateSlot) {
|
|
597
|
+
try {
|
|
598
|
+
this.__snapshotOutgoing();
|
|
766
599
|
|
|
767
|
-
|
|
768
|
-
this.__activeAnimations.push(animation);
|
|
600
|
+
await updateSlot();
|
|
769
601
|
|
|
770
|
-
|
|
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
|
+
}
|
|
771
610
|
}
|
|
772
611
|
|
|
773
|
-
/**
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
this.removeAttribute('transition');
|
|
783
|
-
this.__clearOutgoing();
|
|
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();
|
|
784
621
|
}
|
|
785
622
|
|
|
786
623
|
/**
|
package/web-types.json
CHANGED
package/web-types.lit.json
CHANGED