@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
|
@@ -7,125 +7,286 @@ import '@vaadin/component-base/src/styles/style-props.js';
|
|
|
7
7
|
import { css } from 'lit';
|
|
8
8
|
|
|
9
9
|
export const masterDetailLayoutStyles = css`
|
|
10
|
+
/* stylelint-disable no-duplicate-selectors */
|
|
10
11
|
:host {
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--_detail-column: var(--_detail-size) 0;
|
|
12
|
+
--_rtl-multiplier: 1;
|
|
13
|
+
--_transition-duration: 0s;
|
|
14
|
+
--_transition-easing: cubic-bezier(0.78, 0, 0.22, 1);
|
|
15
15
|
|
|
16
16
|
display: grid;
|
|
17
17
|
box-sizing: border-box;
|
|
18
18
|
height: 100%;
|
|
19
19
|
position: relative;
|
|
20
|
+
overflow: clip;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
:host:not([overlay-containment='page']) {
|
|
20
24
|
z-index: 0;
|
|
21
|
-
overflow: hidden;
|
|
22
|
-
grid-template-columns: [master-start] var(--_master-column) [detail-start] var(--_detail-column) [detail-end];
|
|
23
|
-
grid-template-rows: 100%;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
:host([
|
|
27
|
+
:host([dir='rtl']) {
|
|
28
|
+
--_rtl-multiplier: -1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
:host([orientation='horizontal']) {
|
|
32
|
+
--_transition-offset: calc(30px * var(--_rtl-multiplier));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
:host([orientation='vertical']) {
|
|
36
|
+
--_transition-offset: 0 30px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:host([hidden]),
|
|
40
|
+
::slotted([hidden]) {
|
|
27
41
|
display: none !important;
|
|
28
42
|
}
|
|
29
43
|
|
|
44
|
+
/* CSS grid template */
|
|
45
|
+
|
|
46
|
+
:host {
|
|
47
|
+
--_master-size: min(100%, 30rem);
|
|
48
|
+
--_master-extra: 0px;
|
|
49
|
+
--_detail-size: var(--_detail-cached-size);
|
|
50
|
+
--_detail-extra: 0px;
|
|
51
|
+
--_detail-cached-size: min-content;
|
|
52
|
+
|
|
53
|
+
/* prettier-ignore */
|
|
54
|
+
--_grid-template:
|
|
55
|
+
[master-start] var(--_master-size) [master-extra] var(--_master-extra)
|
|
56
|
+
[detail-start] var(--_detail-size) [detail-extra] var(--_detail-extra)
|
|
57
|
+
[detail-end];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
:host([force-overlay]) {
|
|
61
|
+
/* prettier-ignore */
|
|
62
|
+
--_grid-template:
|
|
63
|
+
[master-start] var(--_master-size) [master-extra] var(--_master-extra)
|
|
64
|
+
[detail-start] 0px [detail-extra] 0px
|
|
65
|
+
[detail-end];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
:host([orientation='horizontal']) {
|
|
69
|
+
grid-template-columns: var(--_grid-template);
|
|
70
|
+
grid-template-rows: 100%;
|
|
71
|
+
}
|
|
72
|
+
|
|
30
73
|
:host([orientation='vertical']) {
|
|
31
74
|
grid-template-columns: 100%;
|
|
32
|
-
grid-template-rows:
|
|
75
|
+
grid-template-rows: var(--_grid-template);
|
|
33
76
|
}
|
|
34
77
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
78
|
+
/* CSS grid placement */
|
|
79
|
+
|
|
80
|
+
:host {
|
|
81
|
+
--_master-area: master-start / detail-start;
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
When the detail size isn't explicitly defined and the detail is set to expand,
|
|
85
|
+
the detail column template is 'min-content 1fr'. In this case, the detail area
|
|
86
|
+
should not span both columns initially (and when recalculating the detail size)
|
|
87
|
+
as spanning both would effectively collapse them into a single '1fr' column where
|
|
88
|
+
min-content resolves to 0, making it impossible to measure the detail's intrinsic
|
|
89
|
+
minimum width from JavaScript.
|
|
90
|
+
*/
|
|
91
|
+
--_detail-area: detail-start / detail-extra;
|
|
38
92
|
}
|
|
39
93
|
|
|
40
|
-
[
|
|
41
|
-
|
|
94
|
+
:host(:is([has-detail], [has-detail-placeholder]):not([recalculating-detail-size])) {
|
|
95
|
+
--_detail-area: detail-start / detail-end;
|
|
42
96
|
}
|
|
43
97
|
|
|
44
|
-
[
|
|
45
|
-
grid-column:
|
|
98
|
+
:host([orientation='horizontal']) #master {
|
|
99
|
+
grid-column: var(--_master-area);
|
|
100
|
+
grid-row: 1;
|
|
46
101
|
}
|
|
47
102
|
|
|
48
|
-
:host([orientation='vertical'])
|
|
49
|
-
grid-column:
|
|
50
|
-
grid-row:
|
|
103
|
+
:host([orientation='vertical']) #master {
|
|
104
|
+
grid-column: 1;
|
|
105
|
+
grid-row: var(--_master-area);
|
|
51
106
|
}
|
|
52
107
|
|
|
53
|
-
:host([orientation='
|
|
54
|
-
grid-column:
|
|
55
|
-
grid-row:
|
|
108
|
+
:host([orientation='horizontal']) :is(#detail, #detailPlaceholder, #detailOutgoing) {
|
|
109
|
+
grid-column: var(--_detail-area);
|
|
110
|
+
grid-row: 1;
|
|
56
111
|
}
|
|
57
112
|
|
|
58
|
-
[
|
|
113
|
+
:host([orientation='vertical']) :is(#detail, #detailPlaceholder, #detailOutgoing) {
|
|
114
|
+
grid-column: 1;
|
|
115
|
+
grid-row: var(--_detail-area);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Expand */
|
|
119
|
+
|
|
120
|
+
:host([expand-master]) {
|
|
121
|
+
--_master-extra: 1fr;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
:host([expand-detail]) {
|
|
125
|
+
--_detail-extra: 1fr;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
:host([keep-detail-column-offscreen]),
|
|
129
|
+
:host([has-detail-placeholder][overlay]:not([has-detail])),
|
|
130
|
+
:host(:not([has-detail-placeholder], [has-detail])) {
|
|
131
|
+
--_master-extra: calc(100% - var(--_master-size));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Backdrop base styles */
|
|
135
|
+
|
|
136
|
+
#backdrop {
|
|
137
|
+
--_transition-easing: linear;
|
|
138
|
+
|
|
59
139
|
position: absolute;
|
|
60
140
|
inset: 0;
|
|
61
|
-
z-index:
|
|
62
|
-
|
|
141
|
+
z-index: 2;
|
|
142
|
+
opacity: 0;
|
|
143
|
+
pointer-events: none;
|
|
63
144
|
background: var(--vaadin-overlay-backdrop-background, rgba(0, 0, 0, 0.2));
|
|
64
145
|
forced-color-adjust: none;
|
|
65
146
|
}
|
|
66
147
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
148
|
+
/* Master base styles */
|
|
149
|
+
|
|
150
|
+
#master {
|
|
151
|
+
opacity: 0;
|
|
152
|
+
pointer-events: none;
|
|
153
|
+
box-sizing: border-box;
|
|
70
154
|
}
|
|
71
155
|
|
|
72
|
-
:host(
|
|
73
|
-
|
|
74
|
-
|
|
156
|
+
:host([has-master]) #master {
|
|
157
|
+
opacity: 1;
|
|
158
|
+
pointer-events: auto;
|
|
75
159
|
}
|
|
76
160
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
161
|
+
/* Detail base styles */
|
|
162
|
+
|
|
163
|
+
#detail {
|
|
164
|
+
translate: var(--_transition-offset);
|
|
165
|
+
opacity: 0;
|
|
166
|
+
z-index: 4;
|
|
80
167
|
}
|
|
81
168
|
|
|
82
|
-
:host([
|
|
83
|
-
|
|
84
|
-
|
|
169
|
+
:host([has-detail]) #detail {
|
|
170
|
+
translate: none;
|
|
171
|
+
opacity: 1;
|
|
85
172
|
}
|
|
86
173
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
174
|
+
#detailOutgoing {
|
|
175
|
+
position: absolute;
|
|
176
|
+
z-index: 3;
|
|
177
|
+
display: none;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
:host([transition='replace']) #detailOutgoing {
|
|
181
|
+
display: block;
|
|
90
182
|
}
|
|
91
183
|
|
|
92
|
-
|
|
184
|
+
#detailPlaceholder {
|
|
185
|
+
z-index: 1;
|
|
186
|
+
visibility: hidden;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
:host([has-detail-placeholder]:not([has-detail], [overlay])) #detailPlaceholder {
|
|
190
|
+
visibility: visible;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
:is(#detail, #detailPlaceholder, #detailOutgoing) {
|
|
194
|
+
box-sizing: border-box;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Detail borders */
|
|
198
|
+
|
|
199
|
+
#detail,
|
|
200
|
+
#detailPlaceholder {
|
|
201
|
+
border-color: var(--vaadin-master-detail-layout-border-color, var(--vaadin-border-color-secondary));
|
|
202
|
+
border-width: var(--vaadin-master-detail-layout-border-width, 1px);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
:host([orientation='horizontal']) #detailPlaceholder,
|
|
206
|
+
:host([orientation='horizontal']:not([overlay])) #detail {
|
|
207
|
+
border-inline-start-style: solid;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
:host([orientation='vertical']) #detailPlaceholder,
|
|
211
|
+
:host([orientation='vertical']:not([overlay])) #detail {
|
|
212
|
+
border-block-start-style: solid;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Overlay */
|
|
216
|
+
|
|
217
|
+
:host([overlay][orientation='horizontal']) {
|
|
218
|
+
--_transition-offset: calc((100% + 30px) * var(--_rtl-multiplier));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
:host([overlay][orientation='vertical']) {
|
|
222
|
+
--_transition-offset: 0 calc(100% + 30px);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
:host([has-detail][overlay]) #backdrop {
|
|
226
|
+
opacity: 1;
|
|
227
|
+
pointer-events: auto;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
:host([has-detail][overlay]) :is(#detail, #detailOutgoing) {
|
|
93
231
|
position: absolute;
|
|
94
|
-
z-index: 2;
|
|
95
232
|
background: var(--vaadin-master-detail-layout-detail-background, var(--vaadin-background-color));
|
|
96
233
|
box-shadow: var(--vaadin-master-detail-layout-detail-shadow, 0 0 20px 0 rgba(0, 0, 0, 0.3));
|
|
97
234
|
grid-column: none;
|
|
235
|
+
grid-row: none;
|
|
98
236
|
}
|
|
99
237
|
|
|
100
|
-
:host([
|
|
101
|
-
display: block;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
:host([overflow]:not([orientation='vertical'])) [part~='detail'] {
|
|
238
|
+
:host([has-detail][overlay][orientation='horizontal']) :is(#detail, #detailOutgoing) {
|
|
105
239
|
inset-block: 0;
|
|
106
|
-
width: var(--_overlay-size, var(--_detail-size, min-content));
|
|
107
240
|
inset-inline-end: 0;
|
|
241
|
+
width: var(--_overlay-size, var(--_detail-size));
|
|
242
|
+
max-width: 100%;
|
|
108
243
|
}
|
|
109
244
|
|
|
110
|
-
:host([
|
|
111
|
-
grid-column: auto;
|
|
112
|
-
grid-row: none;
|
|
245
|
+
:host([has-detail][overlay][orientation='vertical']) :is(#detail, #detailOutgoing) {
|
|
113
246
|
inset-inline: 0;
|
|
114
|
-
height: var(--_overlay-size, var(--_detail-size, min-content));
|
|
115
247
|
inset-block-end: 0;
|
|
248
|
+
height: var(--_overlay-size, var(--_detail-size));
|
|
249
|
+
max-height: 100%;
|
|
116
250
|
}
|
|
117
251
|
|
|
118
|
-
:host([
|
|
119
|
-
:host([overflow][overlay-containment='viewport']) [part~='backdrop'] {
|
|
252
|
+
:host([has-detail][overlay][overlay-containment='page']) :is(#detail, #detailOutgoing, #backdrop) {
|
|
120
253
|
position: fixed;
|
|
254
|
+
padding-top: env(safe-area-inset-top);
|
|
255
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
256
|
+
padding-right: env(safe-area-inset-right);
|
|
257
|
+
--safe-area-inset-top: 0px;
|
|
258
|
+
--safe-area-inset-bottom: 0px;
|
|
259
|
+
--safe-area-inset-left: 0px;
|
|
260
|
+
--safe-area-inset-right: 0px;
|
|
261
|
+
--safe-area-inset-inline-start: 0px;
|
|
262
|
+
--safe-area-inset-inline-end: 0px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
:host([dir='rtl'][has-detail][overlay][overlay-containment='page']) :is(#detail, #detailOutgoing, #backdrop) {
|
|
266
|
+
padding-right: 0;
|
|
267
|
+
padding-left: env(safe-area-inset-left);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* Transitions */
|
|
271
|
+
|
|
272
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
273
|
+
:host(:not([no-animation], [transition='replace'])) {
|
|
274
|
+
--_transition-duration: 200ms;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
:host([overlay]:not([no-animation])) {
|
|
278
|
+
--_transition-duration: 300ms;
|
|
279
|
+
}
|
|
121
280
|
}
|
|
122
281
|
|
|
282
|
+
/* Forced colors */
|
|
283
|
+
|
|
123
284
|
@media (forced-colors: active) {
|
|
124
|
-
:host([
|
|
285
|
+
:host([has-detail][overlay]) :is(#detail, #detailOutgoing) {
|
|
125
286
|
outline: 3px solid !important;
|
|
126
287
|
}
|
|
127
288
|
|
|
128
|
-
|
|
289
|
+
:is(#detail, #detailPlaceholder, #detailOutgoing) {
|
|
129
290
|
background: Canvas !important;
|
|
130
291
|
}
|
|
131
292
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2025 - 2026 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const ANIMATION_ID = 'vaadin-master-detail-layout';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reads CSS custom properties from the element that control
|
|
11
|
+
* animation keyframes and timing.
|
|
12
|
+
*
|
|
13
|
+
* @param {HTMLElement} element
|
|
14
|
+
* @return {{ offset: string, easing: string, duration: number }}
|
|
15
|
+
*/
|
|
16
|
+
function getAnimationParams(element) {
|
|
17
|
+
const computedStyle = getComputedStyle(element);
|
|
18
|
+
const offset = computedStyle.getPropertyValue('--_transition-offset');
|
|
19
|
+
const easing = computedStyle.getPropertyValue('--_transition-easing');
|
|
20
|
+
const durationStr = computedStyle.getPropertyValue('--_transition-duration');
|
|
21
|
+
const duration = durationStr.endsWith('ms') ? parseFloat(durationStr) : parseFloat(durationStr) * 1000;
|
|
22
|
+
return { offset, easing, duration };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns the currently running master-detail-layout animation on the
|
|
27
|
+
* element, if any. Matches by the shared animation ID and `'running'`
|
|
28
|
+
* play state.
|
|
29
|
+
*
|
|
30
|
+
* @param {HTMLElement} element
|
|
31
|
+
* @return {Animation | undefined}
|
|
32
|
+
*/
|
|
33
|
+
export function getCurrentAnimation(element) {
|
|
34
|
+
return element
|
|
35
|
+
.getAnimations()
|
|
36
|
+
.find((animation) => animation.id === ANIMATION_ID && animation.playState !== 'finished');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns the overall progress (0–1) of the current animation on the
|
|
41
|
+
* element, computed as `currentTime / duration`. Returns 0 when no
|
|
42
|
+
* animation is running.
|
|
43
|
+
*
|
|
44
|
+
* @param {HTMLElement} element
|
|
45
|
+
* @return {number}
|
|
46
|
+
*/
|
|
47
|
+
export function getCurrentAnimationProgress(element) {
|
|
48
|
+
const animation = getCurrentAnimation(element);
|
|
49
|
+
if (!animation) {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
const currentTime = animation.currentTime;
|
|
53
|
+
if (currentTime == null) {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
return currentTime / animation.effect.getTiming().duration;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Animates the element using the Web Animations API. Cancels any
|
|
61
|
+
* previous animation and resumes from the given progress for a
|
|
62
|
+
* smooth handoff. No-op when CSS params are missing or progress is 1.
|
|
63
|
+
*
|
|
64
|
+
* @param {HTMLElement} element
|
|
65
|
+
* @param {'in' | 'out'} direction
|
|
66
|
+
* @param {Array<'fade' | 'slide'>} effects
|
|
67
|
+
* @param {number} progress starting progress (0–1) for interrupted resumption
|
|
68
|
+
* @return {Promise<void>} resolves when the animation finishes
|
|
69
|
+
*/
|
|
70
|
+
function animate(element, direction, effects, progress) {
|
|
71
|
+
const { offset, easing, duration } = getAnimationParams(element);
|
|
72
|
+
if (!offset || !duration || progress === 1) {
|
|
73
|
+
return Promise.resolve();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const oldAnimation = getCurrentAnimation(element);
|
|
77
|
+
if (oldAnimation) {
|
|
78
|
+
oldAnimation.cancel();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const keyframes = {};
|
|
82
|
+
if (effects.includes('fade')) {
|
|
83
|
+
keyframes.opacity = [0, 1];
|
|
84
|
+
}
|
|
85
|
+
if (effects.includes('slide')) {
|
|
86
|
+
keyframes.translate = [offset, 0];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const newAnimation = element.animate(keyframes, { id: ANIMATION_ID, easing, duration });
|
|
90
|
+
newAnimation.pause();
|
|
91
|
+
newAnimation.currentTime = duration * progress;
|
|
92
|
+
newAnimation.playbackRate = direction === 'in' ? 1 : -1;
|
|
93
|
+
newAnimation.play();
|
|
94
|
+
return newAnimation.finished;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Runs an enter animation on the element.
|
|
99
|
+
*
|
|
100
|
+
* @param {HTMLElement} element
|
|
101
|
+
* @param {Array<'fade' | 'slide'>} effects
|
|
102
|
+
* @param {number} progress starting progress (0–1) for interrupted resumption
|
|
103
|
+
* @return {Promise<void>} resolves when the animation finishes
|
|
104
|
+
*/
|
|
105
|
+
export function animateIn(element, effects, progress) {
|
|
106
|
+
return animate(element, 'in', effects, progress);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Runs an exit animation on the element.
|
|
111
|
+
*
|
|
112
|
+
* @param {HTMLElement} element
|
|
113
|
+
* @param {Array<'fade' | 'slide'>} effects
|
|
114
|
+
* @param {number} progress starting progress (0–1) for interrupted resumption
|
|
115
|
+
* @return {Promise<void>} resolves when the animation finishes
|
|
116
|
+
*/
|
|
117
|
+
export function animateOut(element, effects, progress) {
|
|
118
|
+
return animate(element, 'out', effects, progress);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Cancels all running animations on the element that match the shared animation ID.
|
|
123
|
+
*
|
|
124
|
+
* @param {HTMLElement} element
|
|
125
|
+
*/
|
|
126
|
+
export function cancelAnimations(element) {
|
|
127
|
+
element.getAnimations({ subtree: true }).forEach((animation) => {
|
|
128
|
+
if (animation.id === ANIMATION_ID) {
|
|
129
|
+
animation.cancel();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Parses a computed `gridTemplateColumns` / `gridTemplateRows` value
|
|
136
|
+
* into an array of track sizes in pixels. Line names (e.g. `[name]`)
|
|
137
|
+
* are stripped before parsing.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} gridTemplate computed grid template string (e.g. `"200px [gap] 10px 400px"`)
|
|
140
|
+
* @return {number[]} track sizes in pixels
|
|
141
|
+
*/
|
|
142
|
+
export function parseTrackSizes(gridTemplate) {
|
|
143
|
+
return gridTemplate
|
|
144
|
+
.replace(/\[[^\]]+\]/gu, '')
|
|
145
|
+
.replace(/\s+/gu, ' ')
|
|
146
|
+
.trim()
|
|
147
|
+
.split(' ')
|
|
148
|
+
.map(parseFloat);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Determines whether the detail area overflows the host element,
|
|
153
|
+
* meaning it should be shown as an overlay instead of side-by-side.
|
|
154
|
+
*
|
|
155
|
+
* Returns `false` when all tracks fit within the host, or when the
|
|
156
|
+
* master's extra space (flexible portion) is large enough to absorb
|
|
157
|
+
* the detail column.
|
|
158
|
+
*
|
|
159
|
+
* @param {number} hostSize the host element's width or height in pixels
|
|
160
|
+
* @param {number[]} trackSizes [masterSize, masterExtra, detailSize] in pixels
|
|
161
|
+
* @return {boolean} `true` if the detail overflows and should be overlaid
|
|
162
|
+
*/
|
|
163
|
+
export function detectOverflow(hostSize, trackSizes) {
|
|
164
|
+
const [masterSize, masterExtra, detailSize] = trackSizes;
|
|
165
|
+
|
|
166
|
+
if (Math.floor(masterSize + masterExtra + detailSize) <= Math.floor(hostSize)) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (Math.floor(masterExtra) >= Math.floor(detailSize)) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
6
|
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
7
|
-
import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
|
|
8
7
|
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
9
8
|
|
|
10
9
|
export interface MasterDetailLayoutCustomEventMap {
|
|
@@ -20,25 +19,53 @@ export interface MasterDetailLayoutEventMap extends HTMLElementEventMap, MasterD
|
|
|
20
19
|
* (or primary) area and a detail (or secondary) area that is displayed next to, or
|
|
21
20
|
* overlaid on top of, the master area, depending on configuration and viewport size.
|
|
22
21
|
*
|
|
22
|
+
* ### Slots
|
|
23
|
+
*
|
|
24
|
+
* The component has two main content areas: the master area (default slot)
|
|
25
|
+
* and the detail area (`detail` slot). When the detail doesn't fit next to
|
|
26
|
+
* the master, it is shown as an overlay on top of the master area:
|
|
27
|
+
*
|
|
28
|
+
* ```html
|
|
29
|
+
* <vaadin-master-detail-layout>
|
|
30
|
+
* <div>Master content</div>
|
|
31
|
+
* <div slot="detail">Detail content</div>
|
|
32
|
+
* </vaadin-master-detail-layout>
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* The component also supports a `detail-placeholder` slot for content shown
|
|
36
|
+
* in the detail area when no detail is selected. Unlike the `detail` slot,
|
|
37
|
+
* the placeholder is simply hidden when it doesn't fit next to the master area,
|
|
38
|
+
* rather than shown as an overlay:
|
|
39
|
+
*
|
|
40
|
+
* ```html
|
|
41
|
+
* <vaadin-master-detail-layout>
|
|
42
|
+
* <div>Master content</div>
|
|
43
|
+
* <div slot="detail-placeholder">Select an item</div>
|
|
44
|
+
* </vaadin-master-detail-layout>
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
23
47
|
* ### Styling
|
|
24
48
|
*
|
|
25
49
|
* The following shadow DOM parts are available for styling:
|
|
26
50
|
*
|
|
27
|
-
* Part name
|
|
28
|
-
*
|
|
29
|
-
* `backdrop`
|
|
30
|
-
* `master`
|
|
31
|
-
* `detail`
|
|
51
|
+
* Part name | Description
|
|
52
|
+
* ----------------------|----------------------
|
|
53
|
+
* `backdrop` | Backdrop covering the master area in the overlay mode
|
|
54
|
+
* `master` | The master area
|
|
55
|
+
* `detail` | The detail area
|
|
56
|
+
* `detail-placeholder` | The detail placeholder area
|
|
32
57
|
*
|
|
33
58
|
* The following state attributes are available for styling:
|
|
34
59
|
*
|
|
35
|
-
* Attribute
|
|
36
|
-
*
|
|
37
|
-
* `expand`
|
|
38
|
-
* `
|
|
39
|
-
* `
|
|
40
|
-
* `
|
|
41
|
-
* `
|
|
60
|
+
* Attribute | Description
|
|
61
|
+
* --------------------------|----------------------
|
|
62
|
+
* `expand-master` | Set when the master area expands to fill available space.
|
|
63
|
+
* `expand-detail` | Set when the detail area expands to fill available space.
|
|
64
|
+
* `orientation` | Set to `horizontal` or `vertical` depending on the orientation.
|
|
65
|
+
* `has-detail` | Set when the detail content is provided and visible.
|
|
66
|
+
* `has-detail-placeholder` | Set when the detail placeholder content is provided.
|
|
67
|
+
* `overlay` | Set when columns don't fit and the detail is shown as an overlay.
|
|
68
|
+
* `overlay-containment` | Set to `layout` or `page`.
|
|
42
69
|
*
|
|
43
70
|
* The following custom CSS properties are available for styling:
|
|
44
71
|
*
|
|
@@ -55,12 +82,18 @@ export interface MasterDetailLayoutEventMap extends HTMLElementEventMap, MasterD
|
|
|
55
82
|
* @fires {CustomEvent} backdrop-click - Fired when the user clicks the backdrop in the overlay mode.
|
|
56
83
|
* @fires {CustomEvent} detail-escape-press - Fired when the user presses Escape in the detail area.
|
|
57
84
|
*/
|
|
58
|
-
declare class MasterDetailLayout extends
|
|
85
|
+
declare class MasterDetailLayout extends ThemableMixin(ElementMixin(HTMLElement)) {
|
|
59
86
|
/**
|
|
60
87
|
* Size (in CSS length units) to be set on the detail area in
|
|
61
|
-
* the CSS grid layout.
|
|
88
|
+
* the CSS grid layout. When there is not enough space to show
|
|
62
89
|
* master and detail areas next to each other, the detail area
|
|
63
|
-
* is shown as an overlay.
|
|
90
|
+
* is shown as an overlay.
|
|
91
|
+
* <p>
|
|
92
|
+
* If not specified, the size is determined automatically by measuring
|
|
93
|
+
* the detail content in a `min-content` CSS grid column when it first
|
|
94
|
+
* becomes visible, and then caching the resulting intrinsic size. To
|
|
95
|
+
* recalculate the cached intrinsic size, use the `recalculateLayout`
|
|
96
|
+
* method.
|
|
64
97
|
*
|
|
65
98
|
* @attr {string} detail-size
|
|
66
99
|
*/
|
|
@@ -96,19 +129,30 @@ declare class MasterDetailLayout extends SlotStylesMixin(ThemableMixin(ElementMi
|
|
|
96
129
|
/**
|
|
97
130
|
* Defines the containment of the detail area when the layout is in
|
|
98
131
|
* overlay mode. When set to `layout`, the overlay is confined to the
|
|
99
|
-
* layout. When set to `
|
|
132
|
+
* layout. When set to `page`, the overlay is confined to the
|
|
100
133
|
* browser's viewport. Defaults to `layout`.
|
|
101
134
|
*
|
|
102
135
|
* @attr {string} overlay-containment
|
|
103
136
|
*/
|
|
104
|
-
overlayContainment: 'layout' | '
|
|
137
|
+
overlayContainment: 'layout' | 'page';
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* When true, the master area grows to fill the available space.
|
|
141
|
+
* If `expandDetail` is also true, both areas share the available
|
|
142
|
+
* space equally.
|
|
143
|
+
*
|
|
144
|
+
* @attr {boolean} expand-master
|
|
145
|
+
*/
|
|
146
|
+
expandMaster: boolean;
|
|
105
147
|
|
|
106
148
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
149
|
+
* When true, the detail area grows to fill the available space.
|
|
150
|
+
* If `expandMaster` is also true, both areas share the available
|
|
151
|
+
* space equally.
|
|
152
|
+
*
|
|
153
|
+
* @attr {boolean} expand-detail
|
|
110
154
|
*/
|
|
111
|
-
|
|
155
|
+
expandDetail: boolean;
|
|
112
156
|
|
|
113
157
|
/**
|
|
114
158
|
* When true, the layout does not use animated transitions for the detail area.
|
|
@@ -117,6 +161,30 @@ declare class MasterDetailLayout extends SlotStylesMixin(ThemableMixin(ElementMi
|
|
|
117
161
|
*/
|
|
118
162
|
noAnimation: boolean;
|
|
119
163
|
|
|
164
|
+
/**
|
|
165
|
+
* When true, the layout forces the detail area to be shown as an overlay,
|
|
166
|
+
* even if there is enough space for master and detail to be shown next to
|
|
167
|
+
* each other using the default (split) mode.
|
|
168
|
+
*
|
|
169
|
+
* @attr {boolean} force-overlay
|
|
170
|
+
*/
|
|
171
|
+
forceOverlay: boolean;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* When `detailSize` is not explicitly set, re-measures the cached intrinsic size of
|
|
175
|
+
* the detail content by placing it in a min-content CSS grid column, then repeats
|
|
176
|
+
* this process for ancestor master-detail layouts without an explicit `detailSize`,
|
|
177
|
+
* if any, so that their detail areas also adapt.
|
|
178
|
+
*
|
|
179
|
+
* Call this method after changing the detail content in a way that affects its intrinsic
|
|
180
|
+
* size — for example, when opening a detail in a nested master-detail layout that was
|
|
181
|
+
* not previously visible.
|
|
182
|
+
*
|
|
183
|
+
* NOTE: This method can be expensive in large layouts as it triggers consecutive
|
|
184
|
+
* synchronous DOM reads and writes.
|
|
185
|
+
*/
|
|
186
|
+
recalculateLayout(): void;
|
|
187
|
+
|
|
120
188
|
addEventListener<K extends keyof MasterDetailLayoutEventMap>(
|
|
121
189
|
type: K,
|
|
122
190
|
listener: (this: MasterDetailLayout, ev: MasterDetailLayoutEventMap[K]) => void,
|