@vaadin/master-detail-layout 24.8.0-alpha3 → 24.8.0-alpha5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/master-detail-layout",
3
- "version": "24.8.0-alpha3",
3
+ "version": "24.8.0-alpha5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -34,14 +34,15 @@
34
34
  "web-component"
35
35
  ],
36
36
  "dependencies": {
37
- "@vaadin/component-base": "24.8.0-alpha3",
38
- "@vaadin/vaadin-lumo-styles": "24.8.0-alpha3",
39
- "@vaadin/vaadin-material-styles": "24.8.0-alpha3",
40
- "@vaadin/vaadin-themable-mixin": "24.8.0-alpha3",
37
+ "@vaadin/a11y-base": "24.8.0-alpha5",
38
+ "@vaadin/component-base": "24.8.0-alpha5",
39
+ "@vaadin/vaadin-lumo-styles": "24.8.0-alpha5",
40
+ "@vaadin/vaadin-material-styles": "24.8.0-alpha5",
41
+ "@vaadin/vaadin-themable-mixin": "24.8.0-alpha5",
41
42
  "lit": "^3.0.0"
42
43
  },
43
44
  "devDependencies": {
44
- "@vaadin/chai-plugins": "24.8.0-alpha3",
45
+ "@vaadin/chai-plugins": "24.8.0-alpha5",
45
46
  "@vaadin/testing-helpers": "^1.1.0",
46
47
  "sinon": "^18.0.0"
47
48
  },
@@ -49,5 +50,5 @@
49
50
  "web-types.json",
50
51
  "web-types.lit.json"
51
52
  ],
52
- "gitHead": "8c49e2337a1905ae68d0d7aee2e672500ea72343"
53
+ "gitHead": "3bb64b9ad9b00ac3adb94eb1bedd81b0f4ae574e"
53
54
  }
