@vaadin/master-detail-layout 25.2.0-alpha5 → 25.2.0-alpha7
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 +35 -21
- package/src/vaadin-master-detail-layout-helpers.js +173 -0
- package/src/vaadin-master-detail-layout.js +138 -292
- 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-alpha7",
|
|
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-alpha7",
|
|
38
|
+
"@vaadin/component-base": "25.2.0-alpha7",
|
|
39
|
+
"@vaadin/vaadin-themable-mixin": "25.2.0-alpha7",
|
|
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-alpha7",
|
|
44
|
+
"@vaadin/chai-plugins": "25.2.0-alpha7",
|
|
45
45
|
"@vaadin/testing-helpers": "^2.0.0",
|
|
46
|
-
"@vaadin/vaadin-lumo-styles": "25.2.0-
|
|
46
|
+
"@vaadin/vaadin-lumo-styles": "25.2.0-alpha7",
|
|
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": "3f0862906d60205d107836d8eca84c6fde4a9129"
|
|
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:
|
|
@@ -51,17 +51,17 @@ export const masterDetailLayoutStyles = css`
|
|
|
51
51
|
[detail-end];
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
:is(#master, #detail, #
|
|
54
|
+
:is(#master, #detail, #detailPlaceholder, #detailOutgoing) {
|
|
55
55
|
box-sizing: border-box;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
#
|
|
58
|
+
#detailPlaceholder {
|
|
59
59
|
z-index: 1;
|
|
60
60
|
opacity: 0;
|
|
61
61
|
pointer-events: none;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
:host([has-detail-placeholder]:not([has-detail], [overlay])) #
|
|
64
|
+
:host([has-detail-placeholder]:not([has-detail], [overlay])) #detailPlaceholder {
|
|
65
65
|
opacity: 1;
|
|
66
66
|
pointer-events: auto;
|
|
67
67
|
}
|
|
@@ -69,9 +69,16 @@ export const masterDetailLayoutStyles = css`
|
|
|
69
69
|
#master {
|
|
70
70
|
grid-column: master-start / detail-start;
|
|
71
71
|
grid-row: 1;
|
|
72
|
+
opacity: 0;
|
|
73
|
+
pointer-events: none;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
:
|
|
76
|
+
:host([has-master]) #master {
|
|
77
|
+
opacity: 1;
|
|
78
|
+
pointer-events: auto;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
:is(#detail, #detailPlaceholder, #detailOutgoing) {
|
|
75
82
|
grid-column: detail-start / detail-end;
|
|
76
83
|
grid-row: 1;
|
|
77
84
|
}
|
|
@@ -81,12 +88,14 @@ export const masterDetailLayoutStyles = css`
|
|
|
81
88
|
grid-row: master-start / detail-start;
|
|
82
89
|
}
|
|
83
90
|
|
|
84
|
-
:host([orientation='vertical']) :is(#detail, #
|
|
91
|
+
:host([orientation='vertical']) :is(#detail, #detailPlaceholder, #detailOutgoing) {
|
|
85
92
|
grid-column: 1;
|
|
86
93
|
grid-row: detail-start / detail-end;
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
#backdrop {
|
|
97
|
+
--_transition-easing: linear;
|
|
98
|
+
|
|
90
99
|
position: absolute;
|
|
91
100
|
inset: 0;
|
|
92
101
|
z-index: 2;
|
|
@@ -111,31 +120,36 @@ export const masterDetailLayoutStyles = css`
|
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
:host([keep-detail-column-offscreen]),
|
|
114
|
-
:host([has-detail-placeholder][overlay]),
|
|
123
|
+
:host([has-detail-placeholder][overlay]:not([has-detail])),
|
|
115
124
|
:host(:not([has-detail-placeholder], [has-detail])) {
|
|
116
125
|
--_master-extra: calc(100% - var(--_master-size));
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
:host([orientation='horizontal']) #
|
|
128
|
+
:host([orientation='horizontal']) #detailPlaceholder,
|
|
120
129
|
:host([orientation='horizontal']:not([overlay])) #detail {
|
|
121
130
|
border-inline-start: var(--vaadin-master-detail-layout-border-width, 1px) solid
|
|
122
131
|
var(--vaadin-master-detail-layout-border-color, var(--vaadin-border-color-secondary));
|
|
123
132
|
}
|
|
124
133
|
|
|
125
|
-
:host([orientation='vertical']) #
|
|
134
|
+
:host([orientation='vertical']) #detailPlaceholder,
|
|
126
135
|
:host([orientation='vertical']:not([overlay])) #detail {
|
|
127
136
|
border-top: var(--vaadin-master-detail-layout-border-width, 1px) solid
|
|
128
137
|
var(--vaadin-master-detail-layout-border-color, var(--vaadin-border-color-secondary));
|
|
129
138
|
}
|
|
130
139
|
|
|
131
|
-
#
|
|
140
|
+
#detailOutgoing {
|
|
132
141
|
position: absolute;
|
|
133
142
|
z-index: 3;
|
|
143
|
+
display: none;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
:host([transition='replace']) #detailOutgoing {
|
|
147
|
+
display: block;
|
|
134
148
|
}
|
|
135
149
|
|
|
136
150
|
/* Detail transition: off-screen by default, on-screen when has-detail */
|
|
137
151
|
#detail {
|
|
138
|
-
translate: var(--
|
|
152
|
+
translate: var(--_transition-offset);
|
|
139
153
|
opacity: 0;
|
|
140
154
|
z-index: 4;
|
|
141
155
|
}
|
|
@@ -146,14 +160,14 @@ export const masterDetailLayoutStyles = css`
|
|
|
146
160
|
}
|
|
147
161
|
|
|
148
162
|
:host([overlay]) {
|
|
149
|
-
--
|
|
163
|
+
--_transition-offset: calc((100% + 30px) * var(--_rtl-multiplier));
|
|
150
164
|
}
|
|
151
165
|
|
|
152
166
|
:host([overlay][orientation='vertical']) {
|
|
153
|
-
--
|
|
167
|
+
--_transition-offset: 0 calc(100% + 30px);
|
|
154
168
|
}
|
|
155
169
|
|
|
156
|
-
:host([has-detail][overlay]) :is(#detail, #
|
|
170
|
+
:host([has-detail][overlay]) :is(#detail, #detailOutgoing) {
|
|
157
171
|
position: absolute;
|
|
158
172
|
background: var(--vaadin-master-detail-layout-detail-background, var(--vaadin-background-color));
|
|
159
173
|
box-shadow: var(--vaadin-master-detail-layout-detail-shadow, 0 0 20px 0 rgba(0, 0, 0, 0.3));
|
|
@@ -166,30 +180,30 @@ export const masterDetailLayoutStyles = css`
|
|
|
166
180
|
pointer-events: auto;
|
|
167
181
|
}
|
|
168
182
|
|
|
169
|
-
:host([has-detail][overlay]:not([orientation='vertical'])) :is(#detail, #
|
|
183
|
+
:host([has-detail][overlay]:not([orientation='vertical'])) :is(#detail, #detailOutgoing) {
|
|
170
184
|
inset-block: 0;
|
|
171
185
|
inset-inline-end: 0;
|
|
172
186
|
width: var(--_overlay-size, var(--_detail-size));
|
|
173
187
|
max-width: 100%;
|
|
174
188
|
}
|
|
175
189
|
|
|
176
|
-
:host([has-detail][overlay][orientation='vertical']) :is(#detail, #
|
|
190
|
+
:host([has-detail][overlay][orientation='vertical']) :is(#detail, #detailOutgoing) {
|
|
177
191
|
inset-inline: 0;
|
|
178
192
|
inset-block-end: 0;
|
|
179
193
|
height: var(--_overlay-size, var(--_detail-size));
|
|
180
194
|
max-height: 100%;
|
|
181
195
|
}
|
|
182
196
|
|
|
183
|
-
:host([has-detail][overlay][overlay-containment='viewport']) :is(#detail, #
|
|
197
|
+
:host([has-detail][overlay][overlay-containment='viewport']) :is(#detail, #detailOutgoing, #backdrop) {
|
|
184
198
|
position: fixed;
|
|
185
199
|
}
|
|
186
200
|
|
|
187
201
|
@media (forced-colors: active) {
|
|
188
|
-
:host([has-detail][overlay]) :is(#detail, #
|
|
202
|
+
:host([has-detail][overlay]) :is(#detail, #detailOutgoing) {
|
|
189
203
|
outline: 3px solid !important;
|
|
190
204
|
}
|
|
191
205
|
|
|
192
|
-
:is(#detail, #
|
|
206
|
+
:is(#detail, #detailPlaceholder, #detailOutgoing) {
|
|
193
207
|
background: Canvas !important;
|
|
194
208
|
}
|
|
195
209
|
}
|
|
@@ -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
|
+
}
|
|
@@ -4,34 +4,21 @@
|
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
6
|
import { html, LitElement, nothing } from 'lit';
|
|
7
|
-
import { getFocusableElements } from '@vaadin/a11y-base/src/focus-utils.js';
|
|
7
|
+
import { getFocusableElements, isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js';
|
|
8
8
|
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
|
|
9
9
|
import { getClosestElement } from '@vaadin/component-base/src/dom-utils.js';
|
|
10
10
|
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
|
|
@@ -215,12 +202,6 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
215
202
|
reflectToAttribute: true,
|
|
216
203
|
},
|
|
217
204
|
|
|
218
|
-
/** @private */
|
|
219
|
-
__replacing: {
|
|
220
|
-
type: Boolean,
|
|
221
|
-
sync: true,
|
|
222
|
-
},
|
|
223
|
-
|
|
224
205
|
/** @private */
|
|
225
206
|
__detailCachedSize: {
|
|
226
207
|
type: String,
|
|
@@ -245,7 +226,7 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
245
226
|
<div id="master" part="master" ?inert="${isLayoutContained}">
|
|
246
227
|
<slot @slotchange="${this.__onSlotChange}"></slot>
|
|
247
228
|
</div>
|
|
248
|
-
<div id="
|
|
229
|
+
<div id="detailOutgoing" inert>
|
|
249
230
|
<slot name="detail-outgoing"></slot>
|
|
250
231
|
</div>
|
|
251
232
|
<div
|
|
@@ -257,7 +238,7 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
257
238
|
>
|
|
258
239
|
<slot name="detail" @slotchange="${this.__onSlotChange}"></slot>
|
|
259
240
|
</div>
|
|
260
|
-
<div id="
|
|
241
|
+
<div id="detailPlaceholder" part="detail-placeholder">
|
|
261
242
|
<slot name="detail-placeholder" @slotchange="${this.__onSlotChange}"></slot>
|
|
262
243
|
</div>
|
|
263
244
|
`;
|
|
@@ -267,6 +248,17 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
267
248
|
connectedCallback() {
|
|
268
249
|
super.connectedCallback();
|
|
269
250
|
this.__initResizeObserver();
|
|
251
|
+
|
|
252
|
+
const ancestorLayouts = this.__ancestorLayouts;
|
|
253
|
+
if (ancestorLayouts.length > 0) {
|
|
254
|
+
ancestorLayouts.forEach((layout) => {
|
|
255
|
+
cancelAnimationFrame(layout.__initialRaf);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
this.__initialRaf = requestAnimationFrame(() => {
|
|
259
|
+
this.recalculateLayout();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
270
262
|
}
|
|
271
263
|
|
|
272
264
|
/** @protected */
|
|
@@ -274,7 +266,8 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
274
266
|
super.disconnectedCallback();
|
|
275
267
|
this.__resizeObserver.disconnect();
|
|
276
268
|
cancelAnimationFrame(this.__resizeRaf);
|
|
277
|
-
this.
|
|
269
|
+
cancelAnimationFrame(this.__initialRaf);
|
|
270
|
+
cancelAnimations(this);
|
|
278
271
|
}
|
|
279
272
|
|
|
280
273
|
/** @private */
|
|
@@ -331,9 +324,10 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
331
324
|
this.__resizeObserver = this.__resizeObserver || new ResizeObserver(() => this.__onResize());
|
|
332
325
|
this.__resizeObserver.disconnect();
|
|
333
326
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
327
|
+
[this, this.$.master, this.$.detail, this.__slottedMaster, this.__slottedDetail].forEach((node) => {
|
|
328
|
+
if (node) {
|
|
329
|
+
this.__resizeObserver.observe(node);
|
|
330
|
+
}
|
|
337
331
|
});
|
|
338
332
|
}
|
|
339
333
|
|
|
@@ -357,12 +351,14 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
357
351
|
__readLayoutState() {
|
|
358
352
|
const isVertical = this.orientation === 'vertical';
|
|
359
353
|
|
|
360
|
-
const
|
|
361
|
-
const
|
|
354
|
+
const slottedMaster = this.__slottedMaster;
|
|
355
|
+
const slottedDetail = this.__slottedDetail;
|
|
356
|
+
const slottedDetailPlaceholder = this.__slottedDetailPlaceholder;
|
|
362
357
|
|
|
358
|
+
const hasMaster = !!slottedMaster;
|
|
363
359
|
const hadDetail = this.hasAttribute('has-detail');
|
|
364
|
-
const hasDetail =
|
|
365
|
-
const hasDetailPlaceholder = !!
|
|
360
|
+
const hasDetail = slottedDetail != null && slottedDetail.checkVisibility();
|
|
361
|
+
const hasDetailPlaceholder = !!slottedDetailPlaceholder;
|
|
366
362
|
|
|
367
363
|
const computedStyle = getComputedStyle(this);
|
|
368
364
|
const hostSizeProp = isVertical ? 'height' : 'width';
|
|
@@ -372,9 +368,10 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
372
368
|
const trackSizes = parseTrackSizes(computedStyle[trackSizesProp]);
|
|
373
369
|
|
|
374
370
|
const hasOverflow = (hasDetail || hasDetailPlaceholder) && detectOverflow(hostSize, trackSizes);
|
|
375
|
-
const focusTarget = !hadDetail && hasDetail && hasOverflow ? getFocusableElements(
|
|
371
|
+
const focusTarget = !hadDetail && hasDetail && hasOverflow ? getFocusableElements(slottedDetail)[0] : null;
|
|
376
372
|
|
|
377
373
|
return {
|
|
374
|
+
hasMaster,
|
|
378
375
|
hadDetail,
|
|
379
376
|
hasDetail,
|
|
380
377
|
hasDetailPlaceholder,
|
|
@@ -389,7 +386,7 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
389
386
|
* Applies layout state to DOM attributes. Pure writes, no reads.
|
|
390
387
|
* @private
|
|
391
388
|
*/
|
|
392
|
-
__writeLayoutState({ hadDetail, hasDetail, hasDetailPlaceholder, hasOverflow, focusTarget, trackSizes }) {
|
|
389
|
+
__writeLayoutState({ hasMaster, hadDetail, hasDetail, hasDetailPlaceholder, hasOverflow, focusTarget, trackSizes }) {
|
|
393
390
|
const [_masterSize, _masterExtra, detailSize] = trackSizes;
|
|
394
391
|
|
|
395
392
|
// If no detailSize is explicitily set, cache the intrinsic size (min-content) of
|
|
@@ -411,6 +408,7 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
411
408
|
}
|
|
412
409
|
|
|
413
410
|
this.toggleAttribute('overlay', hasOverflow);
|
|
411
|
+
this.toggleAttribute('has-master', hasMaster);
|
|
414
412
|
this.toggleAttribute('has-detail', hasDetail);
|
|
415
413
|
this.toggleAttribute('has-detail-placeholder', hasDetailPlaceholder);
|
|
416
414
|
|
|
@@ -419,7 +417,7 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
419
417
|
this.requestUpdate();
|
|
420
418
|
|
|
421
419
|
if (focusTarget) {
|
|
422
|
-
focusTarget.focus({ preventScroll: true });
|
|
420
|
+
focusTarget.focus({ preventScroll: true, focusVisible: isKeyboardActive() });
|
|
423
421
|
}
|
|
424
422
|
}
|
|
425
423
|
|
|
@@ -437,14 +435,14 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
437
435
|
* synchronous DOM reads and writes.
|
|
438
436
|
*/
|
|
439
437
|
recalculateLayout() {
|
|
440
|
-
|
|
441
|
-
// overriding the layout state with stale measurements.
|
|
442
|
-
cancelAnimationFrame(this.__resizeRaf);
|
|
443
|
-
|
|
444
|
-
const invalidatedLayouts = [...this.__ancestorLayouts.filter((layout) => layout.__isDetailAutoSized), this];
|
|
438
|
+
const invalidatedLayouts = [...this.__ancestorLayouts, this];
|
|
445
439
|
|
|
446
440
|
// Write
|
|
447
441
|
invalidatedLayouts.forEach((layout) => {
|
|
442
|
+
// Cancel any pending ResizeObserver rAF to prevent it from potentially
|
|
443
|
+
// overriding the layout state with stale measurements.
|
|
444
|
+
cancelAnimationFrame(layout.__resizeRaf);
|
|
445
|
+
|
|
448
446
|
layout.__detailCachedSize = null;
|
|
449
447
|
|
|
450
448
|
if (layout.__isDetailAutoSized) {
|
|
@@ -507,65 +505,42 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
507
505
|
* @return {Promise<void>}
|
|
508
506
|
* @protected
|
|
509
507
|
*/
|
|
510
|
-
_setDetail(
|
|
508
|
+
async _setDetail(newDetail, skipTransition) {
|
|
511
509
|
// Don't start a transition if detail didn't change
|
|
512
|
-
const
|
|
513
|
-
if ((
|
|
514
|
-
return
|
|
510
|
+
const oldDetail = this.__slottedDetail;
|
|
511
|
+
if (oldDetail === (newDetail || null)) {
|
|
512
|
+
return;
|
|
515
513
|
}
|
|
516
514
|
|
|
517
|
-
const updateSlot = () => {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
// Add new content
|
|
521
|
-
if (element) {
|
|
522
|
-
element.setAttribute('slot', 'detail');
|
|
523
|
-
this.appendChild(element);
|
|
515
|
+
const updateSlot = async () => {
|
|
516
|
+
if (oldDetail && oldDetail.slot === 'detail') {
|
|
517
|
+
oldDetail.remove();
|
|
524
518
|
}
|
|
525
|
-
};
|
|
526
519
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
520
|
+
if (newDetail) {
|
|
521
|
+
newDetail.setAttribute('slot', 'detail');
|
|
522
|
+
this.appendChild(newDetail);
|
|
523
|
+
}
|
|
532
524
|
|
|
533
|
-
|
|
525
|
+
// Wait for Lit elements to render
|
|
526
|
+
await Promise.resolve();
|
|
534
527
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
updateSlot();
|
|
538
|
-
// Finish the transition
|
|
539
|
-
this._finishTransition();
|
|
540
|
-
});
|
|
541
|
-
}
|
|
528
|
+
this.recalculateLayout();
|
|
529
|
+
};
|
|
542
530
|
|
|
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';
|
|
531
|
+
if (skipTransition || this.noAnimation) {
|
|
532
|
+
await updateSlot();
|
|
533
|
+
return;
|
|
561
534
|
}
|
|
562
535
|
|
|
563
|
-
const hasPlaceholder = !!this.
|
|
564
|
-
if (hasPlaceholder && !this.hasAttribute('overlay')) {
|
|
565
|
-
|
|
536
|
+
const hasPlaceholder = !!this.__slottedDetailPlaceholder;
|
|
537
|
+
if ((oldDetail && newDetail) || (hasPlaceholder && !this.hasAttribute('overlay'))) {
|
|
538
|
+
await this._startTransition('replace', updateSlot);
|
|
539
|
+
} else if (!oldDetail && newDetail) {
|
|
540
|
+
await this._startTransition('add', updateSlot);
|
|
541
|
+
} else if (oldDetail && !newDetail) {
|
|
542
|
+
await this._startTransition('remove', updateSlot);
|
|
566
543
|
}
|
|
567
|
-
|
|
568
|
-
return currentDetail ? 'remove' : 'add';
|
|
569
544
|
}
|
|
570
545
|
|
|
571
546
|
/**
|
|
@@ -583,230 +558,101 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
583
558
|
* and picks up from the interrupted position.
|
|
584
559
|
*
|
|
585
560
|
* @param transitionType
|
|
586
|
-
* @param
|
|
561
|
+
* @param updateSlot
|
|
587
562
|
* @return {Promise<void>}
|
|
588
563
|
* @protected
|
|
589
564
|
*/
|
|
590
|
-
async _startTransition(transitionType,
|
|
565
|
+
async _startTransition(transitionType, updateSlot) {
|
|
591
566
|
if (this.noAnimation) {
|
|
592
|
-
|
|
567
|
+
await updateSlot();
|
|
593
568
|
return;
|
|
594
569
|
}
|
|
595
570
|
|
|
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 });
|
|
571
|
+
try {
|
|
572
|
+
this.setAttribute('transition', transitionType);
|
|
573
|
+
|
|
574
|
+
switch (transitionType) {
|
|
575
|
+
case 'add':
|
|
576
|
+
await this.__addTransition(updateSlot);
|
|
577
|
+
break;
|
|
578
|
+
case 'remove':
|
|
579
|
+
await this.__removeTransition(updateSlot);
|
|
580
|
+
break;
|
|
581
|
+
default:
|
|
582
|
+
await this.__replaceTransition(updateSlot);
|
|
583
|
+
break;
|
|
653
584
|
}
|
|
654
|
-
} else {
|
|
655
|
-
slide = this.__slide(this.$.detail, true, opts);
|
|
656
|
-
}
|
|
657
585
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
});
|
|
586
|
+
this.removeAttribute('transition');
|
|
587
|
+
} catch (e) {
|
|
588
|
+
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
589
|
+
return; // Animation was cancelled
|
|
590
|
+
}
|
|
591
|
+
throw e;
|
|
665
592
|
}
|
|
666
|
-
|
|
667
|
-
return slide;
|
|
668
593
|
}
|
|
669
594
|
|
|
670
|
-
/**
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
*
|
|
674
|
-
* @protected
|
|
675
|
-
*/
|
|
676
|
-
_finishTransition() {
|
|
677
|
-
queueMicrotask(() => this.recalculateLayout());
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* Captures the detail panel's current animated state (translate and
|
|
682
|
-
* opacity). Must be called BEFORE `animation.cancel()`, because
|
|
683
|
-
* cancel removes the animation effect and the element reverts to
|
|
684
|
-
* its CSS resting state.
|
|
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;
|
|
694
|
-
}
|
|
695
|
-
const { translate, opacity } = getComputedStyle(this.$.detail);
|
|
696
|
-
return { translate, opacity };
|
|
697
|
-
}
|
|
595
|
+
/** @private */
|
|
596
|
+
async __addTransition(updateSlot) {
|
|
597
|
+
await updateSlot();
|
|
698
598
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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 };
|
|
599
|
+
const progress = getCurrentAnimationProgress(this.$.detail);
|
|
600
|
+
await Promise.all([
|
|
601
|
+
animateIn(this.$.detail, ['fade', 'slide'], progress),
|
|
602
|
+
animateIn(this.$.backdrop, ['fade'], this.hasAttribute('overlay') ? progress : 1),
|
|
603
|
+
]);
|
|
713
604
|
}
|
|
714
605
|
|
|
715
|
-
/**
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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();
|
|
606
|
+
/** @private */
|
|
607
|
+
async __replaceTransition(updateSlot) {
|
|
608
|
+
const oldDetail = this.__slottedDetail;
|
|
609
|
+
if (oldDetail) {
|
|
610
|
+
oldDetail.slot = 'detail-outgoing';
|
|
732
611
|
}
|
|
733
612
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
613
|
+
try {
|
|
614
|
+
this.$.detailOutgoing.style.width = this.__detailCachedSize;
|
|
615
|
+
|
|
616
|
+
await updateSlot();
|
|
617
|
+
|
|
618
|
+
const progress = getCurrentAnimationProgress(this.$.detail);
|
|
619
|
+
await Promise.all([
|
|
620
|
+
animateIn(this.$.detail, ['fade', 'slide'], progress),
|
|
621
|
+
animateOut(this.$.detailOutgoing, ['fade', 'slide'], progress),
|
|
622
|
+
]);
|
|
623
|
+
} finally {
|
|
624
|
+
// Skip removal if the slot was reassigned during the transition.
|
|
625
|
+
// The React component does this to let React handle the removal.
|
|
626
|
+
if (oldDetail && oldDetail.slot === 'detail-outgoing') {
|
|
627
|
+
oldDetail.remove();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
751
630
|
}
|
|
752
631
|
|
|
753
|
-
/**
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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' });
|
|
766
|
-
|
|
767
|
-
this.__activeAnimations = this.__activeAnimations || [];
|
|
768
|
-
this.__activeAnimations.push(animation);
|
|
632
|
+
/** @private */
|
|
633
|
+
async __removeTransition(updateSlot) {
|
|
634
|
+
const progress = getCurrentAnimationProgress(this.$.detail);
|
|
635
|
+
await Promise.all([
|
|
636
|
+
animateOut(this.$.detail, ['fade', 'slide'], progress),
|
|
637
|
+
animateOut(this.$.backdrop, ['fade'], this.hasAttribute('overlay') ? progress : 1),
|
|
638
|
+
]);
|
|
769
639
|
|
|
770
|
-
|
|
640
|
+
await updateSlot();
|
|
771
641
|
}
|
|
772
642
|
|
|
773
|
-
/**
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
*/
|
|
777
|
-
__endTransition() {
|
|
778
|
-
if (this.__activeAnimations) {
|
|
779
|
-
this.__activeAnimations.forEach((a) => a.cancel());
|
|
780
|
-
this.__activeAnimations = null;
|
|
781
|
-
}
|
|
782
|
-
this.removeAttribute('transition');
|
|
783
|
-
this.__clearOutgoing();
|
|
643
|
+
/** @private */
|
|
644
|
+
get __slottedMaster() {
|
|
645
|
+
return this.querySelector(':scope > :is([slot=""], :not([slot]))');
|
|
784
646
|
}
|
|
785
647
|
|
|
786
|
-
/**
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
* light DOM so light DOM styles continue to apply.
|
|
790
|
-
* @private
|
|
791
|
-
*/
|
|
792
|
-
__snapshotOutgoing() {
|
|
793
|
-
const currentDetail = this.querySelector('[slot="detail"]');
|
|
794
|
-
if (!currentDetail) {
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
currentDetail.setAttribute('slot', 'detail-outgoing');
|
|
798
|
-
this.$.outgoing.style.width = this.__detailCachedSize;
|
|
799
|
-
this.__replacing = true;
|
|
648
|
+
/** @private */
|
|
649
|
+
get __slottedDetail() {
|
|
650
|
+
return this.querySelector(':scope > [slot="detail"]');
|
|
800
651
|
}
|
|
801
652
|
|
|
802
|
-
/**
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
*/
|
|
806
|
-
__clearOutgoing() {
|
|
807
|
-
this.querySelectorAll('[slot="detail-outgoing"]').forEach((el) => el.remove());
|
|
808
|
-
this.$.outgoing.style.width = '';
|
|
809
|
-
this.__replacing = false;
|
|
653
|
+
/** @private */
|
|
654
|
+
get __slottedDetailPlaceholder() {
|
|
655
|
+
return this.querySelector(':scope > [slot="detail-placeholder"]');
|
|
810
656
|
}
|
|
811
657
|
|
|
812
658
|
/**
|
package/web-types.json
CHANGED
package/web-types.lit.json
CHANGED