@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.
@@ -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. If there is not enough space to show\nmaster and detail areas next to each other, the detail area\nis shown as an overlay. Defaults to 15em.",
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. If there is not enough space to show\nmaster and detail areas next to each other, the detail area\nis shown as an overlay. Defaults to 15em.",
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-alpha3",
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-alpha3",
38
- "@vaadin/component-base": "25.2.0-alpha3",
39
- "@vaadin/vaadin-themable-mixin": "25.2.0-alpha3",
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-alpha3",
44
- "@vaadin/chai-plugins": "25.2.0-alpha3",
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-alpha3",
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": "6ba3d66b9eeb541945dc071e72e05dac2d4c3e0b"
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: 30em;
12
- --_detail-size: 15em;
13
- --_master-column: var(--_master-size) 0;
14
- --_detail-column: var(--_detail-size) 0;
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: hidden;
26
- grid-template-columns: [master-start] var(--_master-column) [detail-start] var(--_detail-column) [detail-end];
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: [master-start] var(--_master-column) [detail-start] var(--_detail-column) [detail-end];
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
- display: none;
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
- display: block;
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: 1;
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-column: var(--_master-size) 1fr;
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-column: var(--_master-size) calc(100% - var(--_master-size));
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'][has-detail]:not([overlay])) #detail {
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'][has-detail]:not([overlay])) #detail {
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, min-content));
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, min-content));
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. If there is not enough space to show
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. Defaults to 15em.
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. If there is not enough space to show
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. Defaults to 15em.
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.__computeLayoutState();
347
+ const state = this.__readLayoutState();
301
348
  cancelAnimationFrame(this.__resizeRaf);
302
- this.__resizeRaf = requestAnimationFrame(() => this.__applyLayoutState(state));
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
- __computeLayoutState() {
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
- return { hadDetail, hasDetail, hasDetailPlaceholder, hasOverflow, focusTarget };
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
- __applyLayoutState({ hadDetail, hasDetail, hasDetailPlaceholder, hasOverflow, focusTarget }) {
328
- // Set keep-detail-column-offscreen when detail first appears with overlay
329
- // to prevent master width from jumping.
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
- /** @private */
350
- __checkOverflow() {
351
- const isVertical = this.orientation === 'vertical';
352
- const computedStyle = getComputedStyle(this);
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 hostSize = parseFloat(computedStyle[isVertical ? 'height' : 'width']);
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
- if (Math.floor(masterSize + masterExtra + detailSize) <= Math.floor(hostSize)) {
360
- return false;
361
- }
362
- if (Math.floor(masterExtra) >= Math.floor(detailSize)) {
363
- return false;
364
- }
365
- return true;
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 'remove', the DOM update is deferred until the slide-out completes.
466
- * For 'add'/'replace', the DOM is updated immediately and the slide-in
467
- * plays on the new content.
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
- * Animations are interruptible: starting a new transition cancels any
470
- * in-progress animation and the new animation picks up from the
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 Promise.resolve();
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
- return this.__animateTransition(transitionType, opts, updateCallback);
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
- * Creates slide animation(s) for the given transition type and returns
508
- * a promise that resolves when the primary animation completes.
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
- __animateTransition(transitionType, opts, updateCallback) {
519
- const version = (this.__transitionVersion = (this.__transitionVersion || 0) + 1);
520
-
521
- return new Promise((resolve) => {
522
- this.__transitionResolve = resolve;
523
-
524
- const onFinish = (callback) => {
525
- if (this.__transitionVersion === version) {
526
- if (callback) {
527
- callback();
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
- // Fade backdrop in/out for overlay add/remove (not replace — backdrop stays visible)
547
- if (opts.overlay && transitionType !== 'replace') {
548
- const fadeIn = transitionType !== 'remove';
549
- this.__animate(this.$.backdrop, [{ opacity: fadeIn ? 0 : 1 }, { opacity: fadeIn ? 1 : 0 }], {
550
- duration: opts.duration,
551
- easing: 'linear',
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
- const state = this.__computeLayoutState();
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, cleans up state, and resolves the
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-alpha3",
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. If there is not enough space to show\nmaster and detail areas next to each other, the detail area\nis shown as an overlay. Defaults to 15em.",
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. If there is not enough space to show\nmaster and detail areas next to each other, the detail area\nis shown as an overlay. Defaults to 15em.",
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",
@@ -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-alpha3",
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. If there is not enough space to show\nmaster and detail areas next to each other, the detail area\nis shown as an overlay. Defaults to 15em.",
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
  }