@@ -0,0 +1,216 @@
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 } from 'lit';
7
+
8
+ export const transitionStyles = css`
9
+ /* Overlay - horizontal - add */
10
+
11
+ vaadin-master-detail-layout[overlay][orientation='horizontal'][transition='add']::part(detail) {
12
+ view-transition-name: vaadin-master-detail-layout-overlay-horizontal-detail-add;
13
+ }
14
+
15
+ ::view-transition-group(vaadin-master-detail-layout-overlay-horizontal-detail-add) {
16
+ clip-path: inset(0);
17
+ }
18
+
19
+ ::view-transition-new(vaadin-master-detail-layout-overlay-horizontal-detail-add) {
20
+ animation: 300ms ease both vaadin-master-detail-layout-overlay-horizontal-detail-add;
21
+ }
22
+
23
+ @keyframes vaadin-master-detail-layout-overlay-horizontal-detail-add {
24
+ from {
25
+ transform: translateX(100%);
26
+ }
27
+ }
28
+
29
+ /* Overlay - horizontal - remove */
30
+
31
+ vaadin-master-detail-layout[overlay][orientation='horizontal'][transition='remove']::part(detail) {
32
+ view-transition-name: vaadin-master-detail-layout-overlay-horizontal-detail-remove;
33
+ }
34
+
35
+ ::view-transition-group(vaadin-master-detail-layout-overlay-horizontal-detail-remove) {
36
+ clip-path: inset(0);
37
+ }
38
+
39
+ ::view-transition-old(vaadin-master-detail-layout-overlay-horizontal-detail-remove) {
40
+ animation: 300ms ease both vaadin-master-detail-layout-overlay-horizontal-detail-remove;
41
+ }
42
+
43
+ @keyframes vaadin-master-detail-layout-overlay-horizontal-detail-remove {
44
+ to {
45
+ transform: translateX(100%);
46
+ }
47
+ }
48
+
49
+ /* Stack - horizontal - add */
50
+
51
+ vaadin-master-detail-layout[stack][orientation='horizontal'][transition='add'] {
52
+ view-transition-name: vaadin-master-detail-layout-stack-horizontal-add;
53
+ }
54
+
55
+ ::view-transition-group(vaadin-master-detail-layout-stack-horizontal-add) {
56
+ clip-path: inset(0);
57
+ }
58
+
59
+ ::view-transition-new(vaadin-master-detail-layout-stack-horizontal-add) {
60
+ animation: 300ms ease both vaadin-master-detail-layout-stack-horizontal-add-new;
61
+ }
62
+
63
+ ::view-transition-old(vaadin-master-detail-layout-stack-horizontal-add) {
64
+ animation: 300ms ease both vaadin-master-detail-layout-stack-horizontal-add-old;
65
+ }
66
+
67
+ @keyframes vaadin-master-detail-layout-stack-horizontal-add-new {
68
+ from {
69
+ transform: translateX(100px);
70
+ opacity: 0;
71
+ }
72
+ }
73
+
74
+ @keyframes vaadin-master-detail-layout-stack-horizontal-add-old {
75
+ to {
76
+ transform: translateX(-100px);
77
+ opacity: 0;
78
+ }
79
+ }
80
+
81
+ /* Stack - horizontal - remove */
82
+
83
+ vaadin-master-detail-layout[stack][orientation='horizontal'][transition='remove'] {
84
+ view-transition-name: vaadin-master-detail-layout-stack-horizontal-remove;
85
+ }
86
+
87
+ ::view-transition-group(vaadin-master-detail-layout-stack-horizontal-remove) {
88
+ clip-path: inset(0);
89
+ }
90
+
91
+ ::view-transition-new(vaadin-master-detail-layout-stack-horizontal-remove) {
92
+ animation: 300ms ease both vaadin-master-detail-layout-stack-horizontal-remove-new;
93
+ }
94
+
95
+ ::view-transition-old(vaadin-master-detail-layout-stack-horizontal-remove) {
96
+ animation: 300ms ease both vaadin-master-detail-layout-stack-horizontal-remove-old;
97
+ }
98
+
99
+ @keyframes vaadin-master-detail-layout-stack-horizontal-remove-new {
100
+ from {
101
+ transform: translateX(-100px);
102
+ opacity: 0;
103
+ }
104
+ }
105
+
106
+ @keyframes vaadin-master-detail-layout-stack-horizontal-remove-old {
107
+ to {
108
+ transform: translateX(100px);
109
+ opacity: 0;
110
+ }
111
+ }
112
+
113
+ /* Overlay - vertical - add */
114
+
115
+ vaadin-master-detail-layout[overlay][orientation='vertical'][transition='add']::part(detail) {
116
+ view-transition-name: vaadin-master-detail-layout-overlay-vertical-detail-add;
117
+ }
118
+
119
+ ::view-transition-group(vaadin-master-detail-layout-overlay-vertical-detail-add) {
120
+ clip-path: inset(0);
121
+ }
122
+
123
+ ::view-transition-new(vaadin-master-detail-layout-overlay-vertical-detail-add) {
124
+ animation: 300ms ease both vaadin-master-detail-layout-overlay-vertical-detail-add;
125
+ }
126
+
127
+ @keyframes vaadin-master-detail-layout-overlay-vertical-detail-add {
128
+ from {
129
+ transform: translateY(100%);
130
+ }
131
+ }
132
+
133
+ /* Overlay - vertical - remove */
134
+
135
+ vaadin-master-detail-layout[overlay][orientation='vertical'][transition='remove']::part(detail) {
136
+ view-transition-name: vaadin-master-detail-layout-overlay-vertical-detail-remove;
137
+ }
138
+
139
+ ::view-transition-group(vaadin-master-detail-layout-overlay-vertical-detail-remove) {
140
+ clip-path: inset(0);
141
+ }
142
+
143
+ ::view-transition-old(vaadin-master-detail-layout-overlay-vertical-detail-remove) {
144
+ animation: 300ms ease both vaadin-master-detail-layout-overlay-vertical-detail-remove;
145
+ }
146
+
147
+ @keyframes vaadin-master-detail-layout-overlay-vertical-detail-remove {
148
+ to {
149
+ transform: translateY(100%);
150
+ }
151
+ }
152
+
153
+ /* Stack - vertical - add */
154
+
155
+ vaadin-master-detail-layout[stack][orientation='vertical'][transition='add'] {
156
+ view-transition-name: vaadin-master-detail-layout-stack-vertical-add;
157
+ }
158
+
159
+ ::view-transition-group(vaadin-master-detail-layout-stack-vertical-add) {
160
+ clip-path: inset(0);
161
+ }
162
+
163
+ ::view-transition-new(vaadin-master-detail-layout-stack-vertical-add) {
164
+ animation: 300ms ease both vaadin-master-detail-layout-stack-vertical-add-new;
165
+ }
166
+
167
+ ::view-transition-old(vaadin-master-detail-layout-stack-vertical-add) {
168
+ animation: 300ms ease both vaadin-master-detail-layout-stack-vertical-add-old;
169
+ }
170
+
171
+ @keyframes vaadin-master-detail-layout-stack-vertical-add-new {
172
+ from {
173
+ transform: translateY(100px);
174
+ opacity: 0;
175
+ }
176
+ }
177
+
178
+ @keyframes vaadin-master-detail-layout-stack-vertical-add-old {
179
+ to {
180
+ transform: translateY(-100px);
181
+ opacity: 0;
182
+ }
183
+ }
184
+
185
+ /* Stack - vertical - remove */
186
+
187
+ vaadin-master-detail-layout[stack][orientation='vertical'][transition='remove'] {
188
+ view-transition-name: vaadin-master-detail-layout-stack-vertical-remove;
189
+ }
190
+
191
+ ::view-transition-group(vaadin-master-detail-layout-stack-vertical-remove) {
192
+ clip-path: inset(0);
193
+ }
194
+
195
+ ::view-transition-new(vaadin-master-detail-layout-stack-vertical-remove) {
196
+ animation: 300ms ease both vaadin-master-detail-layout-stack-vertical-remove-new;
197
+ }
198
+
199
+ ::view-transition-old(vaadin-master-detail-layout-stack-vertical-remove) {
200
+ animation: 300ms ease both vaadin-master-detail-layout-stack-vertical-remove-old;
201
+ }
202
+
203
+ @keyframes vaadin-master-detail-layout-stack-vertical-remove-new {
204
+ from {
205
+ transform: translateY(-100px);
206
+ opacity: 0;
207
+ }
208
+ }
209
+
210
+ @keyframes vaadin-master-detail-layout-stack-vertical-remove-old {
211
+ to {
212
+ transform: translateY(100px);
213
+ opacity: 0;
214
+ }
215
+ }
216
+ `;
@@ -5,14 +5,36 @@
5
5
  */
6
6
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
7
7
  import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
8
+ import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
8
9
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
9
10
 
10
11
  /**
11
12
  * `<vaadin-master-detail-layout>` is a web component for building UIs with a master
12
13
  * (or primary) area and a detail (or secondary) area that is displayed next to, or
13
14
  * overlaid on top of, the master area, depending on configuration and viewport size.
15
+ *
16
+ * ### Styling
17
+ *
18
+ * The following shadow DOM parts are available for styling:
19
+ *
20
+ * Part name | Description
21
+ * ---------------|----------------------
22
+ * `master` | The master area
23
+ * `detail` | The detail area
24
+ *
25
+ * The following state attributes are available for styling:
26
+ *
27
+ * Attribute | Description
28
+ * ---------------| -----------
29
+ * `containment` | Set to `layout` or `viewport` depending on the containment.
30
+ * `orientation` | Set to `horizontal` or `vertical` depending on the orientation.
31
+ * `has-detail` | Set when the detail content is provided.
32
+ * `overlay` | Set when the layout is using the overlay mode.
33
+ * `stack` | Set when the layout is using the stack mode.
34
+ *
35
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
14
36
  */
