@vaadin/master-detail-layout 25.2.0-alpha1 → 25.2.0-alpha10
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 +272 -25
- package/package.json +8 -8
- package/src/styles/vaadin-master-detail-layout-base-styles.js +219 -56
- package/src/vaadin-master-detail-layout-helpers.js +173 -0
- package/src/vaadin-master-detail-layout.d.ts +90 -22
- package/src/vaadin-master-detail-layout.js +393 -186
- package/web-types.json +62 -54
- package/web-types.lit.json +25 -11
- package/src/styles/vaadin-master-detail-layout-transition-base-styles.js +0 -107
|
@@ -4,48 +4,74 @@
|
|
|
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
|
+
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
|
-
import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-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
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.map(parseFloat);
|
|
23
|
-
}
|
|
14
|
+
import {
|
|
15
|
+
animateIn,
|
|
16
|
+
animateOut,
|
|
17
|
+
cancelAnimations,
|
|
18
|
+
detectOverflow,
|
|
19
|
+
getCurrentAnimationProgress,
|
|
20
|
+
parseTrackSizes,
|
|
21
|
+
} from './vaadin-master-detail-layout-helpers.js';
|
|
24
22
|
|
|
25
23
|
/**
|
|
26
24
|
* `<vaadin-master-detail-layout>` is a web component for building UIs with a master
|
|
27
25
|
* (or primary) area and a detail (or secondary) area that is displayed next to, or
|
|
28
26
|
* overlaid on top of, the master area, depending on configuration and viewport size.
|
|
29
27
|
*
|
|
28
|
+
* ### Slots
|
|
29
|
+
*
|
|
30
|
+
* The component has two main content areas: the master area (default slot)
|
|
31
|
+
* and the detail area (`detail` slot). When the detail doesn't fit next to
|
|
32
|
+
* the master, it is shown as an overlay on top of the master area:
|
|
33
|
+
*
|
|
34
|
+
* ```html
|
|
35
|
+
* <vaadin-master-detail-layout>
|
|
36
|
+
* <div>Master content</div>
|
|
37
|
+
* <div slot="detail">Detail content</div>
|
|
38
|
+
* </vaadin-master-detail-layout>
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* The component also supports a `detail-placeholder` slot for content shown
|
|
42
|
+
* in the detail area when no detail is selected. Unlike the `detail` slot,
|
|
43
|
+
* the placeholder is simply hidden when it doesn't fit next to the master area,
|
|
44
|
+
* rather than shown as an overlay:
|
|
45
|
+
*
|
|
46
|
+
* ```html
|
|
47
|
+
* <vaadin-master-detail-layout>
|
|
48
|
+
* <div>Master content</div>
|
|
49
|
+
* <div slot="detail-placeholder">Select an item</div>
|
|
50
|
+
* </vaadin-master-detail-layout>
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
30
53
|
* ### Styling
|
|
31
54
|
*
|
|
32
55
|
* The following shadow DOM parts are available for styling:
|
|
33
56
|
*
|
|
34
|
-
* Part name
|
|
35
|
-
*
|
|
36
|
-
* `backdrop`
|
|
37
|
-
* `master`
|
|
38
|
-
* `detail`
|
|
57
|
+
* Part name | Description
|
|
58
|
+
* ----------------------|----------------------
|
|
59
|
+
* `backdrop` | Backdrop covering the master area in the overlay mode
|
|
60
|
+
* `master` | The master area
|
|
61
|
+
* `detail` | The detail area
|
|
62
|
+
* `detail-placeholder` | The detail placeholder area
|
|
39
63
|
*
|
|
40
64
|
* The following state attributes are available for styling:
|
|
41
65
|
*
|
|
42
|
-
* Attribute
|
|
43
|
-
*
|
|
44
|
-
* `expand`
|
|
45
|
-
* `
|
|
46
|
-
* `
|
|
47
|
-
* `
|
|
48
|
-
* `
|
|
66
|
+
* Attribute | Description
|
|
67
|
+
* --------------------------|----------------------
|
|
68
|
+
* `expand-master` | Set when the master area expands to fill available space.
|
|
69
|
+
* `expand-detail` | Set when the detail area expands to fill available space.
|
|
70
|
+
* `orientation` | Set to `horizontal` or `vertical` depending on the orientation.
|
|
71
|
+
* `has-detail` | Set when the detail content is provided and visible.
|
|
72
|
+
* `has-detail-placeholder` | Set when the detail placeholder content is provided.
|
|
73
|
+
* `overlay` | Set when columns don't fit and the detail is shown as an overlay.
|
|
74
|
+
* `overlay-containment` | Set to `layout` or `page`.
|
|
49
75
|
*
|
|
50
76
|
* The following custom CSS properties are available for styling:
|
|
51
77
|
*
|
|
@@ -66,9 +92,8 @@ function parseTrackSizes(gridTemplate) {
|
|
|
66
92
|
* @extends HTMLElement
|
|
67
93
|
* @mixes ThemableMixin
|
|
68
94
|
* @mixes ElementMixin
|
|
69
|
-
* @mixes SlotStylesMixin
|
|
70
95
|
*/
|
|
71
|
-
class MasterDetailLayout extends
|
|
96
|
+
class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))) {
|
|
72
97
|
static get is() {
|
|
73
98
|
return 'vaadin-master-detail-layout';
|
|
74
99
|
}
|
|
@@ -81,16 +106,21 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
81
106
|
return {
|
|
82
107
|
/**
|
|
83
108
|
* Size (in CSS length units) to be set on the detail area in
|
|
84
|
-
* the CSS grid layout.
|
|
109
|
+
* the CSS grid layout. When there is not enough space to show
|
|
85
110
|
* master and detail areas next to each other, the detail area
|
|
86
|
-
* is shown as an overlay.
|
|
111
|
+
* is shown as an overlay.
|
|
112
|
+
* <p>
|
|
113
|
+
* If not specified, the size is determined automatically by measuring
|
|
114
|
+
* the detail content in a `min-content` CSS grid column when it first
|
|
115
|
+
* becomes visible, and then caching the resulting intrinsic size. To
|
|
116
|
+
* recalculate the cached intrinsic size, use the `recalculateLayout`
|
|
117
|
+
* method.
|
|
87
118
|
*
|
|
88
119
|
* @attr {string} detail-size
|
|
89
120
|
*/
|
|
90
121
|
detailSize: {
|
|
91
122
|
type: String,
|
|
92
123
|
sync: true,
|
|
93
|
-
observer: '__detailSizeChanged',
|
|
94
124
|
},
|
|
95
125
|
|
|
96
126
|
/**
|
|
@@ -104,7 +134,6 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
104
134
|
masterSize: {
|
|
105
135
|
type: String,
|
|
106
136
|
sync: true,
|
|
107
|
-
observer: '__masterSizeChanged',
|
|
108
137
|
},
|
|
109
138
|
|
|
110
139
|
/**
|
|
@@ -136,7 +165,7 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
136
165
|
/**
|
|
137
166
|
* Defines the containment of the detail area when the layout is in
|
|
138
167
|
* overlay mode. When set to `layout`, the overlay is confined to the
|
|
139
|
-
* layout. When set to `
|
|
168
|
+
* layout. When set to `page`, the overlay is confined to the
|
|
140
169
|
* browser's viewport. Defaults to `layout`.
|
|
141
170
|
*
|
|
142
171
|
* @attr {string} overlay-containment
|
|
@@ -149,13 +178,29 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
149
178
|
},
|
|
150
179
|
|
|
151
180
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
181
|
+
* When true, the master area grows to fill the available space.
|
|
182
|
+
* If `expandDetail` is also true, both areas share the available
|
|
183
|
+
* space equally.
|
|
184
|
+
*
|
|
185
|
+
* @attr {boolean} expand-master
|
|
155
186
|
*/
|
|
156
|
-
|
|
157
|
-
type:
|
|
158
|
-
value:
|
|
187
|
+
expandMaster: {
|
|
188
|
+
type: Boolean,
|
|
189
|
+
value: false,
|
|
190
|
+
reflectToAttribute: true,
|
|
191
|
+
sync: true,
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* When true, the detail area grows to fill the available space.
|
|
196
|
+
* If `expandMaster` is also true, both areas share the available
|
|
197
|
+
* space equally.
|
|
198
|
+
*
|
|
199
|
+
* @attr {boolean} expand-detail
|
|
200
|
+
*/
|
|
201
|
+
expandDetail: {
|
|
202
|
+
type: Boolean,
|
|
203
|
+
value: false,
|
|
159
204
|
reflectToAttribute: true,
|
|
160
205
|
sync: true,
|
|
161
206
|
},
|
|
@@ -168,6 +213,28 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
168
213
|
noAnimation: {
|
|
169
214
|
type: Boolean,
|
|
170
215
|
value: false,
|
|
216
|
+
reflectToAttribute: true,
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* When true, the layout forces the detail area to be shown as an overlay,
|
|
221
|
+
* even if there is enough space for master and detail to be shown next to
|
|
222
|
+
* each other using the default (split) mode.
|
|
223
|
+
*
|
|
224
|
+
* @attr {boolean} force-overlay
|
|
225
|
+
*/
|
|
226
|
+
forceOverlay: {
|
|
227
|
+
type: Boolean,
|
|
228
|
+
value: false,
|
|
229
|
+
reflectToAttribute: true,
|
|
230
|
+
sync: true,
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
/** @private */
|
|
234
|
+
__detailCachedSize: {
|
|
235
|
+
type: String,
|
|
236
|
+
observer: '__detailCachedSizeChanged',
|
|
237
|
+
sync: true,
|
|
171
238
|
},
|
|
172
239
|
};
|
|
173
240
|
}
|
|
@@ -176,31 +243,32 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
176
243
|
return true;
|
|
177
244
|
}
|
|
178
245
|
|
|
179
|
-
/** @return {!Array<!CSSResult>} */
|
|
180
|
-
get slotStyles() {
|
|
181
|
-
return [masterDetailLayoutTransitionStyles];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
246
|
/** @protected */
|
|
185
247
|
render() {
|
|
186
|
-
const isOverlay = this.hasAttribute('has-detail') && this.hasAttribute('
|
|
187
|
-
const
|
|
188
|
-
const isLayoutContained = isOverlay && !
|
|
248
|
+
const isOverlay = this.hasAttribute('has-detail') && this.hasAttribute('overlay');
|
|
249
|
+
const isPage = isOverlay && this.overlayContainment === 'page';
|
|
250
|
+
const isLayoutContained = isOverlay && !isPage;
|
|
189
251
|
|
|
190
252
|
return html`
|
|
191
|
-
<div part="backdrop" @click="${this.__onBackdropClick}"></div>
|
|
253
|
+
<div id="backdrop" part="backdrop" @click="${this.__onBackdropClick}"></div>
|
|
192
254
|
<div id="master" part="master" ?inert="${isLayoutContained}">
|
|
193
255
|
<slot @slotchange="${this.__onSlotChange}"></slot>
|
|
194
256
|
</div>
|
|
257
|
+
<div id="detailOutgoing" inert>
|
|
258
|
+
<slot name="detail-outgoing"></slot>
|
|
259
|
+
</div>
|
|
195
260
|
<div
|
|
196
261
|
id="detail"
|
|
197
262
|
part="detail"
|
|
198
263
|
role="${isOverlay ? 'dialog' : nothing}"
|
|
199
|
-
aria-modal="${
|
|
264
|
+
aria-modal="${isPage ? 'true' : nothing}"
|
|
200
265
|
@keydown="${this.__onDetailKeydown}"
|
|
201
266
|
>
|
|
202
267
|
<slot name="detail" @slotchange="${this.__onSlotChange}"></slot>
|
|
203
268
|
</div>
|
|
269
|
+
<div id="detailPlaceholder" part="detail-placeholder">
|
|
270
|
+
<slot name="detail-placeholder" @slotchange="${this.__onSlotChange}"></slot>
|
|
271
|
+
</div>
|
|
204
272
|
`;
|
|
205
273
|
}
|
|
206
274
|
|
|
@@ -208,6 +276,17 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
208
276
|
connectedCallback() {
|
|
209
277
|
super.connectedCallback();
|
|
210
278
|
this.__initResizeObserver();
|
|
279
|
+
|
|
280
|
+
const ancestorLayouts = this.__ancestorLayouts;
|
|
281
|
+
if (ancestorLayouts.length > 0) {
|
|
282
|
+
ancestorLayouts.forEach((layout) => {
|
|
283
|
+
cancelAnimationFrame(layout.__initialRaf);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
this.__initialRaf = requestAnimationFrame(() => {
|
|
287
|
+
this.recalculateLayout();
|
|
288
|
+
});
|
|
289
|
+
}
|
|
211
290
|
}
|
|
212
291
|
|
|
213
292
|
/** @protected */
|
|
@@ -215,30 +294,40 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
215
294
|
super.disconnectedCallback();
|
|
216
295
|
this.__resizeObserver.disconnect();
|
|
217
296
|
cancelAnimationFrame(this.__resizeRaf);
|
|
297
|
+
cancelAnimationFrame(this.__initialRaf);
|
|
298
|
+
cancelAnimations(this);
|
|
218
299
|
}
|
|
219
300
|
|
|
220
|
-
/** @
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
301
|
+
/** @protected */
|
|
302
|
+
updated(props) {
|
|
303
|
+
super.updated(props);
|
|
224
304
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
305
|
+
if (props.has('masterSize')) {
|
|
306
|
+
this.style.setProperty('--_master-size', this.masterSize);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (props.has('detailSize')) {
|
|
310
|
+
this.style.setProperty('--_detail-size', this.detailSize);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (
|
|
314
|
+
(props.has('masterSize') && props.get('masterSize') != null) ||
|
|
315
|
+
(props.has('detailSize') && props.get('detailSize') != null) ||
|
|
316
|
+
(props.has('orientation') && props.get('orientation') != null) ||
|
|
317
|
+
(props.has('forceOverlay') && props.get('forceOverlay') != null)
|
|
318
|
+
) {
|
|
319
|
+
this.recalculateLayout();
|
|
320
|
+
}
|
|
228
321
|
}
|
|
229
322
|
|
|
230
323
|
/** @private */
|
|
231
|
-
__overlaySizeChanged(size
|
|
232
|
-
this.
|
|
324
|
+
__overlaySizeChanged(size) {
|
|
325
|
+
this.style.setProperty('--_overlay-size', size);
|
|
233
326
|
}
|
|
234
327
|
|
|
235
328
|
/** @private */
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
this.style.setProperty(`--_${prop}`, size);
|
|
239
|
-
} else if (oldSize) {
|
|
240
|
-
this.style.removeProperty(`--_${prop}`);
|
|
241
|
-
}
|
|
329
|
+
__detailCachedSizeChanged(size) {
|
|
330
|
+
this.style.setProperty('--_detail-cached-size', size);
|
|
242
331
|
}
|
|
243
332
|
|
|
244
333
|
/** @private */
|
|
@@ -248,12 +337,13 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
248
337
|
|
|
249
338
|
/** @private */
|
|
250
339
|
__initResizeObserver() {
|
|
251
|
-
this.__resizeObserver
|
|
340
|
+
this.__resizeObserver ||= new ResizeObserver(() => this.__onResize());
|
|
252
341
|
this.__resizeObserver.disconnect();
|
|
253
342
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
343
|
+
[this, this.$.master, this.$.detail, this.__slottedMaster, this.__slottedDetail].forEach((node) => {
|
|
344
|
+
if (node) {
|
|
345
|
+
this.__resizeObserver.observe(node);
|
|
346
|
+
}
|
|
257
347
|
});
|
|
258
348
|
}
|
|
259
349
|
|
|
@@ -264,9 +354,9 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
264
354
|
* @private
|
|
265
355
|
*/
|
|
266
356
|
__onResize() {
|
|
267
|
-
const state = this.
|
|
357
|
+
const state = this.__readLayoutState();
|
|
268
358
|
cancelAnimationFrame(this.__resizeRaf);
|
|
269
|
-
this.__resizeRaf = requestAnimationFrame(() => this.
|
|
359
|
+
this.__resizeRaf = requestAnimationFrame(() => this.__writeLayoutState(state));
|
|
270
360
|
}
|
|
271
361
|
|
|
272
362
|
/**
|
|
@@ -274,57 +364,133 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
274
364
|
* ResizeObserver callback where layout is already computed (no forced reflow).
|
|
275
365
|
* @private
|
|
276
366
|
*/
|
|
277
|
-
|
|
278
|
-
const
|
|
367
|
+
__readLayoutState() {
|
|
368
|
+
const isVertical = this.orientation === 'vertical';
|
|
369
|
+
|
|
370
|
+
const slottedMaster = this.__slottedMaster;
|
|
371
|
+
const slottedDetail = this.__slottedDetail;
|
|
372
|
+
const slottedDetailPlaceholder = this.__slottedDetailPlaceholder;
|
|
373
|
+
|
|
374
|
+
const hasMaster = !!slottedMaster;
|
|
279
375
|
const hadDetail = this.hasAttribute('has-detail');
|
|
280
|
-
const hasDetail =
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
376
|
+
const hasDetail = slottedDetail != null && slottedDetail.checkVisibility();
|
|
377
|
+
const hasDetailPlaceholder = !!slottedDetailPlaceholder;
|
|
378
|
+
|
|
379
|
+
const computedStyle = getComputedStyle(this);
|
|
380
|
+
const hostSizeProp = isVertical ? 'height' : 'width';
|
|
381
|
+
const hostSize = parseFloat(computedStyle[hostSizeProp]);
|
|
382
|
+
|
|
383
|
+
const trackSizesProp = isVertical ? 'gridTemplateRows' : 'gridTemplateColumns';
|
|
384
|
+
const trackSizes = parseTrackSizes(computedStyle[trackSizesProp]);
|
|
385
|
+
|
|
386
|
+
const hasOverflow =
|
|
387
|
+
(hasDetail || hasDetailPlaceholder) && (this.forceOverlay || detectOverflow(hostSize, trackSizes));
|
|
388
|
+
const focusTarget = !hadDetail && hasDetail && hasOverflow ? getFocusableElements(slottedDetail)[0] : null;
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
hasMaster,
|
|
392
|
+
hadDetail,
|
|
393
|
+
hasDetail,
|
|
394
|
+
hasDetailPlaceholder,
|
|
395
|
+
hasOverflow,
|
|
396
|
+
focusTarget,
|
|
397
|
+
hostSize,
|
|
398
|
+
trackSizes,
|
|
399
|
+
};
|
|
284
400
|
}
|
|
285
401
|
|
|
286
402
|
/**
|
|
287
403
|
* Applies layout state to DOM attributes. Pure writes, no reads.
|
|
288
404
|
* @private
|
|
289
405
|
*/
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
406
|
+
__writeLayoutState({ hasMaster, hadDetail, hasDetail, hasDetailPlaceholder, hasOverflow, focusTarget, trackSizes }) {
|
|
407
|
+
const [_masterSize, _masterExtra, detailSize] = trackSizes;
|
|
408
|
+
|
|
409
|
+
// If no detailSize is explicitily set, cache the intrinsic size (min-content) of
|
|
410
|
+
// the slotted detail content to use as a fallback for the detail column size
|
|
411
|
+
// while the detail content is rendered in an overlay.
|
|
412
|
+
if ((hasDetail || hasDetailPlaceholder) && this.__isDetailAutoSized && detailSize > 0) {
|
|
413
|
+
this.__detailCachedSize ||= `${Math.ceil(detailSize)}px`;
|
|
414
|
+
} else {
|
|
415
|
+
this.__detailCachedSize = null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Force the detail column offscreen when it first appears and overflow
|
|
419
|
+
// is already detected. This prevents unnecessary master column shrinking,
|
|
420
|
+
// as the detail content is rendered in an overlay anyway.
|
|
293
421
|
if (!hadDetail && hasDetail && hasOverflow) {
|
|
294
422
|
this.setAttribute('keep-detail-column-offscreen', '');
|
|
295
423
|
} else if (!hasDetail || !hasOverflow) {
|
|
296
424
|
this.removeAttribute('keep-detail-column-offscreen');
|
|
297
425
|
}
|
|
298
426
|
|
|
427
|
+
this.toggleAttribute('overlay', hasOverflow);
|
|
428
|
+
this.toggleAttribute('has-master', hasMaster);
|
|
299
429
|
this.toggleAttribute('has-detail', hasDetail);
|
|
300
|
-
this.toggleAttribute('
|
|
430
|
+
this.toggleAttribute('has-detail-placeholder', hasDetailPlaceholder);
|
|
301
431
|
|
|
302
432
|
// Re-render to update ARIA attributes (role, aria-modal, inert)
|
|
303
|
-
// which depend on has-detail and
|
|
433
|
+
// which depend on has-detail and overlay state.
|
|
304
434
|
this.requestUpdate();
|
|
305
435
|
|
|
306
436
|
if (focusTarget) {
|
|
307
|
-
focusTarget.focus();
|
|
437
|
+
focusTarget.focus({ preventScroll: true, focusVisible: isKeyboardActive() });
|
|
308
438
|
}
|
|
309
439
|
}
|
|
310
440
|
|
|
311
|
-
/**
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
441
|
+
/**
|
|
442
|
+
* When `detailSize` is not explicitly set, re-measures the cached intrinsic size of
|
|
443
|
+
* the detail content by placing it in a min-content CSS grid column, then repeats
|
|
444
|
+
* this process for ancestor master-detail layouts without an explicit `detailSize`,
|
|
445
|
+
* if any, so that their detail areas also adapt.
|
|
446
|
+
*
|
|
447
|
+
* Call this method after changing the detail content in a way that affects its intrinsic
|
|
448
|
+
* size — for example, when opening a detail in a nested master-detail layout that was
|
|
449
|
+
* not previously visible.
|
|
450
|
+
*
|
|
451
|
+
* NOTE: This method can be expensive in large layouts as it triggers consecutive
|
|
452
|
+
* synchronous DOM reads and writes.
|
|
453
|
+
*/
|
|
454
|
+
recalculateLayout() {
|
|
455
|
+
const invalidatedLayouts = [...this.__ancestorLayouts, this];
|
|
315
456
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
457
|
+
// Write
|
|
458
|
+
invalidatedLayouts.forEach((layout) => {
|
|
459
|
+
// Cancel any pending ResizeObserver rAF to prevent it from potentially
|
|
460
|
+
// overriding the layout state with stale measurements.
|
|
461
|
+
cancelAnimationFrame(layout.__resizeRaf);
|
|
320
462
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
463
|
+
layout.__detailCachedSize = null;
|
|
464
|
+
|
|
465
|
+
if (layout.__isDetailAutoSized) {
|
|
466
|
+
layout.removeAttribute('overlay');
|
|
467
|
+
layout.toggleAttribute('recalculating-detail-size', true);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Read/Write
|
|
472
|
+
invalidatedLayouts.forEach((layout) => {
|
|
473
|
+
const state = layout.__readLayoutState();
|
|
474
|
+
layout.__writeLayoutState(state);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Write
|
|
478
|
+
invalidatedLayouts.forEach((layout) => {
|
|
479
|
+
if (layout.__isDetailAutoSized) {
|
|
480
|
+
layout.toggleAttribute('recalculating-detail-size', false);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** @private */
|
|
486
|
+
get __isDetailAutoSized() {
|
|
487
|
+
return this.detailSize == null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** @private */
|
|
491
|
+
get __ancestorLayouts() {
|
|
492
|
+
const parent = getClosestElement(this.constructor.is, this.parentNode);
|
|
493
|
+
return parent ? [...parent.__ancestorLayouts, parent] : [];
|
|
328
494
|
}
|
|
329
495
|
|
|
330
496
|
/** @private */
|
|
@@ -342,128 +508,169 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
342
508
|
}
|
|
343
509
|
|
|
344
510
|
/**
|
|
345
|
-
* Sets the detail element to be displayed in the detail area and starts
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
511
|
+
* Sets the detail element to be displayed in the detail area and starts an
|
|
512
|
+
* animated transition for adding, replacing or removing the detail area.
|
|
513
|
+
* The element is added to the DOM and assigned to the `detail` slot. Any
|
|
514
|
+
* previous detail element is removed. When passing null as the element,
|
|
515
|
+
* the current detail element is removed.
|
|
350
516
|
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
* also be skipped using the `skipTransition` parameter.
|
|
517
|
+
* The transition can be skipped using the `skipTransition` parameter or
|
|
518
|
+
* the `noAnimation` property.
|
|
354
519
|
*
|
|
355
520
|
* @param element the new detail element, or null to remove the current detail
|
|
356
521
|
* @param skipTransition whether to skip the transition
|
|
357
|
-
* @
|
|
522
|
+
* @return {Promise<void>}
|
|
358
523
|
* @protected
|
|
359
524
|
*/
|
|
360
|
-
_setDetail(
|
|
525
|
+
async _setDetail(newDetail, skipTransition) {
|
|
361
526
|
// Don't start a transition if detail didn't change
|
|
362
|
-
const
|
|
363
|
-
if ((
|
|
364
|
-
return
|
|
527
|
+
const oldDetail = this.__slottedDetail;
|
|
528
|
+
if (oldDetail === (newDetail || null)) {
|
|
529
|
+
return;
|
|
365
530
|
}
|
|
366
531
|
|
|
367
|
-
const updateSlot = () => {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
532
|
+
const updateSlot = async () => {
|
|
533
|
+
if (oldDetail?.slot === 'detail') {
|
|
534
|
+
oldDetail.remove();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (newDetail) {
|
|
538
|
+
newDetail.setAttribute('slot', 'detail');
|
|
539
|
+
this.appendChild(newDetail);
|
|
374
540
|
}
|
|
541
|
+
|
|
542
|
+
// Wait for Lit elements to render
|
|
543
|
+
await Promise.resolve();
|
|
544
|
+
|
|
545
|
+
this.recalculateLayout();
|
|
375
546
|
};
|
|
376
547
|
|
|
377
|
-
if (skipTransition) {
|
|
378
|
-
updateSlot();
|
|
379
|
-
return
|
|
548
|
+
if (skipTransition || this.noAnimation) {
|
|
549
|
+
await updateSlot();
|
|
550
|
+
return;
|
|
380
551
|
}
|
|
381
552
|
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
updateSlot
|
|
387
|
-
|
|
388
|
-
this.
|
|
389
|
-
}
|
|
553
|
+
const hasPlaceholder = !!this.__slottedDetailPlaceholder;
|
|
554
|
+
if ((oldDetail && newDetail) || (hasPlaceholder && !this.hasAttribute('overlay'))) {
|
|
555
|
+
await this._startTransition('replace', updateSlot);
|
|
556
|
+
} else if (!oldDetail && newDetail) {
|
|
557
|
+
await this._startTransition('add', updateSlot);
|
|
558
|
+
} else if (oldDetail && !newDetail) {
|
|
559
|
+
await this._startTransition('remove', updateSlot);
|
|
560
|
+
}
|
|
390
561
|
}
|
|
391
562
|
|
|
392
563
|
/**
|
|
393
|
-
* Starts
|
|
394
|
-
* detail area
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
* animating the transition.
|
|
564
|
+
* Starts an animated transition for adding, replacing or removing the
|
|
565
|
+
* detail area using the Web Animations API.
|
|
566
|
+
*
|
|
567
|
+
* For 'add'/'replace': DOM is updated immediately, then animation
|
|
568
|
+
* starts after a microtask (so Lit elements render and layout is
|
|
569
|
+
* recalculated before animation params are read).
|
|
400
570
|
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
571
|
+
* For 'remove': animation plays first, then DOM is updated after
|
|
572
|
+
* the slide-out completes.
|
|
573
|
+
*
|
|
574
|
+
* Interruptible: a new transition cancels any in-progress animation
|
|
575
|
+
* and picks up from the interrupted position.
|
|
404
576
|
*
|
|
405
577
|
* @param transitionType
|
|
406
|
-
* @param
|
|
407
|
-
* @
|
|
578
|
+
* @param updateSlot
|
|
579
|
+
* @return {Promise<void>}
|
|
408
580
|
* @protected
|
|
409
581
|
*/
|
|
410
|
-
_startTransition(transitionType,
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return Promise.resolve();
|
|
582
|
+
async _startTransition(transitionType, updateSlot) {
|
|
583
|
+
if (this.noAnimation) {
|
|
584
|
+
await updateSlot();
|
|
585
|
+
return;
|
|
415
586
|
}
|
|
416
587
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
588
|
+
try {
|
|
589
|
+
this.setAttribute('transition', transitionType);
|
|
590
|
+
|
|
591
|
+
switch (transitionType) {
|
|
592
|
+
case 'add':
|
|
593
|
+
await this.__addTransition(updateSlot);
|
|
594
|
+
break;
|
|
595
|
+
case 'remove':
|
|
596
|
+
await this.__removeTransition(updateSlot);
|
|
597
|
+
break;
|
|
598
|
+
default:
|
|
599
|
+
await this.__replaceTransition(updateSlot);
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
this.removeAttribute('transition');
|
|
604
|
+
} catch (e) {
|
|
605
|
+
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
606
|
+
return; // Animation was cancelled
|
|
607
|
+
}
|
|
608
|
+
throw e;
|
|
609
|
+
}
|
|
428
610
|
}
|
|
429
611
|
|
|
430
|
-
/**
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
612
|
+
/** @private */
|
|
613
|
+
async __addTransition(updateSlot) {
|
|
614
|
+
await updateSlot();
|
|
615
|
+
|
|
616
|
+
const progress = getCurrentAnimationProgress(this.$.detail);
|
|
617
|
+
await Promise.all([
|
|
618
|
+
animateIn(this.$.detail, ['fade', 'slide'], progress),
|
|
619
|
+
animateIn(this.$.backdrop, ['fade'], this.hasAttribute('overlay') ? progress : 1),
|
|
620
|
+
]);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** @private */
|
|
624
|
+
async __replaceTransition(updateSlot) {
|
|
625
|
+
const oldDetail = this.__slottedDetail;
|
|
626
|
+
if (oldDetail) {
|
|
627
|
+
oldDetail.slot = 'detail-outgoing';
|
|
628
|
+
}
|
|
446
629
|
|
|
447
|
-
|
|
448
|
-
|
|
630
|
+
try {
|
|
631
|
+
this.$.detailOutgoing.style.width = this.__detailCachedSize;
|
|
632
|
+
|
|
633
|
+
await updateSlot();
|
|
634
|
+
|
|
635
|
+
const progress = getCurrentAnimationProgress(this.$.detail);
|
|
636
|
+
await Promise.all([
|
|
637
|
+
animateIn(this.$.detail, ['fade', 'slide'], progress),
|
|
638
|
+
animateOut(this.$.detailOutgoing, ['fade', 'slide'], progress),
|
|
639
|
+
]);
|
|
640
|
+
} finally {
|
|
641
|
+
// Skip removal if the slot was reassigned during the transition.
|
|
642
|
+
// The React component does this to let React handle the removal.
|
|
643
|
+
if (oldDetail?.slot === 'detail-outgoing') {
|
|
644
|
+
oldDetail.remove();
|
|
645
|
+
}
|
|
449
646
|
}
|
|
450
|
-
// Resolve the update callback to finish the transition
|
|
451
|
-
this.__resolveUpdateCallback();
|
|
452
|
-
await this.__transition.finished;
|
|
453
|
-
this.removeAttribute('transition');
|
|
454
|
-
this.__transition = null;
|
|
455
|
-
this.__resolveUpdateCallback = null;
|
|
456
647
|
}
|
|
457
648
|
|
|
458
|
-
/**
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
649
|
+
/** @private */
|
|
650
|
+
async __removeTransition(updateSlot) {
|
|
651
|
+
const progress = getCurrentAnimationProgress(this.$.detail);
|
|
652
|
+
await Promise.all([
|
|
653
|
+
animateOut(this.$.detail, ['fade', 'slide'], progress),
|
|
654
|
+
animateOut(this.$.backdrop, ['fade'], this.hasAttribute('overlay') ? progress : 1),
|
|
655
|
+
]);
|
|
656
|
+
|
|
657
|
+
await updateSlot();
|
|
658
|
+
}
|
|
462
659
|
|
|
463
|
-
/**
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
660
|
+
/** @private */
|
|
661
|
+
get __slottedMaster() {
|
|
662
|
+
return this.querySelector(':scope > :is([slot=""], :not([slot]))');
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** @private */
|
|
666
|
+
get __slottedDetail() {
|
|
667
|
+
return this.querySelector(':scope > [slot="detail"]');
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** @private */
|
|
671
|
+
get __slottedDetailPlaceholder() {
|
|
672
|
+
return this.querySelector(':scope > [slot="detail-placeholder"]');
|
|
673
|
+
}
|
|
467
674
|
}
|
|
468
675
|
|
|
469
676
|
defineCustomElement(MasterDetailLayout);
|