@vaadin/master-detail-layout 24.8.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/LICENSE +190 -0
- package/README.md +60 -0
- package/package.json +54 -0
- package/src/vaadin-master-detail-layout-transition-styles.js +289 -0
- package/src/vaadin-master-detail-layout.d.ts +133 -0
- package/src/vaadin-master-detail-layout.js +682 -0
- package/theme/lumo/vaadin-master-detail-layout-styles.d.ts +1 -0
- package/theme/lumo/vaadin-master-detail-layout-styles.js +28 -0
- package/theme/lumo/vaadin-master-detail-layout.d.ts +2 -0
- package/theme/lumo/vaadin-master-detail-layout.js +2 -0
- package/theme/material/vaadin-master-detail-layout-styles.d.ts +2 -0
- package/theme/material/vaadin-master-detail-layout-styles.js +30 -0
- package/theme/material/vaadin-master-detail-layout.d.ts +2 -0
- package/theme/material/vaadin-master-detail-layout.js +2 -0
- package/vaadin-master-detail-layout.d.ts +1 -0
- package/vaadin-master-detail-layout.js +3 -0
- package/web-types.json +232 -0
- package/web-types.lit.json +90 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2025 - 2025 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { css, html, LitElement, nothing } from 'lit';
|
|
7
|
+
import { getFocusableElements } from '@vaadin/a11y-base/src/focus-utils.js';
|
|
8
|
+
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
|
|
9
|
+
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
10
|
+
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
|
+
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
14
|
+
import { transitionStyles } from './vaadin-master-detail-layout-transition-styles.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `<vaadin-master-detail-layout>` is a web component for building UIs with a master
|
|
18
|
+
* (or primary) area and a detail (or secondary) area that is displayed next to, or
|
|
19
|
+
* overlaid on top of, the master area, depending on configuration and viewport size.
|
|
20
|
+
*
|
|
21
|
+
* ### Styling
|
|
22
|
+
*
|
|
23
|
+
* The following custom CSS properties are available for styling (needed to be set
|
|
24
|
+
* on the `<html>` element since they are used by the global view transitions):
|
|
25
|
+
*
|
|
26
|
+
* Custom CSS property | Description | Default
|
|
27
|
+
* -----------------------------------------------------|---------------------|--------
|
|
28
|
+
* `--vaadin-master-detail-layout-transition-duration` | Transition duration | 300ms
|
|
29
|
+
*
|
|
30
|
+
* The following shadow DOM parts are available for styling:
|
|
31
|
+
*
|
|
32
|
+
* Part name | Description
|
|
33
|
+
* ---------------|----------------------
|
|
34
|
+
* `backdrop` | Backdrop covering the master area in the overlay mode
|
|
35
|
+
* `master` | The master area
|
|
36
|
+
* `detail` | The detail area
|
|
37
|
+
*
|
|
38
|
+
* The following state attributes are available for styling:
|
|
39
|
+
*
|
|
40
|
+
* Attribute | Description
|
|
41
|
+
* ---------------| -----------
|
|
42
|
+
* `containment` | Set to `layout` or `viewport` depending on the containment.
|
|
43
|
+
* `orientation` | Set to `horizontal` or `vertical` depending on the orientation.
|
|
44
|
+
* `has-detail` | Set when the detail content is provided.
|
|
45
|
+
* `overlay` | Set when the layout is using the overlay mode.
|
|
46
|
+
* `stack` | Set when the layout is using the stack mode.
|
|
47
|
+
*
|
|
48
|
+
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
|
|
49
|
+
*
|
|
50
|
+
* @customElement
|
|
51
|
+
* @extends HTMLElement
|
|
52
|
+
* @mixes ThemableMixin
|
|
53
|
+
* @mixes ElementMixin
|
|
54
|
+
* @mixes ResizeMixin
|
|
55
|
+
* @mixes SlotStylesMixin
|
|
56
|
+
*/
|
|
57
|
+
class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement))))) {
|
|
58
|
+
static get is() {
|
|
59
|
+
return 'vaadin-master-detail-layout';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static get styles() {
|
|
63
|
+
return css`
|
|
64
|
+
:host {
|
|
65
|
+
display: flex;
|
|
66
|
+
box-sizing: border-box;
|
|
67
|
+
height: 100%;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
:host([hidden]) {
|
|
71
|
+
display: none !important;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
:host(:not([has-detail])) [part='detail'],
|
|
75
|
+
[part='backdrop'] {
|
|
76
|
+
display: none;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Overlay mode */
|
|
80
|
+
:host(:is([overlay], [stack])) {
|
|
81
|
+
position: relative;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
:host(:is([overlay], [stack])[containment='layout']) [part='detail'],
|
|
85
|
+
:host([overlay][containment='layout']) [part='backdrop'] {
|
|
86
|
+
position: absolute;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
:host(:is([overlay], [stack])[containment='viewport']) [part='detail'],
|
|
90
|
+
:host([overlay][containment='viewport']) [part='backdrop'] {
|
|
91
|
+
position: fixed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
:host([overlay][has-detail]) [part='backdrop'] {
|
|
95
|
+
display: block;
|
|
96
|
+
inset: 0;
|
|
97
|
+
z-index: 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
:host([overlay]) [part='detail'] {
|
|
101
|
+
z-index: 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
:host([overlay][orientation='horizontal']) [part='detail'] {
|
|
105
|
+
inset-inline-end: 0;
|
|
106
|
+
height: 100%;
|
|
107
|
+
width: var(--_detail-min-size, min-content);
|
|
108
|
+
max-width: 100%;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
:host([overlay][orientation='horizontal'][containment='viewport']) [part='detail'] {
|
|
112
|
+
inset-block-start: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
:host([overlay][orientation='horizontal']) [part='master'] {
|
|
116
|
+
max-width: 100%;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* No fixed size */
|
|
120
|
+
:host(:not([has-master-size])) [part='master'],
|
|
121
|
+
:host(:not([has-detail-size])) [part='detail'] {
|
|
122
|
+
flex-grow: 1;
|
|
123
|
+
flex-basis: 50%;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Fixed size */
|
|
127
|
+
:host([has-master-size]) [part='master'],
|
|
128
|
+
:host([has-detail-size]) [part='detail'] {
|
|
129
|
+
flex-shrink: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
:host([has-master-size][orientation='horizontal']) [part='master'] {
|
|
133
|
+
width: var(--_master-size);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
:host([has-detail-size][orientation='horizontal']:not([stack])) [part='detail'] {
|
|
137
|
+
width: var(--_detail-size);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
:host([has-master-size][has-detail-size]) [part='master'] {
|
|
141
|
+
flex-grow: 1;
|
|
142
|
+
flex-basis: var(--_master-size);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
:host([has-master-size][has-detail-size]) [part='detail'] {
|
|
146
|
+
flex-grow: 1;
|
|
147
|
+
flex-basis: var(--_detail-size);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Min size */
|
|
151
|
+
:host([has-master-min-size][orientation='horizontal']:not([overlay])) [part='master'] {
|
|
152
|
+
min-width: var(--_master-min-size);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
:host([has-detail-min-size][orientation='horizontal']:not([overlay]):not([stack])) [part='detail'] {
|
|
156
|
+
min-width: var(--_detail-min-size);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
:host([has-master-min-size]) [part='master'],
|
|
160
|
+
:host([has-detail-min-size]) [part='detail'] {
|
|
161
|
+
flex-shrink: 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Vertical */
|
|
165
|
+
:host([orientation='vertical']) {
|
|
166
|
+
flex-direction: column;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
:host([orientation='vertical'][overlay]) [part='master'] {
|
|
170
|
+
max-height: 100%;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
:host([orientation='vertical'][overlay]) [part='detail'] {
|
|
174
|
+
inset-block-end: 0;
|
|
175
|
+
width: 100%;
|
|
176
|
+
height: var(--_detail-min-size, min-content);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
:host([overlay][orientation='vertical'][containment='viewport']) [part='detail'] {
|
|
180
|
+
inset-inline-start: 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* Fixed size */
|
|
184
|
+
:host([has-master-size][orientation='vertical']) [part='master'] {
|
|
185
|
+
height: var(--_master-size);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
:host([has-detail-size][orientation='vertical']:not([stack])) [part='detail'] {
|
|
189
|
+
height: var(--_detail-size);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Min size */
|
|
193
|
+
:host([has-master-min-size][orientation='vertical']:not([overlay])) [part='master'],
|
|
194
|
+
:host([has-master-min-size][orientation='vertical'][overlay]) {
|
|
195
|
+
min-height: var(--_master-min-size);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
:host([has-detail-min-size][orientation='vertical']:not([overlay]):not([stack])) [part='detail'] {
|
|
199
|
+
min-height: var(--_detail-min-size);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Stack mode */
|
|
203
|
+
:host([stack]) [part='master'] {
|
|
204
|
+
max-height: 100%;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
:host([stack]) [part='detail'] {
|
|
208
|
+
inset: 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
[part='master']::before {
|
|
212
|
+
background-position-y: var(--_stack-threshold);
|
|
213
|
+
}
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
static get properties() {
|
|
218
|
+
return {
|
|
219
|
+
/**
|
|
220
|
+
* Fixed size (in CSS length units) to be set on the detail area.
|
|
221
|
+
* When specified, it prevents the detail area from growing or
|
|
222
|
+
* shrinking. If there is not enough space to show master and detail
|
|
223
|
+
* areas next to each other, the layout switches to the overlay mode.
|
|
224
|
+
*
|
|
225
|
+
* @attr {string} detail-size
|
|
226
|
+
*/
|
|
227
|
+
detailSize: {
|
|
228
|
+
type: String,
|
|
229
|
+
sync: true,
|
|
230
|
+
observer: '__detailSizeChanged',
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Minimum size (in CSS length units) to be set on the detail area.
|
|
235
|
+
* When specified, it prevents the detail area from shrinking below
|
|
236
|
+
* this size. If there is not enough space to show master and detail
|
|
237
|
+
* areas next to each other, the layout switches to the overlay mode.
|
|
238
|
+
*
|
|
239
|
+
* @attr {string} detail-min-size
|
|
240
|
+
*/
|
|
241
|
+
detailMinSize: {
|
|
242
|
+
type: String,
|
|
243
|
+
sync: true,
|
|
244
|
+
observer: '__detailMinSizeChanged',
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Fixed size (in CSS length units) to be set on the master area.
|
|
249
|
+
* When specified, it prevents the master area from growing or
|
|
250
|
+
* shrinking. If there is not enough space to show master and detail
|
|
251
|
+
* areas next to each other, the layout switches to the overlay mode.
|
|
252
|
+
*
|
|
253
|
+
* @attr {string} master-size
|
|
254
|
+
*/
|
|
255
|
+
masterSize: {
|
|
256
|
+
type: String,
|
|
257
|
+
sync: true,
|
|
258
|
+
observer: '__masterSizeChanged',
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Minimum size (in CSS length units) to be set on the master area.
|
|
263
|
+
* When specified, it prevents the master area from shrinking below
|
|
264
|
+
* this size. If there is not enough space to show master and detail
|
|
265
|
+
* areas next to each other, the layout switches to the overlay mode.
|
|
266
|
+
*
|
|
267
|
+
* @attr {string} master-min-size
|
|
268
|
+
*/
|
|
269
|
+
masterMinSize: {
|
|
270
|
+
type: String,
|
|
271
|
+
sync: true,
|
|
272
|
+
observer: '__masterMinSizeChanged',
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Define how master and detail areas are shown next to each other,
|
|
277
|
+
* and the way how size and min-size properties are applied to them.
|
|
278
|
+
* Possible values are: `horizontal` or `vertical`.
|
|
279
|
+
* Defaults to horizontal.
|
|
280
|
+
*/
|
|
281
|
+
orientation: {
|
|
282
|
+
type: String,
|
|
283
|
+
value: 'horizontal',
|
|
284
|
+
reflectToAttribute: true,
|
|
285
|
+
observer: '__orientationChanged',
|
|
286
|
+
sync: true,
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* When specified, forces the layout to use overlay mode, even if
|
|
291
|
+
* there is enough space for master and detail to be shown next to
|
|
292
|
+
* each other using the default (split) mode.
|
|
293
|
+
*
|
|
294
|
+
* @attr {boolean} force-overlay
|
|
295
|
+
*/
|
|
296
|
+
forceOverlay: {
|
|
297
|
+
type: Boolean,
|
|
298
|
+
value: false,
|
|
299
|
+
observer: '__forceOverlayChanged',
|
|
300
|
+
sync: true,
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Defines the containment of the detail area when the layout is in
|
|
305
|
+
* overlay mode. When set to `layout`, the overlay is confined to the
|
|
306
|
+
* layout. When set to `viewport`, the overlay is confined to the
|
|
307
|
+
* browser's viewport. Defaults to `layout`.
|
|
308
|
+
*/
|
|
309
|
+
containment: {
|
|
310
|
+
type: String,
|
|
311
|
+
value: 'layout',
|
|
312
|
+
reflectToAttribute: true,
|
|
313
|
+
sync: true,
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The threshold (in CSS length units) at which the layout switches to
|
|
318
|
+
* the "stack" mode, making detail area fully cover the master area.
|
|
319
|
+
*
|
|
320
|
+
* @attr {string} stack-threshold
|
|
321
|
+
*/
|
|
322
|
+
stackThreshold: {
|
|
323
|
+
type: String,
|
|
324
|
+
observer: '__stackThresholdChanged',
|
|
325
|
+
sync: true,
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* When true, the layout does not use animated transitions for the detail area.
|
|
330
|
+
*
|
|
331
|
+
* @attr {boolean} no-animation
|
|
332
|
+
*/
|
|
333
|
+
noAnimation: {
|
|
334
|
+
type: Boolean,
|
|
335
|
+
value: false,
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* When true, the component uses the overlay mode. This property is read-only.
|
|
340
|
+
* In order to enforce the overlay mode, use `forceOverlay` property.
|
|
341
|
+
* @protected
|
|
342
|
+
*/
|
|
343
|
+
_overlay: {
|
|
344
|
+
type: Boolean,
|
|
345
|
+
attribute: 'overlay',
|
|
346
|
+
reflectToAttribute: true,
|
|
347
|
+
sync: true,
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* When true, the component uses the stack mode. This property is read-only.
|
|
352
|
+
* In order to enforce the stack mode, use `stackThreshold` property.
|
|
353
|
+
* @protected
|
|
354
|
+
*/
|
|
355
|
+
_stack: {
|
|
356
|
+
type: Boolean,
|
|
357
|
+
attribute: 'stack',
|
|
358
|
+
reflectToAttribute: true,
|
|
359
|
+
sync: true,
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* When true, the component has the detail content provided.
|
|
364
|
+
* @protected
|
|
365
|
+
*/
|
|
366
|
+
_hasDetail: {
|
|
367
|
+
type: Boolean,
|
|
368
|
+
attribute: 'has-detail',
|
|
369
|
+
reflectToAttribute: true,
|
|
370
|
+
sync: true,
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
static get experimental() {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** @override */
|
|
380
|
+
get slotStyles() {
|
|
381
|
+
return [transitionStyles];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** @protected */
|
|
385
|
+
render() {
|
|
386
|
+
return html`
|
|
387
|
+
<div part="backdrop"></div>
|
|
388
|
+
<div id="master" part="master" ?inert="${this._hasDetail && this._overlay && this.containment === 'layout'}">
|
|
389
|
+
<slot></slot>
|
|
390
|
+
</div>
|
|
391
|
+
<div
|
|
392
|
+
id="detail"
|
|
393
|
+
part="detail"
|
|
394
|
+
role="${this._overlay || this._stack ? 'dialog' : nothing}"
|
|
395
|
+
aria-modal="${this._overlay && this.containment === 'viewport' ? 'true' : nothing}"
|
|
396
|
+
>
|
|
397
|
+
<slot name="detail" @slotchange="${this.__onDetailSlotChange}"></slot>
|
|
398
|
+
</div>
|
|
399
|
+
`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** @private */
|
|
403
|
+
__onDetailSlotChange(e) {
|
|
404
|
+
const children = e.target.assignedNodes();
|
|
405
|
+
|
|
406
|
+
this._hasDetail = children.length > 0;
|
|
407
|
+
this.__detectLayoutMode();
|
|
408
|
+
|
|
409
|
+
// Move focus to the detail area when it is added to the DOM,
|
|
410
|
+
// in case if the layout is using overlay or stack mode.
|
|
411
|
+
if ((this.hasAttribute('overlay') || this.hasAttribute('stack')) && children.length > 0) {
|
|
412
|
+
const focusables = getFocusableElements(children[0]);
|
|
413
|
+
if (focusables.length) {
|
|
414
|
+
focusables[0].focus();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* @protected
|
|
421
|
+
* @override
|
|
422
|
+
*/
|
|
423
|
+
_onResize() {
|
|
424
|
+
this.__detectLayoutMode();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** @private */
|
|
428
|
+
__detailSizeChanged(size, oldSize) {
|
|
429
|
+
this.__updateStyleProperty('detail-size', size, oldSize);
|
|
430
|
+
this.__detectLayoutMode();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** @private */
|
|
434
|
+
__detailMinSizeChanged(size, oldSize) {
|
|
435
|
+
this.__updateStyleProperty('detail-min-size', size, oldSize);
|
|
436
|
+
this.__detectLayoutMode();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** @private */
|
|
440
|
+
__masterSizeChanged(size, oldSize) {
|
|
441
|
+
this.__updateStyleProperty('master-size', size, oldSize);
|
|
442
|
+
this.__detectLayoutMode();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** @private */
|
|
446
|
+
__masterMinSizeChanged(size, oldSize) {
|
|
447
|
+
this.__updateStyleProperty('master-min-size', size, oldSize);
|
|
448
|
+
this.__detectLayoutMode();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** @private */
|
|
452
|
+
__orientationChanged(orientation, oldOrientation) {
|
|
453
|
+
if (orientation || oldOrientation) {
|
|
454
|
+
this.__detectLayoutMode();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** @private */
|
|
459
|
+
__forceOverlayChanged(forceOverlay, oldForceOverlay) {
|
|
460
|
+
if (forceOverlay || oldForceOverlay) {
|
|
461
|
+
this.__detectLayoutMode();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** @private */
|
|
466
|
+
__stackThresholdChanged(threshold, oldThreshold) {
|
|
467
|
+
if (threshold || oldThreshold) {
|
|
468
|
+
if (threshold) {
|
|
469
|
+
this.$.master.style.setProperty('--_stack-threshold', threshold);
|
|
470
|
+
} else {
|
|
471
|
+
this.$.master.style.removeProperty('--_stack-threshold');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
this.__detectLayoutMode();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** @private */
|
|
479
|
+
__updateStyleProperty(prop, size, oldSize) {
|
|
480
|
+
if (size) {
|
|
481
|
+
this.style.setProperty(`--_${prop}`, size);
|
|
482
|
+
} else if (oldSize) {
|
|
483
|
+
this.style.removeProperty(`--_${prop}`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this.toggleAttribute(`has-${prop}`, !!size);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** @private */
|
|
490
|
+
__detectLayoutMode() {
|
|
491
|
+
this._overlay = false;
|
|
492
|
+
this._stack = false;
|
|
493
|
+
|
|
494
|
+
if (this.forceOverlay) {
|
|
495
|
+
this._overlay = true;
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (this.stackThreshold != null) {
|
|
500
|
+
// Set stack to true to disable masterMinSize and detailMinSize
|
|
501
|
+
// that would affect size measurements below when in split mode
|
|
502
|
+
this._stack = true;
|
|
503
|
+
|
|
504
|
+
const threshold = this.__getStackThresholdInPixels();
|
|
505
|
+
const size = this.orientation === 'vertical' ? this.offsetHeight : this.offsetWidth;
|
|
506
|
+
if (size > threshold) {
|
|
507
|
+
this._stack = false;
|
|
508
|
+
} else {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (!this._hasDetail) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (this.orientation === 'vertical') {
|
|
518
|
+
this.__detectVerticalMode();
|
|
519
|
+
} else {
|
|
520
|
+
this.__detectHorizontalMode();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** @private */
|
|
525
|
+
__detectHorizontalMode() {
|
|
526
|
+
const detailWidth = this.$.detail.offsetWidth;
|
|
527
|
+
|
|
528
|
+
// Detect minimum width needed by master content. Use max-width to ensure
|
|
529
|
+
// the layout can switch back to split mode once there is enough space.
|
|
530
|
+
// If there is master size or min-size set, use that instead to force the
|
|
531
|
+
// overlay mode by setting `masterSize` / `masterMinSize` to 100%/
|
|
532
|
+
this.$.master.style.maxWidth = this.masterSize || this.masterMinSize || 'min-content';
|
|
533
|
+
const masterWidth = this.$.master.offsetWidth;
|
|
534
|
+
this.$.master.style.maxWidth = '';
|
|
535
|
+
|
|
536
|
+
// If the combined minimum size of both the master and the detail content
|
|
537
|
+
// exceeds the size of the layout, the layout changes to the overlay mode.
|
|
538
|
+
this._overlay = this.offsetWidth < masterWidth + detailWidth;
|
|
539
|
+
|
|
540
|
+
// Toggling the overlay resizes master content, which can cause document
|
|
541
|
+
// scroll bar to appear or disappear, and trigger another resize of the
|
|
542
|
+
// layout which can affect previous measurements and end up in horizontal
|
|
543
|
+
// scroll. Check if that is the case and if so, preserve the overlay mode.
|
|
544
|
+
if (this.offsetWidth < this.scrollWidth) {
|
|
545
|
+
this._overlay = true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** @private */
|
|
550
|
+
__detectVerticalMode() {
|
|
551
|
+
// Remove overlay attribute temporarily to detect if there is enough space
|
|
552
|
+
// for both areas so that layout could switch back to the split mode.
|
|
553
|
+
this._overlay = false;
|
|
554
|
+
|
|
555
|
+
const masterHeight = this.$.master.clientHeight;
|
|
556
|
+
|
|
557
|
+
// If the combined minimum size of both the master and the detail content
|
|
558
|
+
// exceeds the available height, the layout changes to the overlay mode.
|
|
559
|
+
if (this.offsetHeight < masterHeight + this.$.detail.clientHeight) {
|
|
560
|
+
this._overlay = true;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** @private */
|
|
565
|
+
__getStackThresholdInPixels() {
|
|
566
|
+
const { backgroundPositionY } = getComputedStyle(this.$.master, '::before');
|
|
567
|
+
return parseFloat(backgroundPositionY);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Sets the detail element to be displayed in the detail area and starts a
|
|
572
|
+
* view transition that animates adding, replacing or removing the detail
|
|
573
|
+
* area. During the view transition, the element is added to the DOM and
|
|
574
|
+
* assigned to the `detail` slot. Any previous detail element is removed.
|
|
575
|
+
* When passing null as the element, the current detail element is removed.
|
|
576
|
+
*
|
|
577
|
+
* If the browser does not support view transitions, the respective updates
|
|
578
|
+
* are applied immediately without starting a transition. The transition can
|
|
579
|
+
* also be skipped using the `skipTransition` parameter.
|
|
580
|
+
*
|
|
581
|
+
* @param element the new detail element, or null to remove the current detail
|
|
582
|
+
* @param skipTransition whether to skip the transition
|
|
583
|
+
* @returns {Promise<void>}
|
|
584
|
+
* @protected
|
|
585
|
+
*/
|
|
586
|
+
_setDetail(element, skipTransition) {
|
|
587
|
+
// Don't start a transition if detail didn't change
|
|
588
|
+
const currentDetail = this.querySelector('[slot="detail"]');
|
|
589
|
+
if ((element || null) === currentDetail) {
|
|
590
|
+
return Promise.resolve();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const updateSlot = () => {
|
|
594
|
+
// Remove old content
|
|
595
|
+
this.querySelectorAll('[slot="detail"]').forEach((oldElement) => oldElement.remove());
|
|
596
|
+
// Add new content
|
|
597
|
+
if (element) {
|
|
598
|
+
element.setAttribute('slot', 'detail');
|
|
599
|
+
this.appendChild(element);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
if (skipTransition) {
|
|
604
|
+
updateSlot();
|
|
605
|
+
return Promise.resolve();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const hasDetail = !!currentDetail;
|
|
609
|
+
const transitionType = hasDetail && element ? 'replace' : hasDetail ? 'remove' : 'add';
|
|
610
|
+
return this._startTransition(transitionType, () => {
|
|
611
|
+
// Update the DOM
|
|
612
|
+
updateSlot();
|
|
613
|
+
// Finish the transition
|
|
614
|
+
this._finishTransition();
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Starts a view transition that animates adding, replacing or removing the
|
|
620
|
+
* detail area. Once the transition is ready and the browser has taken a
|
|
621
|
+
* snapshot of the current layout, the provided update callback is called.
|
|
622
|
+
* The callback should update the DOM, which can happen asynchronously.
|
|
623
|
+
* Once the DOM is updated, the caller must call `_finishTransition`,
|
|
624
|
+
* which results in the browser taking a snapshot of the new layout and
|
|
625
|
+
* animating the transition.
|
|
626
|
+
*
|
|
627
|
+
* If the browser does not support view transitions, or the `noAnimation`
|
|
628
|
+
* property is set, the update callback is called immediately without
|
|
629
|
+
* starting a transition.
|
|
630
|
+
*
|
|
631
|
+
* @param transitionType
|
|
632
|
+
* @param updateCallback
|
|
633
|
+
* @returns {Promise<void>}
|
|
634
|
+
* @protected
|
|
635
|
+
*/
|
|
636
|
+
_startTransition(transitionType, updateCallback) {
|
|
637
|
+
const useTransition = typeof document.startViewTransition === 'function' && !this.noAnimation;
|
|
638
|
+
if (!useTransition) {
|
|
639
|
+
updateCallback();
|
|
640
|
+
return Promise.resolve();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
this.setAttribute('transition', transitionType);
|
|
644
|
+
this.__transition = document.startViewTransition(() => {
|
|
645
|
+
// Return a promise that can be resolved once the DOM is updated
|
|
646
|
+
return new Promise((resolve) => {
|
|
647
|
+
this.__resolveUpdateCallback = resolve;
|
|
648
|
+
// Notify the caller that the transition is ready, so that they can
|
|
649
|
+
// update the DOM
|
|
650
|
+
updateCallback();
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
return this.__transition.finished;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Finishes the current view transition, if any. This method should be called
|
|
658
|
+
* after the DOM has been updated to finish the transition and animate the
|
|
659
|
+
* change in the layout.
|
|
660
|
+
*
|
|
661
|
+
* @returns {Promise<void>}
|
|
662
|
+
* @protected
|
|
663
|
+
*/
|
|
664
|
+
async _finishTransition() {
|
|
665
|
+
// Detect new layout mode after DOM has been updated
|
|
666
|
+
this.__detectLayoutMode();
|
|
667
|
+
|
|
668
|
+
if (!this.__transition) {
|
|
669
|
+
return Promise.resolve();
|
|
670
|
+
}
|
|
671
|
+
// Resolve the update callback to finish the transition
|
|
672
|
+
this.__resolveUpdateCallback();
|
|
673
|
+
await this.__transition.finished;
|
|
674
|
+
this.removeAttribute('transition');
|
|
675
|
+
this.__transition = null;
|
|
676
|
+
this.__resolveUpdateCallback = null;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
defineCustomElement(MasterDetailLayout);
|
|
681
|
+
|
|
682
|
+
export { MasterDetailLayout };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@vaadin/vaadin-lumo-styles/color.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import '@vaadin/vaadin-lumo-styles/color.js';
|
|
2
|
+
import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
3
|
+
|
|
4
|
+
registerStyles(
|
|
5
|
+
'vaadin-master-detail-layout',
|
|
6
|
+
css`
|
|
7
|
+
:host(:is([overlay], [stack])) [part='detail'] {
|
|
8
|
+
background-color: var(--lumo-base-color);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
:host([overlay]) [part='detail'] {
|
|
12
|
+
box-shadow: var(--lumo-box-shadow-s);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
:host([overlay][orientation='horizontal']) [part='detail'] {
|
|
16
|
+
border-inline-start: 1px solid var(--lumo-contrast-10pct);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:host([overlay][orientation='vertical']) [part='detail'] {
|
|
20
|
+
border-block-start: 1px solid var(--lumo-contrast-10pct);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
:host([overlay]) [part='backdrop'] {
|
|
24
|
+
background-color: var(--lumo-shade-20pct);
|
|
25
|
+
}
|
|
26
|
+
`,
|
|
27
|
+
{ moduleId: 'lumo-master-detail-layout' },
|
|
28
|
+
);
|