15
- declare class MasterDetailLayout extends ResizeMixin(ThemableMixin(ElementMixin(HTMLElement))) {
37
+ declare class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ThemableMixin(ElementMixin(HTMLElement)))) {
16
38
  /**
17
39
  * Fixed size (in CSS length units) to be set on the detail area.
18
40
  * When specified, it prevents the detail area from growing or
@@ -38,7 +60,6 @@ declare class MasterDetailLayout extends ResizeMixin(ThemableMixin(ElementMixin(
38
60
  * When specified, it prevents the master area from growing or
39
61
  * shrinking. If there is not enough space to show master and detail
40
62
  * areas next to each other, the layout switches to the overlay mode.
41
- * Setting `100%` enforces the overlay mode to be used by default.
42
63
  *
43
64
  * @attr {string} master-size
44
65
  */
@@ -49,11 +70,62 @@ declare class MasterDetailLayout extends ResizeMixin(ThemableMixin(ElementMixin(
49
70
  * When specified, it prevents the master area from shrinking below
50
71
  * this size. If there is not enough space to show master and detail
51
72
  * areas next to each other, the layout switches to the overlay mode.
52
- * Setting `100%` enforces the overlay mode to be used by default.
53
73
  *
54
74
  * @attr {string} master-min-size
55
75
  */
56
76
  masterMinSize: string | null | undefined;
77
+
78
+ /**
79
+ * Define how master and detail areas are shown next to each other,
80
+ * and the way how size and min-size properties are applied to them.
81
+ * Possible values are: `horizontal` or `vertical`.
82
+ * Defaults to horizontal.
83
+ */
84
+ orientation: 'horizontal' | 'vertical';
85
+
86
+ /**
87
+ * When specified, forces the layout to use overlay mode, even if
88
+ * there is enough space for master and detail to be shown next to
89
+ * each other using the default (split) mode.
90
+ *
91
+ * @attr {boolean} force-overlay
92
+ */
93
+ forceOverlay: boolean;
94
+
95
+ /**
96
+ * Defines the containment of the detail area when the layout is in
97
+ * overlay mode. When set to `layout`, the overlay is confined to the
98
+ * layout. When set to `viewport`, the overlay is confined to the
99
+ * browser's viewport. Defaults to `layout`.
100
+ */
101
+ containment: 'layout' | 'viewport';
102
+
103
+ /**
104
+ * The threshold (in CSS length units) at which the layout switches to
105
+ * the "stack" mode, making detail area fully cover the master area.
106
+ *
107
+ * @attr {string} stack-threshold
108
+ */
109
+ stackThreshold: string | null | undefined;
110
+
111
+ /**
112
+ * When true, the layout does not use animated transitions for the detail area.
113
+ *
114
+ * @attr {boolean} no-animation
115
+ */
116
+ noAnimation: boolean;
117
+
118
+ /**
119
+ * Sets the detail element to be displayed in the detail area and starts a
120
+ * view transition that animates adding, replacing or removing the detail
121
+ * area. During the view transition, the element is added to the DOM and
122
+ * assigned to the `detail` slot. Any previous detail element is removed.
123
+ * When passing null as the element, the current detail element is removed.
124
+ *
125
+ * If the browser does not support view transitions, the respective updates
126
+ * are applied immediately without starting a transition.
127
+ */
128
+ setDetail(detail: HTMLElement | null): Promise<void>;
57
129
  }
58
130
 
59
131
  declare global {
@@ -3,25 +3,50 @@
3
3
  * Copyright (c) 2025 - 2025 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import { css, html, LitElement } from 'lit';
6
+ import { css, html, LitElement, nothing } from 'lit';
7
+ import { getFocusableElements } from '@vaadin/a11y-base/src/focus-utils.js';
7
8
  import { defineCustomElement } from '@vaadin/component-base/src/define.js';
8
9
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
9
10
  import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
10
11
  import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
12
+ import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
11
13
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
14
+ import { transitionStyles } from './vaadin-master-detail-layout-transition-styles.js';
12
15
 
13
16
  /**
14
17
  * `<vaadin-master-detail-layout>` is a web component for building UIs with a master
15
18
  * (or primary) area and a detail (or secondary) area that is displayed next to, or
16
19
  * overlaid on top of, the master area, depending on configuration and viewport size.
17
20
  *
21
+ * ### Styling
22
+ *
23
+ * The following shadow DOM parts are available for styling:
24
+ *
25
+ * Part name | Description
26
+ * ---------------|----------------------
27
+ * `master` | The master area
28
+ * `detail` | The detail area
29
+ *
30
+ * The following state attributes are available for styling:
31
+ *
32
+ * Attribute | Description
33
+ * ---------------| -----------
34
+ * `containment` | Set to `layout` or `viewport` depending on the containment.
35
+ * `orientation` | Set to `horizontal` or `vertical` depending on the orientation.
36
+ * `has-detail` | Set when the detail content is provided.
37
+ * `overlay` | Set when the layout is using the overlay mode.
38
+ * `stack` | Set when the layout is using the stack mode.
39
+ *
40
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
41
+ *
18
42
  * @customElement
19
43
  * @extends HTMLElement
20
44
  * @mixes ThemableMixin
21
45
  * @mixes ElementMixin
22
46
  * @mixes ResizeMixin
47
+ * @mixes SlotStylesMixin
23
48
  */
24
- class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement)))) {
49
+ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement))))) {
25
50
  static get is() {
26
51
  return 'vaadin-master-detail-layout';
27
52
  }
@@ -43,19 +68,30 @@ class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitM
43
68
  }
