@vaadin/master-detail-layout 25.1.2 → 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 +276 -64
- package/package.json +8 -8
- package/src/styles/vaadin-master-detail-layout-base-styles.js +204 -105
- package/src/vaadin-master-detail-layout-helpers.js +173 -0
- package/src/vaadin-master-detail-layout.d.ts +102 -66
- package/src/vaadin-master-detail-layout.js +446 -329
- package/web-types.json +56 -92
- package/web-types.lit.json +22 -22
- package/src/styles/vaadin-master-detail-layout-transition-base-styles.js +0 -147
|
@@ -4,40 +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 { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
|
|
12
|
-
import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
|
|
13
12
|
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
14
13
|
import { masterDetailLayoutStyles } from './styles/vaadin-master-detail-layout-base-styles.js';
|
|
15
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
animateIn,
|
|
16
|
+
animateOut,
|
|
17
|
+
cancelAnimations,
|
|
18
|
+
detectOverflow,
|
|
19
|
+
getCurrentAnimationProgress,
|
|
20
|
+
parseTrackSizes,
|
|
21
|
+
} from './vaadin-master-detail-layout-helpers.js';
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* `<vaadin-master-detail-layout>` is a web component for building UIs with a master
|
|
19
25
|
* (or primary) area and a detail (or secondary) area that is displayed next to, or
|
|
20
26
|
* overlaid on top of, the master area, depending on configuration and viewport size.
|
|
21
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
|
+
*
|
|
22
53
|
* ### Styling
|
|
23
54
|
*
|
|
24
55
|
* The following shadow DOM parts are available for styling:
|
|
25
56
|
*
|
|
26
|
-
* Part name
|
|
27
|
-
*
|
|
28
|
-
* `backdrop`
|
|
29
|
-
* `master`
|
|
30
|
-
* `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
|
|
31
63
|
*
|
|
32
64
|
* The following state attributes are available for styling:
|
|
33
65
|
*
|
|
34
|
-
* Attribute
|
|
35
|
-
*
|
|
36
|
-
* `
|
|
37
|
-
* `
|
|
38
|
-
* `
|
|
39
|
-
* `
|
|
40
|
-
* `
|
|
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`.
|
|
41
75
|
*
|
|
42
76
|
* The following custom CSS properties are available for styling:
|
|
43
77
|
*
|
|
@@ -51,17 +85,15 @@ import { masterDetailLayoutTransitionStyles } from './styles/vaadin-master-detai
|
|
|
51
85
|
*
|
|
52
86
|
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
|
|
53
87
|
*
|
|
54
|
-
* @fires {CustomEvent} backdrop-click - Fired when the user clicks the backdrop in the
|
|
88
|
+
* @fires {CustomEvent} backdrop-click - Fired when the user clicks the backdrop in the overlay mode.
|
|
55
89
|
* @fires {CustomEvent} detail-escape-press - Fired when the user presses Escape in the detail area.
|
|
56
90
|
*
|
|
57
91
|
* @customElement vaadin-master-detail-layout
|
|
58
92
|
* @extends HTMLElement
|
|
59
93
|
* @mixes ThemableMixin
|
|
60
94
|
* @mixes ElementMixin
|
|
61
|
-
* @mixes ResizeMixin
|
|
62
|
-
* @mixes SlotStylesMixin
|
|
63
95
|
*/
|
|
64
|
-
class MasterDetailLayout extends
|
|
96
|
+
class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))) {
|
|
65
97
|
static get is() {
|
|
66
98
|
return 'vaadin-master-detail-layout';
|
|
67
99
|
}
|
|
@@ -73,63 +105,48 @@ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(Themab
|
|
|
73
105
|
static get properties() {
|
|
74
106
|
return {
|
|
75
107
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
108
|
+
* Size (in CSS length units) to be set on the detail area in
|
|
109
|
+
* the CSS grid layout. When there is not enough space to show
|
|
110
|
+
* master and detail areas next to each other, the detail area
|
|
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.
|
|
81
118
|
*
|
|
82
119
|
* @attr {string} detail-size
|
|
83
120
|
*/
|
|
84
121
|
detailSize: {
|
|
85
122
|
type: String,
|
|
86
123
|
sync: true,
|
|
87
|
-
observer: '__detailSizeChanged',
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Minimum size (in CSS length units) to be set on the detail area.
|
|
92
|
-
* When specified, it prevents the detail area from shrinking below
|
|
93
|
-
* this size. If there is not enough space to show master and detail
|
|
94
|
-
* areas next to each other, the details are shown as an overlay:
|
|
95
|
-
* either as drawer or stack, depending on the `stackOverlay` property.
|
|
96
|
-
*
|
|
97
|
-
* @attr {string} detail-min-size
|
|
98
|
-
*/
|
|
99
|
-
detailMinSize: {
|
|
100
|
-
type: String,
|
|
101
|
-
sync: true,
|
|
102
|
-
observer: '__detailMinSizeChanged',
|
|
103
124
|
},
|
|
104
125
|
|
|
105
126
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* either as drawer or stack, depending on the `stackOverlay` property.
|
|
127
|
+
* Size (in CSS length units) to be set on the master area in
|
|
128
|
+
* the CSS grid layout. If there is not enough space to show
|
|
129
|
+
* master and detail areas next to each other, the detail area
|
|
130
|
+
* is shown as an overlay. Defaults to 30em.
|
|
111
131
|
*
|
|
112
132
|
* @attr {string} master-size
|
|
113
133
|
*/
|
|
114
134
|
masterSize: {
|
|
115
135
|
type: String,
|
|
116
136
|
sync: true,
|
|
117
|
-
observer: '__masterSizeChanged',
|
|
118
137
|
},
|
|
119
138
|
|
|
120
139
|
/**
|
|
121
|
-
*
|
|
122
|
-
* When
|
|
123
|
-
*
|
|
124
|
-
* areas next to each other, the details are shown as an overlay:
|
|
125
|
-
* either as drawer or stack, depending on the `stackOverlay` property.
|
|
140
|
+
* Size (in CSS length units) for the detail area when shown as an
|
|
141
|
+
* overlay. When not set, falls back to `detailSize`. Set to `100%`
|
|
142
|
+
* to make the detail cover the full layout.
|
|
126
143
|
*
|
|
127
|
-
* @attr {string}
|
|
144
|
+
* @attr {string} overlay-size
|
|
128
145
|
*/
|
|
129
|
-
|
|
146
|
+
overlaySize: {
|
|
130
147
|
type: String,
|
|
131
148
|
sync: true,
|
|
132
|
-
observer: '
|
|
149
|
+
observer: '__overlaySizeChanged',
|
|
133
150
|
},
|
|
134
151
|
|
|
135
152
|
/**
|
|
@@ -142,35 +159,18 @@ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(Themab
|
|
|
142
159
|
type: String,
|
|
143
160
|
value: 'horizontal',
|
|
144
161
|
reflectToAttribute: true,
|
|
145
|
-
observer: '__orientationChanged',
|
|
146
|
-
sync: true,
|
|
147
|
-
},
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* When specified, forces the details to be shown as an overlay
|
|
151
|
-
* (either as drawer or stack), even if there is enough space for
|
|
152
|
-
* master and detail to be shown next to each other using the default
|
|
153
|
-
* (split) mode.
|
|
154
|
-
*
|
|
155
|
-
* In order to enforce the stack mode, use this property together with
|
|
156
|
-
* `stackOverlay` property and set both to `true`.
|
|
157
|
-
*
|
|
158
|
-
* @attr {boolean} force-overlay
|
|
159
|
-
*/
|
|
160
|
-
forceOverlay: {
|
|
161
|
-
type: Boolean,
|
|
162
|
-
value: false,
|
|
163
|
-
observer: '__forceOverlayChanged',
|
|
164
162
|
sync: true,
|
|
165
163
|
},
|
|
166
164
|
|
|
167
165
|
/**
|
|
168
166
|
* Defines the containment of the detail area when the layout is in
|
|
169
167
|
* overlay mode. When set to `layout`, the overlay is confined to the
|
|
170
|
-
* layout. When set to `
|
|
168
|
+
* layout. When set to `page`, the overlay is confined to the
|
|
171
169
|
* browser's viewport. Defaults to `layout`.
|
|
170
|
+
*
|
|
171
|
+
* @attr {string} overlay-containment
|
|
172
172
|
*/
|
|
173
|
-
|
|
173
|
+
overlayContainment: {
|
|
174
174
|
type: String,
|
|
175
175
|
value: 'layout',
|
|
176
176
|
reflectToAttribute: true,
|
|
@@ -178,62 +178,62 @@ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(Themab
|
|
|
178
178
|
},
|
|
179
179
|
|
|
180
180
|
/**
|
|
181
|
-
* When true, the
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* In order to enforce the stack mode, use this property together with
|
|
186
|
-
* `forceOverlay` property and set both to `true`.
|
|
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.
|
|
187
184
|
*
|
|
188
|
-
* @attr {
|
|
185
|
+
* @attr {boolean} expand-master
|
|
189
186
|
*/
|
|
190
|
-
|
|
187
|
+
expandMaster: {
|
|
191
188
|
type: Boolean,
|
|
192
189
|
value: false,
|
|
193
|
-
|
|
190
|
+
reflectToAttribute: true,
|
|
194
191
|
sync: true,
|
|
195
192
|
},
|
|
196
193
|
|
|
197
194
|
/**
|
|
198
|
-
* When true, the
|
|
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.
|
|
199
198
|
*
|
|
200
|
-
* @attr {boolean}
|
|
199
|
+
* @attr {boolean} expand-detail
|
|
201
200
|
*/
|
|
202
|
-
|
|
201
|
+
expandDetail: {
|
|
203
202
|
type: Boolean,
|
|
204
203
|
value: false,
|
|
204
|
+
reflectToAttribute: true,
|
|
205
|
+
sync: true,
|
|
205
206
|
},
|
|
206
207
|
|
|
207
208
|
/**
|
|
208
|
-
* When true, the
|
|
209
|
-
*
|
|
209
|
+
* When true, the layout does not use animated transitions for the detail area.
|
|
210
|
+
*
|
|
211
|
+
* @attr {boolean} no-animation
|
|
210
212
|
*/
|
|
211
|
-
|
|
213
|
+
noAnimation: {
|
|
212
214
|
type: Boolean,
|
|
213
|
-
|
|
215
|
+
value: false,
|
|
214
216
|
reflectToAttribute: true,
|
|
215
|
-
sync: true,
|
|
216
217
|
},
|
|
217
218
|
|
|
218
219
|
/**
|
|
219
|
-
* When true, the
|
|
220
|
-
*
|
|
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
|
|
221
225
|
*/
|
|
222
|
-
|
|
226
|
+
forceOverlay: {
|
|
223
227
|
type: Boolean,
|
|
224
|
-
|
|
228
|
+
value: false,
|
|
225
229
|
reflectToAttribute: true,
|
|
226
230
|
sync: true,
|
|
227
231
|
},
|
|
228
232
|
|
|
229
|
-
/**
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
_hasDetail: {
|
|
234
|
-
type: Boolean,
|
|
235
|
-
attribute: 'has-detail',
|
|
236
|
-
reflectToAttribute: true,
|
|
233
|
+
/** @private */
|
|
234
|
+
__detailCachedSize: {
|
|
235
|
+
type: String,
|
|
236
|
+
observer: '__detailCachedSizeChanged',
|
|
237
237
|
sync: true,
|
|
238
238
|
},
|
|
239
239
|
};
|
|
@@ -243,317 +243,434 @@ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(Themab
|
|
|
243
243
|
return true;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
/** @override */
|
|
247
|
-
get slotStyles() {
|
|
248
|
-
return [masterDetailLayoutTransitionStyles];
|
|
249
|
-
}
|
|
250
|
-
|
|
251
246
|
/** @protected */
|
|
252
247
|
render() {
|
|
248
|
+
const isOverlay = this.hasAttribute('has-detail') && this.hasAttribute('overlay');
|
|
249
|
+
const isPage = isOverlay && this.overlayContainment === 'page';
|
|
250
|
+
const isLayoutContained = isOverlay && !isPage;
|
|
251
|
+
|
|
253
252
|
return html`
|
|
254
|
-
<div part="backdrop" @click="${this.__onBackdropClick}"></div>
|
|
253
|
+
<div id="backdrop" part="backdrop" @click="${this.__onBackdropClick}"></div>
|
|
254
|
+
<div id="master" part="master" ?inert="${isLayoutContained}">
|
|
255
|
+
<slot @slotchange="${this.__onSlotChange}"></slot>
|
|
256
|
+
</div>
|
|
257
|
+
<div id="detailOutgoing" inert>
|
|
258
|
+
<slot name="detail-outgoing"></slot>
|
|
259
|
+
</div>
|
|
255
260
|
<div
|
|
256
|
-
id="
|
|
257
|
-
part="
|
|
258
|
-
|
|
261
|
+
id="detail"
|
|
262
|
+
part="detail"
|
|
263
|
+
role="${isOverlay ? 'dialog' : nothing}"
|
|
264
|
+
aria-modal="${isPage ? 'true' : nothing}"
|
|
265
|
+
@keydown="${this.__onDetailKeydown}"
|
|
259
266
|
>
|
|
260
|
-
<slot></slot>
|
|
267
|
+
<slot name="detail" @slotchange="${this.__onSlotChange}"></slot>
|
|
261
268
|
</div>
|
|
262
|
-
<div part="
|
|
263
|
-
<
|
|
264
|
-
id="detail"
|
|
265
|
-
part="detail"
|
|
266
|
-
role="${this._drawer || this._stack ? 'dialog' : nothing}"
|
|
267
|
-
aria-modal="${this._drawer && this.containment === 'viewport' ? 'true' : nothing}"
|
|
268
|
-
@keydown="${this.__onDetailKeydown}"
|
|
269
|
-
>
|
|
270
|
-
<slot name="detail" @slotchange="${this.__onDetailSlotChange}"></slot>
|
|
271
|
-
</div>
|
|
269
|
+
<div id="detailPlaceholder" part="detail-placeholder">
|
|
270
|
+
<slot name="detail-placeholder" @slotchange="${this.__onSlotChange}"></slot>
|
|
272
271
|
</div>
|
|
273
272
|
`;
|
|
274
273
|
}
|
|
275
274
|
|
|
276
|
-
/** @
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
this.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
275
|
+
/** @protected */
|
|
276
|
+
connectedCallback() {
|
|
277
|
+
super.connectedCallback();
|
|
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
|
+
});
|
|
290
289
|
}
|
|
291
290
|
}
|
|
292
291
|
|
|
293
|
-
/** @
|
|
294
|
-
|
|
295
|
-
|
|
292
|
+
/** @protected */
|
|
293
|
+
disconnectedCallback() {
|
|
294
|
+
super.disconnectedCallback();
|
|
295
|
+
this.__resizeObserver.disconnect();
|
|
296
|
+
cancelAnimationFrame(this.__resizeRaf);
|
|
297
|
+
cancelAnimationFrame(this.__initialRaf);
|
|
298
|
+
cancelAnimations(this);
|
|
296
299
|
}
|
|
297
300
|
|
|
298
|
-
/** @
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
this.
|
|
301
|
+
/** @protected */
|
|
302
|
+
updated(props) {
|
|
303
|
+
super.updated(props);
|
|
304
|
+
|
|
305
|
+
if (props.has('masterSize')) {
|
|
306
|
+
this.style.setProperty('--_master-size', this.masterSize);
|
|
304
307
|
}
|
|
305
|
-
}
|
|
306
308
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
+
}
|
|
313
321
|
}
|
|
314
322
|
|
|
315
323
|
/** @private */
|
|
316
|
-
|
|
317
|
-
this.
|
|
318
|
-
this.__detectLayoutMode();
|
|
324
|
+
__overlaySizeChanged(size) {
|
|
325
|
+
this.style.setProperty('--_overlay-size', size);
|
|
319
326
|
}
|
|
320
327
|
|
|
321
328
|
/** @private */
|
|
322
|
-
|
|
323
|
-
this.
|
|
324
|
-
this.__detectLayoutMode();
|
|
329
|
+
__detailCachedSizeChanged(size) {
|
|
330
|
+
this.style.setProperty('--_detail-cached-size', size);
|
|
325
331
|
}
|
|
326
332
|
|
|
327
333
|
/** @private */
|
|
328
|
-
|
|
329
|
-
this.
|
|
330
|
-
this.__detectLayoutMode();
|
|
334
|
+
__onSlotChange() {
|
|
335
|
+
this.__initResizeObserver();
|
|
331
336
|
}
|
|
332
337
|
|
|
333
338
|
/** @private */
|
|
334
|
-
|
|
335
|
-
this.
|
|
336
|
-
this.
|
|
339
|
+
__initResizeObserver() {
|
|
340
|
+
this.__resizeObserver ||= new ResizeObserver(() => this.__onResize());
|
|
341
|
+
this.__resizeObserver.disconnect();
|
|
342
|
+
|
|
343
|
+
[this, this.$.master, this.$.detail, this.__slottedMaster, this.__slottedDetail].forEach((node) => {
|
|
344
|
+
if (node) {
|
|
345
|
+
this.__resizeObserver.observe(node);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
337
348
|
}
|
|
338
349
|
|
|
339
|
-
/**
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Called by the ResizeObserver. Reads layout state synchronously (no forced
|
|
352
|
+
* reflow since layout is already computed), then defers writes to rAF.
|
|
353
|
+
* Cancels any pending rAF so the write phase always uses the latest state.
|
|
354
|
+
* @private
|
|
355
|
+
*/
|
|
356
|
+
__onResize() {
|
|
357
|
+
const state = this.__readLayoutState();
|
|
358
|
+
cancelAnimationFrame(this.__resizeRaf);
|
|
359
|
+
this.__resizeRaf = requestAnimationFrame(() => this.__writeLayoutState(state));
|
|
344
360
|
}
|
|
345
361
|
|
|
346
|
-
/**
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Reads DOM/style state needed for layout detection. Safe to call in
|
|
364
|
+
* ResizeObserver callback where layout is already computed (no forced reflow).
|
|
365
|
+
* @private
|
|
366
|
+
*/
|
|
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;
|
|
375
|
+
const hadDetail = this.hasAttribute('has-detail');
|
|
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
|
+
};
|
|
351
400
|
}
|
|
352
401
|
|
|
353
|
-
/**
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
402
|
+
/**
|
|
403
|
+
* Applies layout state to DOM attributes. Pure writes, no reads.
|
|
404
|
+
* @private
|
|
405
|
+
*/
|
|
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;
|
|
357
416
|
}
|
|
358
|
-
}
|
|
359
417
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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.
|
|
421
|
+
if (!hadDetail && hasDetail && hasOverflow) {
|
|
422
|
+
this.setAttribute('keep-detail-column-offscreen', '');
|
|
423
|
+
} else if (!hasDetail || !hasOverflow) {
|
|
424
|
+
this.removeAttribute('keep-detail-column-offscreen');
|
|
366
425
|
}
|
|
367
426
|
|
|
368
|
-
this.toggleAttribute(
|
|
369
|
-
|
|
427
|
+
this.toggleAttribute('overlay', hasOverflow);
|
|
428
|
+
this.toggleAttribute('has-master', hasMaster);
|
|
429
|
+
this.toggleAttribute('has-detail', hasDetail);
|
|
430
|
+
this.toggleAttribute('has-detail-placeholder', hasDetailPlaceholder);
|
|
370
431
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
432
|
+
// Re-render to update ARIA attributes (role, aria-modal, inert)
|
|
433
|
+
// which depend on has-detail and overlay state.
|
|
434
|
+
this.requestUpdate();
|
|
435
|
+
|
|
436
|
+
if (focusTarget) {
|
|
437
|
+
focusTarget.focus({ preventScroll: true, focusVisible: isKeyboardActive() });
|
|
377
438
|
}
|
|
378
439
|
}
|
|
379
440
|
|
|
380
|
-
/**
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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];
|
|
384
456
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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);
|
|
389
462
|
|
|
390
|
-
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
463
|
+
layout.__detailCachedSize = null;
|
|
393
464
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
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
|
+
});
|
|
399
483
|
}
|
|
400
484
|
|
|
401
485
|
/** @private */
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
// Detect minimum width needed by master content. Use max-width to ensure
|
|
406
|
-
// the layout can switch back to split mode once there is enough space.
|
|
407
|
-
// If there is master size or min-size set, use that instead to force the
|
|
408
|
-
// overlay mode by setting `masterSize` / `masterMinSize` to 100%/
|
|
409
|
-
this.$.master.style.maxWidth = this.masterSize || this.masterMinSize || 'min-content';
|
|
410
|
-
const masterWidth = this.$.master.offsetWidth;
|
|
411
|
-
this.$.master.style.maxWidth = '';
|
|
412
|
-
|
|
413
|
-
// If the combined minimum size of both the master and the detail content
|
|
414
|
-
// exceeds the size of the layout, the layout changes to the overlay mode.
|
|
415
|
-
this.__setOverlayMode(this.offsetWidth < masterWidth + detailWidth);
|
|
416
|
-
|
|
417
|
-
// Toggling the overlay resizes master content, which can cause document
|
|
418
|
-
// scroll bar to appear or disappear, and trigger another resize of the
|
|
419
|
-
// layout which can affect previous measurements and end up in horizontal
|
|
420
|
-
// scroll. Check if that is the case and if so, preserve the overlay mode.
|
|
421
|
-
if (this.offsetWidth < this.scrollWidth) {
|
|
422
|
-
this.__setOverlayMode(true);
|
|
423
|
-
}
|
|
486
|
+
get __isDetailAutoSized() {
|
|
487
|
+
return this.detailSize == null;
|
|
424
488
|
}
|
|
425
489
|
|
|
426
490
|
/** @private */
|
|
427
|
-
|
|
428
|
-
const
|
|
491
|
+
get __ancestorLayouts() {
|
|
492
|
+
const parent = getClosestElement(this.constructor.is, this.parentNode);
|
|
493
|
+
return parent ? [...parent.__ancestorLayouts, parent] : [];
|
|
494
|
+
}
|
|
429
495
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
496
|
+
/** @private */
|
|
497
|
+
__onBackdropClick() {
|
|
498
|
+
this.dispatchEvent(new CustomEvent('backdrop-click'));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** @private */
|
|
502
|
+
__onDetailKeydown(event) {
|
|
503
|
+
if (event.key === 'Escape' && !event.defaultPrevented) {
|
|
504
|
+
// Prevent firing on parent layout when using nested layouts
|
|
505
|
+
event.preventDefault();
|
|
506
|
+
this.dispatchEvent(new CustomEvent('detail-escape-press'));
|
|
434
507
|
}
|
|
435
508
|
}
|
|
436
509
|
|
|
437
510
|
/**
|
|
438
|
-
* Sets the detail element to be displayed in the detail area and starts
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
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.
|
|
443
516
|
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* also be skipped using the `skipTransition` parameter.
|
|
517
|
+
* The transition can be skipped using the `skipTransition` parameter or
|
|
518
|
+
* the `noAnimation` property.
|
|
447
519
|
*
|
|
448
520
|
* @param element the new detail element, or null to remove the current detail
|
|
449
521
|
* @param skipTransition whether to skip the transition
|
|
450
|
-
* @
|
|
522
|
+
* @return {Promise<void>}
|
|
451
523
|
* @protected
|
|
452
524
|
*/
|
|
453
|
-
_setDetail(
|
|
525
|
+
async _setDetail(newDetail, skipTransition) {
|
|
454
526
|
// Don't start a transition if detail didn't change
|
|
455
|
-
const
|
|
456
|
-
if ((
|
|
457
|
-
return
|
|
527
|
+
const oldDetail = this.__slottedDetail;
|
|
528
|
+
if (oldDetail === (newDetail || null)) {
|
|
529
|
+
return;
|
|
458
530
|
}
|
|
459
531
|
|
|
460
|
-
const updateSlot = () => {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
// Add new content
|
|
464
|
-
if (element) {
|
|
465
|
-
element.setAttribute('slot', 'detail');
|
|
466
|
-
this.appendChild(element);
|
|
532
|
+
const updateSlot = async () => {
|
|
533
|
+
if (oldDetail?.slot === 'detail') {
|
|
534
|
+
oldDetail.remove();
|
|
467
535
|
}
|
|
536
|
+
|
|
537
|
+
if (newDetail) {
|
|
538
|
+
newDetail.setAttribute('slot', 'detail');
|
|
539
|
+
this.appendChild(newDetail);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Wait for Lit elements to render
|
|
543
|
+
await Promise.resolve();
|
|
544
|
+
|
|
545
|
+
this.recalculateLayout();
|
|
468
546
|
};
|
|
469
547
|
|
|
470
|
-
if (skipTransition) {
|
|
471
|
-
updateSlot();
|
|
472
|
-
return
|
|
548
|
+
if (skipTransition || this.noAnimation) {
|
|
549
|
+
await updateSlot();
|
|
550
|
+
return;
|
|
473
551
|
}
|
|
474
552
|
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
updateSlot
|
|
480
|
-
|
|
481
|
-
this.
|
|
482
|
-
}
|
|
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
|
+
}
|
|
483
561
|
}
|
|
484
562
|
|
|
485
563
|
/**
|
|
486
|
-
* Starts
|
|
487
|
-
* detail area
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
* 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).
|
|
493
570
|
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
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.
|
|
497
576
|
*
|
|
498
577
|
* @param transitionType
|
|
499
|
-
* @param
|
|
500
|
-
* @
|
|
578
|
+
* @param updateSlot
|
|
579
|
+
* @return {Promise<void>}
|
|
501
580
|
* @protected
|
|
502
581
|
*/
|
|
503
|
-
_startTransition(transitionType,
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
return Promise.resolve();
|
|
582
|
+
async _startTransition(transitionType, updateSlot) {
|
|
583
|
+
if (this.noAnimation) {
|
|
584
|
+
await updateSlot();
|
|
585
|
+
return;
|
|
508
586
|
}
|
|
509
587
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
+
}
|
|
521
610
|
}
|
|
522
611
|
|
|
523
|
-
/**
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
if (
|
|
538
|
-
|
|
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
|
+
}
|
|
629
|
+
|
|
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
|
+
}
|
|
539
646
|
}
|
|
540
|
-
// Resolve the update callback to finish the transition
|
|
541
|
-
this.__resolveUpdateCallback();
|
|
542
|
-
await this.__transition.finished;
|
|
543
|
-
this.removeAttribute('transition');
|
|
544
|
-
this.__transition = null;
|
|
545
|
-
this.__resolveUpdateCallback = null;
|
|
546
647
|
}
|
|
547
648
|
|
|
548
|
-
/**
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
+
}
|
|
552
659
|
|
|
553
|
-
/**
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
+
}
|
|
557
674
|
}
|
|
558
675
|
|
|
559
676
|
defineCustomElement(MasterDetailLayout);
|