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