@vaadin/master-detail-layout 25.2.0-alpha3 → 25.2.0-alpha5
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
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"type": {
|
|
34
34
|
"text": "string"
|
|
35
35
|
},
|
|
36
|
-
"description": "Size (in CSS length units) to be set on the detail area in\nthe CSS grid layout.
|
|
36
|
+
"description": "Size (in CSS length units) to be set on the detail area in\nthe CSS grid layout. When there is not enough space to show\nmaster and detail areas next to each other, the detail area\nis shown as an overlay.\n<p>\nIf not specified, the size is determined automatically by measuring\nthe detail content in a `min-content` CSS grid column when it first\nbecomes visible, and then caching the resulting intrinsic size. To\nrecalculate the cached intrinsic size, use the `recalculateLayout`\nmethod.",
|
|
37
37
|
"attribute": "detail-size"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
@@ -95,6 +95,11 @@
|
|
|
95
95
|
},
|
|
96
96
|
"description": "Size (in CSS length units) for the detail area when shown as an\noverlay. When not set, falls back to `detailSize`. Set to `100%`\nto make the detail cover the full layout.",
|
|
97
97
|
"attribute": "overlay-size"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"kind": "method",
|
|
101
|
+
"name": "recalculateLayout",
|
|
102
|
+
"description": "When `detailSize` is not explicitly set, re-measures the cached intrinsic size of\nthe detail content by placing it in a min-content CSS grid column, then repeats\nthis process for ancestor master-detail layouts without an explicit `detailSize`,\nif any, so that their detail areas also adapt.\n\nCall this method after changing the detail content in a way that affects its intrinsic\nsize — for example, when opening a detail in a nested master-detail layout that was\nnot previously visible.\n\nNOTE: This method can be expensive in large layouts as it triggers consecutive\nsynchronous DOM reads and writes."
|
|
98
103
|
}
|
|
99
104
|
],
|
|
100
105
|
"events": [
|
|
@@ -119,7 +124,7 @@
|
|
|
119
124
|
"type": {
|
|
120
125
|
"text": "string"
|
|
121
126
|
},
|
|
122
|
-
"description": "Size (in CSS length units) to be set on the detail area in\nthe CSS grid layout.
|
|
127
|
+
"description": "Size (in CSS length units) to be set on the detail area in\nthe CSS grid layout. When there is not enough space to show\nmaster and detail areas next to each other, the detail area\nis shown as an overlay.\n<p>\nIf not specified, the size is determined automatically by measuring\nthe detail content in a `min-content` CSS grid column when it first\nbecomes visible, and then caching the resulting intrinsic size. To\nrecalculate the cached intrinsic size, use the `recalculateLayout`\nmethod.",
|
|
123
128
|
"fieldName": "detailSize"
|
|
124
129
|
},
|
|
125
130
|
{
|
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-alpha5",
|
|
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-alpha5",
|
|
38
|
+
"@vaadin/component-base": "25.2.0-alpha5",
|
|
39
|
+
"@vaadin/vaadin-themable-mixin": "25.2.0-alpha5",
|
|
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-alpha5",
|
|
44
|
+
"@vaadin/chai-plugins": "25.2.0-alpha5",
|
|
45
45
|
"@vaadin/testing-helpers": "^2.0.0",
|
|
46
|
-
"@vaadin/vaadin-lumo-styles": "25.2.0-
|
|
46
|
+
"@vaadin/vaadin-lumo-styles": "25.2.0-alpha5",
|
|
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": "2f0c822a389571591b1b9d2c27d45e008ccbae6b"
|
|
55
55
|
}
|
|
@@ -8,10 +8,12 @@ import { css } from 'lit';
|
|
|
8
8
|
|
|
9
9
|
export const masterDetailLayoutStyles = css`
|
|
10
10
|
:host {
|
|
11
|
-
--_master-size:
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--_detail-
|
|
11
|
+
--_master-size: 30rem;
|
|
12
|
+
--_master-extra: 0px;
|
|
13
|
+
--_detail-size: var(--_detail-cached-size);
|
|
14
|
+
--_detail-extra: 0px;
|
|
15
|
+
--_detail-cached-size: min-content;
|
|
16
|
+
|
|
15
17
|
--_transition-duration: 0s;
|
|
16
18
|
--_transition-easing: cubic-bezier(0.78, 0, 0.22, 1);
|
|
17
19
|
--_rtl-multiplier: 1;
|
|
@@ -22,12 +24,16 @@ export const masterDetailLayoutStyles = css`
|
|
|
22
24
|
height: 100%;
|
|
23
25
|
position: relative;
|
|
24
26
|
z-index: 0;
|
|
25
|
-
overflow:
|
|
26
|
-
grid-template-columns:
|
|
27
|
+
overflow: clip;
|
|
28
|
+
grid-template-columns:
|
|
29
|
+
[master-start] var(--_master-size) var(--_master-extra)
|
|
30
|
+
[detail-start] var(--_detail-size) var(--_detail-extra)
|
|
31
|
+
[detail-end];
|
|
27
32
|
grid-template-rows: 100%;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
:host([hidden])
|
|
35
|
+
:host([hidden]),
|
|
36
|
+
::slotted([hidden]) {
|
|
31
37
|
display: none !important;
|
|
32
38
|
}
|
|
33
39
|
|
|
@@ -39,7 +45,10 @@ export const masterDetailLayoutStyles = css`
|
|
|
39
45
|
--_detail-offscreen: 0 30px;
|
|
40
46
|
|
|
41
47
|
grid-template-columns: 100%;
|
|
42
|
-
grid-template-rows:
|
|
48
|
+
grid-template-rows:
|
|
49
|
+
[master-start] var(--_master-size) var(--_master-extra)
|
|
50
|
+
[detail-start] var(--_detail-size) var(--_detail-extra)
|
|
51
|
+
[detail-end];
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
:is(#master, #detail, #detail-placeholder, #outgoing) {
|
|
@@ -47,11 +56,14 @@ export const masterDetailLayoutStyles = css`
|
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
#detail-placeholder {
|
|
50
|
-
|
|
59
|
+
z-index: 1;
|
|
60
|
+
opacity: 0;
|
|
61
|
+
pointer-events: none;
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
:host([has-detail-placeholder]:not([has-detail], [overlay])) #detail-placeholder {
|
|
54
|
-
|
|
65
|
+
opacity: 1;
|
|
66
|
+
pointer-events: auto;
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
#master {
|
|
@@ -77,7 +89,7 @@ export const masterDetailLayoutStyles = css`
|
|
|
77
89
|
#backdrop {
|
|
78
90
|
position: absolute;
|
|
79
91
|
inset: 0;
|
|
80
|
-
z-index:
|
|
92
|
+
z-index: 2;
|
|
81
93
|
opacity: 0;
|
|
82
94
|
pointer-events: none;
|
|
83
95
|
background: var(--vaadin-overlay-backdrop-background, rgba(0, 0, 0, 0.2));
|
|
@@ -86,43 +98,51 @@ export const masterDetailLayoutStyles = css`
|
|
|
86
98
|
|
|
87
99
|
:host([expand='both']),
|
|
88
100
|
:host([expand='master']) {
|
|
89
|
-
--_master-
|
|
101
|
+
--_master-extra: 1fr;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
:host([expand='both']:is([has-detail], [has-detail-placeholder])),
|
|
105
|
+
:host([expand='detail']:is([has-detail], [has-detail-placeholder])) {
|
|
106
|
+
--_detail-extra: 1fr;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
:host([recalculating-detail-size]:is([has-detail], [has-detail-placeholder])) {
|
|
110
|
+
--_detail-extra: 0px;
|
|
90
111
|
}
|
|
91
112
|
|
|
92
113
|
:host([keep-detail-column-offscreen]),
|
|
93
114
|
:host([has-detail-placeholder][overlay]),
|
|
94
115
|
:host(:not([has-detail-placeholder], [has-detail])) {
|
|
95
|
-
--_master-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
:host([expand='both']),
|
|
99
|
-
:host([expand='detail']) {
|
|
100
|
-
--_detail-column: var(--_detail-size) 1fr;
|
|
116
|
+
--_master-extra: calc(100% - var(--_master-size));
|
|
101
117
|
}
|
|
102
118
|
|
|
103
119
|
:host([orientation='horizontal']) #detail-placeholder,
|
|
104
|
-
:host([orientation='horizontal']
|
|
120
|
+
:host([orientation='horizontal']:not([overlay])) #detail {
|
|
105
121
|
border-inline-start: var(--vaadin-master-detail-layout-border-width, 1px) solid
|
|
106
122
|
var(--vaadin-master-detail-layout-border-color, var(--vaadin-border-color-secondary));
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
:host([orientation='vertical']) #detail-placeholder,
|
|
110
|
-
:host([orientation='vertical']
|
|
126
|
+
:host([orientation='vertical']:not([overlay])) #detail {
|
|
111
127
|
border-top: var(--vaadin-master-detail-layout-border-width, 1px) solid
|
|
112
128
|
var(--vaadin-master-detail-layout-border-color, var(--vaadin-border-color-secondary));
|
|
113
129
|
}
|
|
114
130
|
|
|
131
|
+
#outgoing {
|
|
132
|
+
position: absolute;
|
|
133
|
+
z-index: 3;
|
|
134
|
+
}
|
|
135
|
+
|
|
115
136
|
/* Detail transition: off-screen by default, on-screen when has-detail */
|
|
116
137
|
#detail {
|
|
117
138
|
translate: var(--_detail-offscreen);
|
|
139
|
+
opacity: 0;
|
|
140
|
+
z-index: 4;
|
|
118
141
|
}
|
|
119
142
|
|
|
120
143
|
:host([has-detail]) #detail {
|
|
121
144
|
translate: none;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
#outgoing:not([hidden]) {
|
|
125
|
-
z-index: 1;
|
|
145
|
+
opacity: 1;
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
:host([overlay]) {
|
|
@@ -135,7 +155,6 @@ export const masterDetailLayoutStyles = css`
|
|
|
135
155
|
|
|
136
156
|
:host([has-detail][overlay]) :is(#detail, #outgoing) {
|
|
137
157
|
position: absolute;
|
|
138
|
-
z-index: 2;
|
|
139
158
|
background: var(--vaadin-master-detail-layout-detail-background, var(--vaadin-background-color));
|
|
140
159
|
box-shadow: var(--vaadin-master-detail-layout-detail-shadow, 0 0 20px 0 rgba(0, 0, 0, 0.3));
|
|
141
160
|
grid-column: none;
|
|
@@ -150,13 +169,15 @@ export const masterDetailLayoutStyles = css`
|
|
|
150
169
|
:host([has-detail][overlay]:not([orientation='vertical'])) :is(#detail, #outgoing) {
|
|
151
170
|
inset-block: 0;
|
|
152
171
|
inset-inline-end: 0;
|
|
153
|
-
width: var(--_overlay-size, var(--_detail-size
|
|
172
|
+
width: var(--_overlay-size, var(--_detail-size));
|
|
173
|
+
max-width: 100%;
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
:host([has-detail][overlay][orientation='vertical']) :is(#detail, #outgoing) {
|
|
157
177
|
inset-inline: 0;
|
|
158
178
|
inset-block-end: 0;
|
|
159
|
-
height: var(--_overlay-size, var(--_detail-size
|
|
179
|
+
height: var(--_overlay-size, var(--_detail-size));
|
|
180
|
+
max-height: 100%;
|
|
160
181
|
}
|
|
161
182
|
|
|
162
183
|
:host([has-detail][overlay][overlay-containment='viewport']) :is(#detail, #outgoing, #backdrop) {
|
|
@@ -84,9 +84,15 @@ export interface MasterDetailLayoutEventMap extends HTMLElementEventMap, MasterD
|
|
|
84
84
|
declare class MasterDetailLayout extends ThemableMixin(ElementMixin(HTMLElement)) {
|
|
85
85
|
/**
|
|
86
86
|
* Size (in CSS length units) to be set on the detail area in
|
|
87
|
-
* the CSS grid layout.
|
|
87
|
+
* the CSS grid layout. When there is not enough space to show
|
|
88
88
|
* master and detail areas next to each other, the detail area
|
|
89
|
-
* is shown as an overlay.
|
|
89
|
+
* is shown as an overlay.
|
|
90
|
+
* <p>
|
|
91
|
+
* If not specified, the size is determined automatically by measuring
|
|
92
|
+
* the detail content in a `min-content` CSS grid column when it first
|
|
93
|
+
* becomes visible, and then caching the resulting intrinsic size. To
|
|
94
|
+
* recalculate the cached intrinsic size, use the `recalculateLayout`
|
|
95
|
+
* method.
|
|
90
96
|
*
|
|
91
97
|
* @attr {string} detail-size
|
|
92
98
|
*/
|
|
@@ -143,6 +149,21 @@ declare class MasterDetailLayout extends ThemableMixin(ElementMixin(HTMLElement)
|
|
|
143
149
|
*/
|
|
144
150
|
noAnimation: boolean;
|
|
145
151
|
|
|
152
|
+
/**
|
|
153
|
+
* When `detailSize` is not explicitly set, re-measures the cached intrinsic size of
|
|
154
|
+
* the detail content by placing it in a min-content CSS grid column, then repeats
|
|
155
|
+
* this process for ancestor master-detail layouts without an explicit `detailSize`,
|
|
156
|
+
* if any, so that their detail areas also adapt.
|
|
157
|
+
*
|
|
158
|
+
* Call this method after changing the detail content in a way that affects its intrinsic
|
|
159
|
+
* size — for example, when opening a detail in a nested master-detail layout that was
|
|
160
|
+
* not previously visible.
|
|
161
|
+
*
|
|
162
|
+
* NOTE: This method can be expensive in large layouts as it triggers consecutive
|
|
163
|
+
* synchronous DOM reads and writes.
|
|
164
|
+
*/
|
|
165
|
+
recalculateLayout(): void;
|
|
166
|
+
|
|
146
167
|
addEventListener<K extends keyof MasterDetailLayoutEventMap>(
|
|
147
168
|
type: K,
|
|
148
169
|
listener: (this: MasterDetailLayout, ev: MasterDetailLayoutEventMap[K]) => void,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { html, LitElement, nothing } from 'lit';
|
|
7
7
|
import { getFocusableElements } from '@vaadin/a11y-base/src/focus-utils.js';
|
|
8
8
|
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
|
|
9
|
+
import { getClosestElement } from '@vaadin/component-base/src/dom-utils.js';
|
|
9
10
|
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
10
11
|
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
|
|
11
12
|
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
@@ -20,6 +21,18 @@ function parseTrackSizes(gridTemplate) {
|
|
|
20
21
|
.map(parseFloat);
|
|
21
22
|
}
|
|
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
|
+
}
|
|
35
|
+
|
|
23
36
|
/**
|
|
24
37
|
* `<vaadin-master-detail-layout>` is a web component for building UIs with a master
|
|
25
38
|
* (or primary) area and a detail (or secondary) area that is displayed next to, or
|
|
@@ -105,9 +118,15 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
105
118
|
return {
|
|
106
119
|
/**
|
|
107
120
|
* Size (in CSS length units) to be set on the detail area in
|
|
108
|
-
* the CSS grid layout.
|
|
121
|
+
* the CSS grid layout. When there is not enough space to show
|
|
109
122
|
* master and detail areas next to each other, the detail area
|
|
110
|
-
* is shown as an overlay.
|
|
123
|
+
* is shown as an overlay.
|
|
124
|
+
* <p>
|
|
125
|
+
* If not specified, the size is determined automatically by measuring
|
|
126
|
+
* the detail content in a `min-content` CSS grid column when it first
|
|
127
|
+
* becomes visible, and then caching the resulting intrinsic size. To
|
|
128
|
+
* recalculate the cached intrinsic size, use the `recalculateLayout`
|
|
129
|
+
* method.
|
|
111
130
|
*
|
|
112
131
|
* @attr {string} detail-size
|
|
113
132
|
*/
|
|
@@ -155,6 +174,7 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
155
174
|
value: 'horizontal',
|
|
156
175
|
reflectToAttribute: true,
|
|
157
176
|
sync: true,
|
|
177
|
+
observer: '__orientationChanged',
|
|
158
178
|
},
|
|
159
179
|
|
|
160
180
|
/**
|
|
@@ -200,6 +220,13 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
200
220
|
type: Boolean,
|
|
201
221
|
sync: true,
|
|
202
222
|
},
|
|
223
|
+
|
|
224
|
+
/** @private */
|
|
225
|
+
__detailCachedSize: {
|
|
226
|
+
type: String,
|
|
227
|
+
observer: '__detailCachedSizeChanged',
|
|
228
|
+
sync: true,
|
|
229
|
+
},
|
|
203
230
|
};
|
|
204
231
|
}
|
|
205
232
|
|
|
@@ -253,11 +280,26 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
253
280
|
/** @private */
|
|
254
281
|
__masterSizeChanged(size, oldSize) {
|
|
255
282
|
this.__updateStyleProperty('master-size', size, oldSize);
|
|
283
|
+
|
|
284
|
+
if (oldSize != null) {
|
|
285
|
+
this.recalculateLayout();
|
|
286
|
+
}
|
|
256
287
|
}
|
|
257
288
|
|
|
258
289
|
/** @private */
|
|
259
290
|
__detailSizeChanged(size, oldSize) {
|
|
260
291
|
this.__updateStyleProperty('detail-size', size, oldSize);
|
|
292
|
+
|
|
293
|
+
if (oldSize != null) {
|
|
294
|
+
this.recalculateLayout();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** @private */
|
|
299
|
+
__orientationChanged(_orientation, oldOrientation) {
|
|
300
|
+
if (oldOrientation != null) {
|
|
301
|
+
this.recalculateLayout();
|
|
302
|
+
}
|
|
261
303
|
}
|
|
262
304
|
|
|
263
305
|
/** @private */
|
|
@@ -265,6 +307,11 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
265
307
|
this.__updateStyleProperty('overlay-size', size, oldSize);
|
|
266
308
|
}
|
|
267
309
|
|
|
310
|
+
/** @private */
|
|
311
|
+
__detailCachedSizeChanged(size, oldSize) {
|
|
312
|
+
this.__updateStyleProperty('detail-cached-size', size, oldSize);
|
|
313
|
+
}
|
|
314
|
+
|
|
268
315
|
/** @private */
|
|
269
316
|
__updateStyleProperty(prop, size, oldSize) {
|
|
270
317
|
if (size) {
|
|
@@ -297,9 +344,9 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
297
344
|
* @private
|
|
298
345
|
*/
|
|
299
346
|
__onResize() {
|
|
300
|
-
const state = this.
|
|
347
|
+
const state = this.__readLayoutState();
|
|
301
348
|
cancelAnimationFrame(this.__resizeRaf);
|
|
302
|
-
this.__resizeRaf = requestAnimationFrame(() => this.
|
|
349
|
+
this.__resizeRaf = requestAnimationFrame(() => this.__writeLayoutState(state));
|
|
303
350
|
}
|
|
304
351
|
|
|
305
352
|
/**
|
|
@@ -307,26 +354,56 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
307
354
|
* ResizeObserver callback where layout is already computed (no forced reflow).
|
|
308
355
|
* @private
|
|
309
356
|
*/
|
|
310
|
-
|
|
357
|
+
__readLayoutState() {
|
|
358
|
+
const isVertical = this.orientation === 'vertical';
|
|
359
|
+
|
|
311
360
|
const detailContent = this.querySelector(':scope > [slot="detail"]');
|
|
312
361
|
const detailPlaceholder = this.querySelector(':scope > [slot="detail-placeholder"]');
|
|
313
362
|
|
|
314
363
|
const hadDetail = this.hasAttribute('has-detail');
|
|
315
364
|
const hasDetail = detailContent != null && detailContent.checkVisibility();
|
|
316
365
|
const hasDetailPlaceholder = !!detailPlaceholder;
|
|
317
|
-
const hasOverflow = (hasDetail || hasDetailPlaceholder) && this.__checkOverflow();
|
|
318
366
|
|
|
367
|
+
const computedStyle = getComputedStyle(this);
|
|
368
|
+
const hostSizeProp = isVertical ? 'height' : 'width';
|
|
369
|
+
const hostSize = parseFloat(computedStyle[hostSizeProp]);
|
|
370
|
+
|
|
371
|
+
const trackSizesProp = isVertical ? 'gridTemplateRows' : 'gridTemplateColumns';
|
|
372
|
+
const trackSizes = parseTrackSizes(computedStyle[trackSizesProp]);
|
|
373
|
+
|
|
374
|
+
const hasOverflow = (hasDetail || hasDetailPlaceholder) && detectOverflow(hostSize, trackSizes);
|
|
319
375
|
const focusTarget = !hadDetail && hasDetail && hasOverflow ? getFocusableElements(detailContent)[0] : null;
|
|
320
|
-
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
hadDetail,
|
|
379
|
+
hasDetail,
|
|
380
|
+
hasDetailPlaceholder,
|
|
381
|
+
hasOverflow,
|
|
382
|
+
focusTarget,
|
|
383
|
+
hostSize,
|
|
384
|
+
trackSizes,
|
|
385
|
+
};
|
|
321
386
|
}
|
|
322
387
|
|
|
323
388
|
/**
|
|
324
389
|
* Applies layout state to DOM attributes. Pure writes, no reads.
|
|
325
390
|
* @private
|
|
326
391
|
*/
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
392
|
+
__writeLayoutState({ hadDetail, hasDetail, hasDetailPlaceholder, hasOverflow, focusTarget, trackSizes }) {
|
|
393
|
+
const [_masterSize, _masterExtra, detailSize] = trackSizes;
|
|
394
|
+
|
|
395
|
+
// If no detailSize is explicitily set, cache the intrinsic size (min-content) of
|
|
396
|
+
// the slotted detail content to use as a fallback for the detail column size
|
|
397
|
+
// while the detail content is rendered in an overlay.
|
|
398
|
+
if ((hasDetail || hasDetailPlaceholder) && this.__isDetailAutoSized && detailSize > 0) {
|
|
399
|
+
this.__detailCachedSize = this.__detailCachedSize || `${Math.ceil(detailSize)}px`;
|
|
400
|
+
} else {
|
|
401
|
+
this.__detailCachedSize = null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Force the detail column offscreen when it first appears and overflow
|
|
405
|
+
// is already detected. This prevents unnecessary master column shrinking,
|
|
406
|
+
// as the detail content is rendered in an overlay anyway.
|
|
330
407
|
if (!hadDetail && hasDetail && hasOverflow) {
|
|
331
408
|
this.setAttribute('keep-detail-column-offscreen', '');
|
|
332
409
|
} else if (!hasDetail || !hasOverflow) {
|
|
@@ -346,23 +423,59 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
346
423
|
}
|
|
347
424
|
}
|
|
348
425
|
|
|
349
|
-
/**
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
426
|
+
/**
|
|
427
|
+
* When `detailSize` is not explicitly set, re-measures the cached intrinsic size of
|
|
428
|
+
* the detail content by placing it in a min-content CSS grid column, then repeats
|
|
429
|
+
* this process for ancestor master-detail layouts without an explicit `detailSize`,
|
|
430
|
+
* if any, so that their detail areas also adapt.
|
|
431
|
+
*
|
|
432
|
+
* Call this method after changing the detail content in a way that affects its intrinsic
|
|
433
|
+
* size — for example, when opening a detail in a nested master-detail layout that was
|
|
434
|
+
* not previously visible.
|
|
435
|
+
*
|
|
436
|
+
* NOTE: This method can be expensive in large layouts as it triggers consecutive
|
|
437
|
+
* synchronous DOM reads and writes.
|
|
438
|
+
*/
|
|
439
|
+
recalculateLayout() {
|
|
440
|
+
// Cancel any pending ResizeObserver rAF to prevent it from potentially
|
|
441
|
+
// overriding the layout state with stale measurements.
|
|
442
|
+
cancelAnimationFrame(this.__resizeRaf);
|
|
353
443
|
|
|
354
|
-
const
|
|
355
|
-
const [masterSize, masterExtra, detailSize] = parseTrackSizes(
|
|
356
|
-
computedStyle[isVertical ? 'gridTemplateRows' : 'gridTemplateColumns'],
|
|
357
|
-
);
|
|
444
|
+
const invalidatedLayouts = [...this.__ancestorLayouts.filter((layout) => layout.__isDetailAutoSized), this];
|
|
358
445
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
446
|
+
// Write
|
|
447
|
+
invalidatedLayouts.forEach((layout) => {
|
|
448
|
+
layout.__detailCachedSize = null;
|
|
449
|
+
|
|
450
|
+
if (layout.__isDetailAutoSized) {
|
|
451
|
+
layout.removeAttribute('overlay');
|
|
452
|
+
layout.toggleAttribute('recalculating-detail-size', true);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Read/Write
|
|
457
|
+
invalidatedLayouts.forEach((layout) => {
|
|
458
|
+
const state = layout.__readLayoutState();
|
|
459
|
+
layout.__writeLayoutState(state);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Write
|
|
463
|
+
invalidatedLayouts.forEach((layout) => {
|
|
464
|
+
if (layout.__isDetailAutoSized) {
|
|
465
|
+
layout.toggleAttribute('recalculating-detail-size', false);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** @private */
|
|
471
|
+
get __isDetailAutoSized() {
|
|
472
|
+
return this.detailSize == null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** @private */
|
|
476
|
+
get __ancestorLayouts() {
|
|
477
|
+
const parent = getClosestElement(this.constructor.is, this.parentNode);
|
|
478
|
+
return parent ? [...parent.__ancestorLayouts, parent] : [];
|
|
366
479
|
}
|
|
367
480
|
|
|
368
481
|
/** @private */
|
|
@@ -413,10 +526,7 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
413
526
|
|
|
414
527
|
if (skipTransition || this.noAnimation) {
|
|
415
528
|
updateSlot();
|
|
416
|
-
queueMicrotask(() =>
|
|
417
|
-
const state = this.__computeLayoutState();
|
|
418
|
-
this.__applyLayoutState(state);
|
|
419
|
-
});
|
|
529
|
+
queueMicrotask(() => this.recalculateLayout());
|
|
420
530
|
return Promise.resolve();
|
|
421
531
|
}
|
|
422
532
|
|
|
@@ -462,23 +572,25 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
462
572
|
* Starts an animated transition for adding, replacing or removing the
|
|
463
573
|
* detail area using the Web Animations API.
|
|
464
574
|
*
|
|
465
|
-
* For '
|
|
466
|
-
*
|
|
467
|
-
*
|
|
575
|
+
* For 'add'/'replace': DOM is updated immediately, then animation
|
|
576
|
+
* starts after a microtask (so Lit elements render and layout is
|
|
577
|
+
* recalculated before animation params are read).
|
|
578
|
+
*
|
|
579
|
+
* For 'remove': animation plays first, then DOM is updated after
|
|
580
|
+
* the slide-out completes.
|
|
468
581
|
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
* interrupted position (see `__captureDetailState`).
|
|
582
|
+
* Interruptible: a new transition cancels any in-progress animation
|
|
583
|
+
* and picks up from the interrupted position.
|
|
472
584
|
*
|
|
473
585
|
* @param transitionType
|
|
474
586
|
* @param updateCallback
|
|
475
587
|
* @return {Promise<void>}
|
|
476
588
|
* @protected
|
|
477
589
|
*/
|
|
478
|
-
_startTransition(transitionType, updateCallback) {
|
|
590
|
+
async _startTransition(transitionType, updateCallback) {
|
|
479
591
|
if (this.noAnimation) {
|
|
480
592
|
updateCallback();
|
|
481
|
-
return
|
|
593
|
+
return;
|
|
482
594
|
}
|
|
483
595
|
|
|
484
596
|
// Capture mid-flight state before cancelling active animations
|
|
@@ -492,66 +604,67 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
492
604
|
|
|
493
605
|
this.setAttribute('transition', transitionType);
|
|
494
606
|
|
|
607
|
+
const version = (this.__transitionVersion = (this.__transitionVersion || 0) + 1);
|
|
608
|
+
|
|
495
609
|
if (transitionType !== 'remove') {
|
|
610
|
+
// Add/Replace: update DOM, wait for Lit rendering + recalculateLayout
|
|
496
611
|
updateCallback();
|
|
612
|
+
await Promise.resolve();
|
|
613
|
+
if (this.__transitionVersion !== version) return;
|
|
497
614
|
}
|
|
498
615
|
|
|
499
616
|
const opts = this.__getAnimationParams();
|
|
500
617
|
opts.interrupted = interrupted;
|
|
501
618
|
opts.overlay = this.hasAttribute('overlay');
|
|
502
619
|
|
|
503
|
-
|
|
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();
|
|
504
631
|
}
|
|
505
632
|
|
|
506
633
|
/**
|
|
507
|
-
*
|
|
508
|
-
* a promise that resolves when the
|
|
509
|
-
* A version counter prevents stale callbacks from executing after
|
|
510
|
-
* a newer transition has started.
|
|
634
|
+
* Starts slide animation(s) for the given transition type and returns
|
|
635
|
+
* a promise that resolves when the detail slide completes.
|
|
511
636
|
*
|
|
512
637
|
* @param {string} transitionType
|
|
513
638
|
* @param {{ offscreen: string, duration: number, easing: string, interrupted?: { translate: string, opacity: string }, overlay?: boolean }} opts
|
|
514
|
-
* @param {Function} updateCallback
|
|
515
639
|
* @return {Promise<void>}
|
|
516
640
|
* @private
|
|
517
641
|
*/
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
this.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
this.__endTransition();
|
|
530
|
-
}
|
|
531
|
-
};
|
|
532
|
-
|
|
533
|
-
if (transitionType === 'remove') {
|
|
534
|
-
this.__slide(this.$.detail, false, opts).then(() => onFinish(updateCallback));
|
|
535
|
-
} else if (transitionType === 'replace') {
|
|
536
|
-
// Outgoing slides out on top (z-index), revealing incoming underneath.
|
|
537
|
-
// In overlay mode, the incoming also slides in simultaneously.
|
|
538
|
-
this.__slide(this.$.outgoing, false, opts).then(() => onFinish());
|
|
539
|
-
if (opts.overlay) {
|
|
540
|
-
this.__slide(this.$.detail, true, { ...opts, interrupted: null });
|
|
541
|
-
}
|
|
542
|
-
} else {
|
|
543
|
-
this.__slide(this.$.detail, true, opts).then(() => onFinish());
|
|
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 });
|
|
544
653
|
}
|
|
654
|
+
} else {
|
|
655
|
+
slide = this.__slide(this.$.detail, true, opts);
|
|
656
|
+
}
|
|
545
657
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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;
|
|
555
668
|
}
|
|
556
669
|
|
|
557
670
|
/**
|
|
@@ -561,8 +674,7 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
561
674
|
* @protected
|
|
562
675
|
*/
|
|
563
676
|
_finishTransition() {
|
|
564
|
-
|
|
565
|
-
this.__applyLayoutState(state);
|
|
677
|
+
queueMicrotask(() => this.recalculateLayout());
|
|
566
678
|
}
|
|
567
679
|
|
|
568
680
|
/**
|
|
@@ -659,8 +771,7 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
659
771
|
}
|
|
660
772
|
|
|
661
773
|
/**
|
|
662
|
-
* Cancels in-progress animations
|
|
663
|
-
* pending transition promise.
|
|
774
|
+
* Cancels in-progress animations and cleans up transition state.
|
|
664
775
|
* @private
|
|
665
776
|
*/
|
|
666
777
|
__endTransition() {
|
|
@@ -670,14 +781,6 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
670
781
|
}
|
|
671
782
|
this.removeAttribute('transition');
|
|
672
783
|
this.__clearOutgoing();
|
|
673
|
-
// Cancel any pending ResizeObserver rAF that captured stale state
|
|
674
|
-
// during the animation — _finishTransition already applied the
|
|
675
|
-
// correct post-transition state synchronously.
|
|
676
|
-
cancelAnimationFrame(this.__resizeRaf);
|
|
677
|
-
if (this.__transitionResolve) {
|
|
678
|
-
this.__transitionResolve();
|
|
679
|
-
this.__transitionResolve = null;
|
|
680
|
-
}
|
|
681
784
|
}
|
|
682
785
|
|
|
683
786
|
/**
|
|
@@ -692,6 +795,7 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
692
795
|
return;
|
|
693
796
|
}
|
|
694
797
|
currentDetail.setAttribute('slot', 'detail-outgoing');
|
|
798
|
+
this.$.outgoing.style.width = this.__detailCachedSize;
|
|
695
799
|
this.__replacing = true;
|
|
696
800
|
}
|
|
697
801
|
|
|
@@ -701,6 +805,7 @@ class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElem
|
|
|
701
805
|
*/
|
|
702
806
|
__clearOutgoing() {
|
|
703
807
|
this.querySelectorAll('[slot="detail-outgoing"]').forEach((el) => el.remove());
|
|
808
|
+
this.$.outgoing.style.width = '';
|
|
704
809
|
this.__replacing = false;
|
|
705
810
|
}
|
|
706
811
|
|
package/web-types.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/web-types",
|
|
3
3
|
"name": "@vaadin/master-detail-layout",
|
|
4
|
-
"version": "25.2.0-
|
|
4
|
+
"version": "25.2.0-alpha5",
|
|
5
5
|
"description-markup": "markdown",
|
|
6
6
|
"contributions": {
|
|
7
7
|
"html": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"attributes": [
|
|
13
13
|
{
|
|
14
14
|
"name": "detail-size",
|
|
15
|
-
"description": "Size (in CSS length units) to be set on the detail area in\nthe CSS grid layout.
|
|
15
|
+
"description": "Size (in CSS length units) to be set on the detail area in\nthe CSS grid layout. When there is not enough space to show\nmaster and detail areas next to each other, the detail area\nis shown as an overlay.\n<p>\nIf not specified, the size is determined automatically by measuring\nthe detail content in a `min-content` CSS grid column when it first\nbecomes visible, and then caching the resulting intrinsic size. To\nrecalculate the cached intrinsic size, use the `recalculateLayout`\nmethod.",
|
|
16
16
|
"value": {
|
|
17
17
|
"type": [
|
|
18
18
|
"string",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"properties": [
|
|
104
104
|
{
|
|
105
105
|
"name": "detailSize",
|
|
106
|
-
"description": "Size (in CSS length units) to be set on the detail area in\nthe CSS grid layout.
|
|
106
|
+
"description": "Size (in CSS length units) to be set on the detail area in\nthe CSS grid layout. When there is not enough space to show\nmaster and detail areas next to each other, the detail area\nis shown as an overlay.\n<p>\nIf not specified, the size is determined automatically by measuring\nthe detail content in a `min-content` CSS grid column when it first\nbecomes visible, and then caching the resulting intrinsic size. To\nrecalculate the cached intrinsic size, use the `recalculateLayout`\nmethod.",
|
|
107
107
|
"value": {
|
|
108
108
|
"type": [
|
|
109
109
|
"string",
|
package/web-types.lit.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/web-types",
|
|
3
3
|
"name": "@vaadin/master-detail-layout",
|
|
4
|
-
"version": "25.2.0-
|
|
4
|
+
"version": "25.2.0-alpha5",
|
|
5
5
|
"description-markup": "markdown",
|
|
6
6
|
"framework": "lit",
|
|
7
7
|
"framework-config": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"name": ".detailSize",
|
|
31
|
-
"description": "Size (in CSS length units) to be set on the detail area in\nthe CSS grid layout.
|
|
31
|
+
"description": "Size (in CSS length units) to be set on the detail area in\nthe CSS grid layout. When there is not enough space to show\nmaster and detail areas next to each other, the detail area\nis shown as an overlay.\n<p>\nIf not specified, the size is determined automatically by measuring\nthe detail content in a `min-content` CSS grid column when it first\nbecomes visible, and then caching the resulting intrinsic size. To\nrecalculate the cached intrinsic size, use the `recalculateLayout`\nmethod.",
|
|
32
32
|
"value": {
|
|
33
33
|
"kind": "expression"
|
|
34
34
|
}
|