44
69
 
45
70
  /* Overlay mode */
46
- :host([overlay][has-detail]) {
71
+ :host(:is([overlay], [stack])) {
47
72
  position: relative;
48
73
  }
49
74
 
50
- :host([overlay]) [part='detail'] {
75
+ :host(:is([overlay], [stack])[containment='layout']) [part='detail'] {
51
76
  position: absolute;
77
+ }
78
+
79
+ :host(:is([overlay], [stack])[containment='viewport']) [part='detail'] {
80
+ position: fixed;
81
+ }
82
+
83
+ :host([overlay][orientation='horizontal']) [part='detail'] {
52
84
  inset-inline-end: 0;
53
85
  height: 100%;
54
86
  width: var(--_detail-min-size, min-content);
55
87
  max-width: 100%;
56
88
  }
57
89
 
58
- :host([overlay]) [part='master'] {
90
+ :host([overlay][orientation='horizontal'][containment='viewport']) [part='detail'] {
91
+ inset-block-start: 0;
92
+ }
93
+
94
+ :host([overlay][orientation='horizontal']) [part='master'] {
59
95
  max-width: 100%;
60
96
  }
61
97
 
@@ -67,14 +103,17 @@ class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitM
67
103
  }
68
104
 
69
105
  /* Fixed size */
70
- :host([has-master-size]) [part='master'] {
71
- width: var(--_master-size);
106
+ :host([has-master-size]) [part='master'],
107
+ :host([has-detail-size]) [part='detail'] {
72
108
  flex-shrink: 0;
73
109
  }
74
110
 
75
- :host([has-detail-size]) [part='detail'] {
111
+ :host([has-master-size][orientation='horizontal']) [part='master'] {
112
+ width: var(--_master-size);
113
+ }
114
+
115
+ :host([has-detail-size][orientation='horizontal']:not([stack])) [part='detail'] {
76
116
  width: var(--_detail-size);
77
- flex-shrink: 0;
78
117
  }
79
118
 
80
119
  :host([has-master-size][has-detail-size]) [part='master'] {
@@ -88,11 +127,11 @@ class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitM
88
127
  }
89
128
 
90
129
  /* Min size */
91
- :host([has-master-min-size]:not([overlay])) [part='master'] {
130
+ :host([has-master-min-size][orientation='horizontal']:not([overlay])) [part='master'] {
92
131
  min-width: var(--_master-min-size);
93
132
  }
94
133
 
95
- :host([has-detail-min-size]:not([overlay])) [part='detail'] {
134
+ :host([has-detail-min-size][orientation='horizontal']:not([overlay]):not([stack])) [part='detail'] {
96
135
  min-width: var(--_detail-min-size);
97
136
  }
98
137
 
@@ -100,6 +139,57 @@ class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitM
100
139
  :host([has-detail-min-size]) [part='detail'] {
101
140
  flex-shrink: 0;
102
141
  }
142
+
143
+ /* Vertical */
144
+ :host([orientation='vertical']) {
145
+ flex-direction: column;
146
+ }
147
+
148
+ :host([orientation='vertical'][overlay]) [part='master'] {
149
+ max-height: 100%;
150
+ }
151
+
152
+ :host([orientation='vertical'][overlay]) [part='detail'] {
153
+ inset-block-end: 0;
154
+ width: 100%;
155
+ height: var(--_detail-min-size, min-content);
156
+ }
157
+
158
+ :host([overlay][orientation='vertical'][containment='viewport']) [part='detail'] {
159
+ inset-inline-start: 0;
160
+ }
161
+
162
+ /* Fixed size */
163
+ :host([has-master-size][orientation='vertical']) [part='master'] {
164
+ height: var(--_master-size);
165
+ }
166
+
167
+ :host([has-detail-size][orientation='vertical']:not([stack])) [part='detail'] {
168
+ height: var(--_detail-size);
169
+ }
170
+
171
+ /* Min size */
172
+ :host([has-master-min-size][orientation='vertical']:not([overlay])) [part='master'],
173
+ :host([has-master-min-size][orientation='vertical'][overlay]) {
174
+ min-height: var(--_master-min-size);
175
+ }
176
+
177
+ :host([has-detail-min-size][orientation='vertical']:not([overlay]):not([stack])) [part='detail'] {
178
+ min-height: var(--_detail-min-size);
179
+ }
180
+
181
+ /* Stack mode */
182
+ :host([stack]) [part='master'] {
183
+ max-height: 100%;
184
+ }
185
+
186
+ :host([stack]) [part='detail'] {
187
+ inset: 0;
188
+ }
189
+
190
+ [part='master']::before {
191
+ background-position-y: var(--_stack-threshold);
192
+ }
103
193
  `;
104
194
  }
105
195
 
@@ -138,7 +228,6 @@ class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitM
138
228
  * When specified, it prevents the master area from growing or
139
229
  * shrinking. If there is not enough space to show master and detail
140
230
  * areas next to each other, the layout switches to the overlay mode.
141
- * Setting `100%` enforces the overlay mode to be used by default.
142
231
  *
143
232
  * @attr {string} master-size
144
233
  */
@@ -153,7 +242,6 @@ class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitM
153
242
  * When specified, it prevents the master area from shrinking below
154
243
  * this size. If there is not enough space to show master and detail
155
244
  * areas next to each other, the layout switches to the overlay mode.
156
- * Setting `100%` enforces the overlay mode to be used by default.
157
245
  *
158
246
  * @attr {string} master-min-size
159
247
  */
@@ -162,16 +250,113 @@ class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitM
162
250
  sync: true,
163
251
  observer: '__masterMinSizeChanged',
164
252
  },
253
+
254
+ /**
255
+ * Define how master and detail areas are shown next to each other,
256
+ * and the way how size and min-size properties are applied to them.
257
+ * Possible values are: `horizontal` or `vertical`.
258
+ * Defaults to horizontal.
259
+ */
260
+ orientation: {
261
+ type: String,
262
+ value: 'horizontal',
263
+ reflectToAttribute: true,
264
+ observer: '__orientationChanged',
265
+ sync: true,
266
+ },
267
+
268
+ /**
269
+ * When specified, forces the layout to use overlay mode, even if
270
+ * there is enough space for master and detail to be shown next to
271
+ * each other using the default (split) mode.
272
+ *
273
+ * @attr {boolean} force-overlay
274
+ */
275
+ forceOverlay: {
276
+ type: Boolean,
277
+ value: false,
278
+ observer: '__forceOverlayChanged',
279
+ sync: true,
280
+ },
281
+
282
+ /**
283
+ * Defines the containment of the detail area when the layout is in
284
+ * overlay mode. When set to `layout`, the overlay is confined to the
285
+ * layout. When set to `viewport`, the overlay is confined to the
286
+ * browser's viewport. Defaults to `layout`.
287
+ */
288
+ containment: {
289
+ type: String,
290
+ value: 'layout',
291
+ reflectToAttribute: true,
292
+ sync: true,
293
+ },
294
+
295
+ /**
296
+ * The threshold (in CSS length units) at which the layout switches to
297
+ * the "stack" mode, making detail area fully cover the master area.
298
+ *
299
+ * @attr {string} stack-threshold
300
+ */
301
+ stackThreshold: {
302
+ type: String,
303
+ observer: '__stackThresholdChanged',
304
+ sync: true,
305
+ },
306
+
307
+ /**
308
+ * When true, the layout does not use animated transitions for the detail area.
309
+ *
310
+ * @attr {boolean} no-animation
311
+ */
312
+ noAnimation: {
313
+ type: Boolean,
314
+ value: false,
315
+ },
316
+
317
+ /**
318
+ * When true, the component uses the overlay mode. This property is read-only.
319
+ * In order to enforce the overlay mode, use `forceOverlay` property.
320
+ * @protected
321
+ */
322
+ _overlay: {
323
+ type: Boolean,
324
+ attribute: 'overlay',
325
+ reflectToAttribute: true,
326
+ sync: true,
327
+ },
328
+
329
+ /**
330
+ * When true, the component uses the stack mode. This property is read-only.
331
+ * In order to enforce the stack mode, use `stackThreshold` property.
332
+ * @protected
333
+ */
334
+ _stack: {
335
+ type: Boolean,
336
+ attribute: 'stack',
337
+ reflectToAttribute: true,
338
+ sync: true,
339
+ },
165
340
  };
166
341
  }
167
342
 
343
+ /** @override */
344
+ get slotStyles() {
345
+ return [transitionStyles];
346
+ }
347
+
168
348
  /** @protected */
169
349
  render() {
170
350
  return html`
171
- <div id="master" part="master">
351
+ <div id="master" part="master" ?inert="${this._overlay && this.containment === 'layout'}">
172
352
  <slot></slot>
173
353
  </div>
174
- <div id="detail" part="detail">
354
+ <div
355
+ id="detail"
356
+ part="detail"
357
+ role="${this._overlay || this._stack ? 'dialog' : nothing}"
358
+ aria-modal="${this._overlay && this.containment === 'viewport' ? 'true' : nothing}"
359
+ >
175
360
  <slot name="detail" @slotchange="${this.__onDetailSlotChange}"></slot>
176
361
  </div>
177
362
  `;
@@ -179,8 +364,19 @@ class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitM
179
364
 
180
365
  /** @private */
181
366
  __onDetailSlotChange(e) {
182
- this.toggleAttribute('has-detail', e.target.assignedNodes().length > 0);
367
+ const children = e.target.assignedNodes();
368
+
369
+ this.toggleAttribute('has-detail', children.length > 0);
183
370
  this.__detectLayoutMode();
371
+
372
+ // Move focus to the detail area when it is added to the DOM,
373
+ // in case if the layout is using overlay or stack mode.
374
+ if ((this.hasAttribute('overlay') || this.hasAttribute('stack')) && children.length > 0) {
375
+ const focusables = getFocusableElements(children[0]);
376
+ if (focusables.length) {
377
+ focusables[0].focus();
378
+ }
379
+ }
184
380
  }
185
381
 
186
382
  /**
@@ -215,6 +411,33 @@ class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitM
215
411
  this.__detectLayoutMode();
216
412
  }
217
413
 
414
+ /** @private */
415
+ __orientationChanged(orientation, oldOrientation) {
416
+ if (orientation || oldOrientation) {
417
+ this.__detectLayoutMode();
418
+ }
419
+ }
420
+
421
+ /** @private */
422
+ __forceOverlayChanged(forceOverlay, oldForceOverlay) {
423
+ if (forceOverlay || oldForceOverlay) {
424
+ this.__detectLayoutMode();
425
+ }
426
+ }
427
+
428
+ /** @private */
429
+ __stackThresholdChanged(threshold, oldThreshold) {
430
+ if (threshold || oldThreshold) {
431
+ if (threshold) {
432
+ this.$.master.style.setProperty('--_stack-threshold', threshold);
433
+ } else {
434
+ this.$.master.style.removeProperty('--_stack-threshold');
435
+ }
436
+
437
+ this.__detectLayoutMode();
438
+ }
439
+ }
440
+
218
441
  /** @private */
219
442
  __updateStyleProperty(prop, size, oldSize) {
220
443
  if (size) {
@@ -228,11 +451,36 @@ class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitM
228
451
 
229
452
  /** @private */
230
453
  __detectLayoutMode() {
454
+ this._overlay = false;
455
+ this._stack = false;
456
+
457
+ if (this.forceOverlay) {
458
+ this._overlay = true;
459
+ return;
460
+ }
461
+
462
+ if (this.stackThreshold != null) {
463
+ const threshold = this.__getStackThresholdInPixels();
464
+ const size = this.orientation === 'vertical' ? this.offsetHeight : this.offsetWidth;
465
+ if (size <= threshold) {
466
+ this._stack = true;
467
+ return;
468
+ }
469
+ }
470
+
231
471
  if (!this.hasAttribute('has-detail')) {
232
- this.removeAttribute('overlay');
233
472
  return;
234
473
  }
235
474
 
475
+ if (this.orientation === 'vertical') {
476
+ this.__detectVerticalMode();
477
+ } else {
478
+ this.__detectHorizontalMode();
479
+ }
480
+ }
481
+
482
+ /** @private */
483
+ __detectHorizontalMode() {
236
484
  const detailWidth = this.$.detail.offsetWidth;
237
485
 
238
486
  // Detect minimum width needed by master content. Use max-width to ensure
@@ -245,18 +493,77 @@ class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitM
245
493
 
246
494
  // If the combined minimum size of both the master and the detail content
247
495
  // exceeds the size of the layout, the layout changes to the overlay mode.
248
- if (this.offsetWidth < masterWidth + detailWidth) {
249
- this.setAttribute('overlay', '');
250
- } else {
251
- this.removeAttribute('overlay');
252
- }
496
+ this._overlay = this.offsetWidth < masterWidth + detailWidth;
253
497
 
254
498
  // Toggling the overlay resizes master content, which can cause document
255
499
  // scroll bar to appear or disappear, and trigger another resize of the
256
500
  // layout which can affect previous measurements and end up in horizontal
257
501
  // scroll. Check if that is the case and if so, preserve the overlay mode.
258
502
  if (this.offsetWidth < this.scrollWidth) {
259
- this.setAttribute('overlay', '');
503
+ this._overlay = true;
504
+ }
505
+ }
506
+
507
+ /** @private */
508
+ __detectVerticalMode() {
509
+ // Remove overlay attribute temporarily to detect if there is enough space
510
+ // for both areas so that layout could switch back to the split mode.
511
+ this._overlay = false;
512
+
513
+ const masterHeight = this.$.master.clientHeight;
514
+
515
+ // If the combined minimum size of both the master and the detail content
516
+ // exceeds the available height, the layout changes to the overlay mode.
517
+ if (this.offsetHeight < masterHeight + this.$.detail.clientHeight) {
518
+ this._overlay = true;
519
+ }
520
+ }
521
+
522
+ /** @private */
523
+ __getStackThresholdInPixels() {
524
+ const { backgroundPositionY } = getComputedStyle(this.$.master, '::before');
525
+ return parseFloat(backgroundPositionY);
526
+ }
527
+
528
+ /**
529
+ * Sets the detail element to be displayed in the detail area and starts a
530
+ * view transition that animates adding, replacing or removing the detail
531
+ * area. During the view transition, the element is added to the DOM and
532
+ * assigned to the `detail` slot. Any previous detail element is removed.
533
+ * When passing null as the element, the current detail element is removed.
534
+ *
535
+ * If the browser does not support view transitions, the respective updates
536
+ * are applied immediately without starting a transition.
537
+ *
538
+ * @param element the new detail element, or null to remove the current detail
539
+ * @returns {Promise<void>}
540
+ */
541
+ async setDetail(element) {
542
+ // Don't start a transition if detail didn't change
543
+ const currentDetail = this.querySelector('[slot="detail"]');
544
+ if ((element || null) === currentDetail) {
545
+ return;
546
+ }
547
+
548
+ const updateSlot = () => {
549
+ // Remove old content
550
+ this.querySelectorAll('[slot="detail"]').forEach((oldElement) => oldElement.remove());
551
+ // Add new content
552
+ if (element) {
553
+ element.setAttribute('slot', 'detail');
554
+ this.appendChild(element);
555
+ }
556
+ };
557
+
558
+ if (typeof document.startViewTransition === 'function' && !this.noAnimation) {
559
+ const hasDetail = !!currentDetail;
560
+ const transitionType = hasDetail && element ? 'replace' : hasDetail ? 'remove' : 'add';
561
+ this.setAttribute('transition', transitionType);
562
+ const transition = document.startViewTransition(updateSlot);
563
+ await transition.finished;
564
+ this.removeAttribute('transition');
565
+ } else {
566
+ updateSlot();
260
567
  }
261
568
  }
262
569
  }
package/web-types.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/web-types",
3
3
  "name": "@vaadin/master-detail-layout",
4
- "version": "24.8.0-alpha3",
4
+ "version": "24.8.0-alpha5",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
8
8
  "elements": [
9
9
  {
10
10
  "name": "vaadin-master-detail-layout",
11
- "description": "`<vaadin-master-detail-layout>` is a web component for building UIs with a master\n(or primary) area and a detail (or secondary) area that is displayed next to, or\noverlaid on top of, the master area, depending on configuration and viewport size.",
11
+ "description": "`<vaadin-master-detail-layout>` is a web component for building UIs with a master\n(or primary) area and a detail (or secondary) area that is displayed next to, or\noverlaid on top of, the master area, depending on configuration and viewport size.\n\n### Styling\n\nThe following shadow DOM parts are available for styling:\n\nPart name | Description\n---------------|----------------------\n`master` | The master area\n`detail` | The detail area\n\nThe following state attributes are available for styling:\n\nAttribute | Description\n---------------| -----------\n`containment` | Set to `layout` or `viewport` depending on the containment.\n`orientation` | Set to `horizontal` or `vertical` depending on the orientation.\n`has-detail` | Set when the detail content is provided.\n`overlay` | Set when the layout is using the overlay mode.\n`stack` | Set when the layout is using the stack mode.\n\nSee [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.",
12
12
  "attributes": [
13
13
  {
14
14
  "name": "detail-size",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  {
36
36
  "name": "master-size",
37
- "description": "Fixed size (in CSS length units) to be set on the master area.\nWhen specified, it prevents the master area from growing or\nshrinking. If there is not enough space to show master and detail\nareas next to each other, the layout switches to the overlay mode.\nSetting `100%` enforces the overlay mode to be used by default.",
37
+ "description": "Fixed size (in CSS length units) to be set on the master area.\nWhen specified, it prevents the master area from growing or\nshrinking. If there is not enough space to show master and detail\nareas next to each other, the layout switches to the overlay mode.",
38
38
  "value": {
39
39
  "type": [
40
40
  "string",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  {
47
47
  "name": "master-min-size",
48
- "description": "Minimum size (in CSS length units) to be set on the master area.\nWhen specified, it prevents the master area from shrinking below\nthis size. If there is not enough space to show master and detail\nareas next to each other, the layout switches to the overlay mode.\nSetting `100%` enforces the overlay mode to be used by default.",
48
+ "description": "Minimum size (in CSS length units) to be set on the master area.\nWhen specified, it prevents the master area from shrinking below\nthis size. If there is not enough space to show master and detail\nareas next to each other, the layout switches to the overlay mode.",
49
49
  "value": {
50
50
  "type": [
51
51
  "string",
@@ -54,6 +54,61 @@
54
54
  ]
55
55
  }
56
56
  },
57
+ {
58
+ "name": "orientation",
59
+ "description": "Define how master and detail areas are shown next to each other,\nand the way how size and min-size properties are applied to them.\nPossible values are: `horizontal` or `vertical`.\nDefaults to horizontal.",
60
+ "value": {
61
+ "type": [
62
+ "string",
63
+ "null",
64
+ "undefined"
65
+ ]
66
+ }
67
+ },
68
+ {
69
+ "name": "force-overlay",
70
+ "description": "When specified, forces the layout to use overlay mode, even if\nthere is enough space for master and detail to be shown next to\neach other using the default (split) mode.",
71
+ "value": {
72
+ "type": [
73
+ "boolean",
74
+ "null",
75
+ "undefined"
76
+ ]
77
+ }
78
+ },
79
+ {
80
+ "name": "containment",
81
+ "description": "Defines the containment of the detail area when the layout is in\noverlay mode. When set to `layout`, the overlay is confined to the\nlayout. When set to `viewport`, the overlay is confined to the\nbrowser's viewport. Defaults to `layout`.",
82
+ "value": {
83
+ "type": [
84
+ "string",
85
+ "null",
86
+ "undefined"
87
+ ]
88
+ }
89
+ },
90
+ {
91
+ "name": "stack-threshold",
92
+ "description": "The threshold (in CSS length units) at which the layout switches to\nthe \"stack\" mode, making detail area fully cover the master area.",
93
+ "value": {
94
+ "type": [
95
+ "string",
96
+ "null",
97
+ "undefined"
98
+ ]
99
+ }
100
+ },
101
+ {
102
+ "name": "no-animation",
103
+ "description": "When true, the layout does not use animated transitions for the detail area.",
104
+ "value": {
105
+ "type": [
106
+ "boolean",
107
+ "null",
108
+ "undefined"
109
+ ]
110
+ }
111
+ },
57
112
  {
58
113
  "name": "theme",
59
114
  "description": "The theme variants to apply to the component.",
@@ -92,7 +147,7 @@
92
147
  },
93
148
  {
94
149
  "name": "masterSize",
95
- "description": "Fixed size (in CSS length units) to be set on the master area.\nWhen specified, it prevents the master area from growing or\nshrinking. If there is not enough space to show master and detail\nareas next to each other, the layout switches to the overlay mode.\nSetting `100%` enforces the overlay mode to be used by default.",
150
+ "description": "Fixed size (in CSS length units) to be set on the master area.\nWhen specified, it prevents the master area from growing or\nshrinking. If there is not enough space to show master and detail\nareas next to each other, the layout switches to the overlay mode.",
96
151
  "value": {
97
152
  "type": [
98
153
  "string",
@@ -103,7 +158,7 @@
103
158
  },
104
159
  {
105
160
  "name": "masterMinSize",
106
- "description": "Minimum size (in CSS length units) to be set on the master area.\nWhen specified, it prevents the master area from shrinking below\nthis size. If there is not enough space to show master and detail\nareas next to each other, the layout switches to the overlay mode.\nSetting `100%` enforces the overlay mode to be used by default.",
161
+ "description": "Minimum size (in CSS length units) to be set on the master area.\nWhen specified, it prevents the master area from shrinking below\nthis size. If there is not enough space to show master and detail\nareas next to each other, the layout switches to the overlay mode.",
107
162
  "value": {
108
163
  "type": [
109
164
  "string",
@@ -111,6 +166,61 @@
111
166
  "undefined"
112
167
  ]
113
168
  }
169
+ },
170
+ {
171
+ "name": "orientation",
172
+ "description": "Define how master and detail areas are shown next to each other,\nand the way how size and min-size properties are applied to them.\nPossible values are: `horizontal` or `vertical`.\nDefaults to horizontal.",
173
+ "value": {
174
+ "type": [
175
+ "string",
176
+ "null",
177
+ "undefined"
178
+ ]
179
+ }
180
+ },
181
+ {
182
+ "name": "forceOverlay",
183
+ "description": "When specified, forces the layout to use overlay mode, even if\nthere is enough space for master and detail to be shown next to\neach other using the default (split) mode.",
184
+ "value": {
185
+ "type": [
186
+ "boolean",
187
+ "null",
188
+ "undefined"
189
+ ]
190
+ }
191
+ },
192
+ {
193
+ "name": "containment",
194
+ "description": "Defines the containment of the detail area when the layout is in\noverlay mode. When set to `layout`, the overlay is confined to the\nlayout. When set to `viewport`, the overlay is confined to the\nbrowser's viewport. Defaults to `layout`.",
195
+ "value": {
196
+ "type": [
197
+ "string",
198
+ "null",
199
+ "undefined"
200
+ ]
201
+ }
202
+ },
203
+ {
204
+ "name": "stackThreshold",
205
+ "description": "The threshold (in CSS length units) at which the layout switches to\nthe \"stack\" mode, making detail area fully cover the master area.",
206
+ "value": {
207
+ "type": [
208
+ "string",
209
+ "null",
210
+ "undefined"
211
+ ]
212
+ }
213
+ },
214
+ {
215
+ "name": "noAnimation",
216
+ "description": "When true, the layout does not use animated transitions for the detail area.",
217
+ "value": {
218
+ "type": [
219
+ "boolean",
220
+ "null",
221
+ "undefined"
222
+ ]
223
+ }
114
224
  }
115
225
  ],
116
226
  "events": []
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/web-types",
3
3
  "name": "@vaadin/master-detail-layout",
4
- "version": "24.8.0-alpha3",
4
+ "version": "24.8.0-alpha5",
5
5
  "description-markup": "markdown",
6
6
  "framework": "lit",
7
7
  "framework-config": {
@@ -16,9 +16,23 @@
16
16
  "elements": [
17
17
  {
18
18
  "name": "vaadin-master-detail-layout",
19
- "description": "`<vaadin-master-detail-layout>` is a web component for building UIs with a master\n(or primary) area and a detail (or secondary) area that is displayed next to, or\noverlaid on top of, the master area, depending on configuration and viewport size.",
19
+ "description": "`<vaadin-master-detail-layout>` is a web component for building UIs with a master\n(or primary) area and a detail (or secondary) area that is displayed next to, or\noverlaid on top of, the master area, depending on configuration and viewport size.\n\n### Styling\n\nThe following shadow DOM parts are available for styling:\n\nPart name | Description\n---------------|----------------------\n`master` | The master area\n`detail` | The detail area\n\nThe following state attributes are available for styling:\n\nAttribute | Description\n---------------| -----------\n`containment` | Set to `layout` or `viewport` depending on the containment.\n`orientation` | Set to `horizontal` or `vertical` depending on the orientation.\n`has-detail` | Set when the detail content is provided.\n`overlay` | Set when the layout is using the overlay mode.\n`stack` | Set when the layout is using the stack mode.\n\nSee [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.",
20
20
  "extension": true,
21
21
  "attributes": [
22
+ {
23
+ "name": "?forceOverlay",
24
+ "description": "When specified, forces the layout to use overlay mode, even if\nthere is enough space for master and detail to be shown next to\neach other using the default (split) mode.",
25
+ "value": {
26
+ "kind": "expression"
27
+ }
28
+ },
29
+ {
30
+ "name": "?noAnimation",
31
+ "description": "When true, the layout does not use animated transitions for the detail area.",
32
+ "value": {
33
+ "kind": "expression"
34
+ }
35
+ },
22
36
  {
23
37
  "name": ".detailSize",
24
38
  "description": "Fixed size (in CSS length units) to be set on the detail area.\nWhen specified, it prevents the detail area from growing or\nshrinking. If there is not enough space to show master and detail\nareas next to each other, the layout switches to the overlay mode.",
@@ -35,14 +49,35 @@
35
49
  },
36
50
  {
37
51
  "name": ".masterSize",
38
- "description": "Fixed size (in CSS length units) to be set on the master area.\nWhen specified, it prevents the master area from growing or\nshrinking. If there is not enough space to show master and detail\nareas next to each other, the layout switches to the overlay mode.\nSetting `100%` enforces the overlay mode to be used by default.",
52
+ "description": "Fixed size (in CSS length units) to be set on the master area.\nWhen specified, it prevents the master area from growing or\nshrinking. If there is not enough space to show master and detail\nareas next to each other, the layout switches to the overlay mode.",
39
53
  "value": {
40
54
  "kind": "expression"
41
55
  }
42
56
  },
43
57
  {
44
58
  "name": ".masterMinSize",
45
- "description": "Minimum size (in CSS length units) to be set on the master area.\nWhen specified, it prevents the master area from shrinking below\nthis size. If there is not enough space to show master and detail\nareas next to each other, the layout switches to the overlay mode.\nSetting `100%` enforces the overlay mode to be used by default.",
59
+ "description": "Minimum size (in CSS length units) to be set on the master area.\nWhen specified, it prevents the master area from shrinking below\nthis size. If there is not enough space to show master and detail\nareas next to each other, the layout switches to the overlay mode.",
60
+ "value": {
61
+ "kind": "expression"
62
+ }
63
+ },
64
+ {
65
+ "name": ".orientation",
66
+ "description": "Define how master and detail areas are shown next to each other,\nand the way how size and min-size properties are applied to them.\nPossible values are: `horizontal` or `vertical`.\nDefaults to horizontal.",
67
+ "value": {
68
+ "kind": "expression"
69
+ }
70
+ },
71
+ {
72
+ "name": ".containment",
73
+ "description": "Defines the containment of the detail area when the layout is in\noverlay mode. When set to `layout`, the overlay is confined to the\nlayout. When set to `viewport`, the overlay is confined to the\nbrowser's viewport. Defaults to `layout`.",
74
+ "value": {
75
+ "kind": "expression"
76
+ }
77
+ },
78
+ {
79
+ "name": ".stackThreshold",
80
+ "description": "The threshold (in CSS length units) at which the layout switches to\nthe \"stack\" mode, making detail area fully cover the master area.",
46
81
  "value": {
47
82
  "kind": "expression"
48
83
  }