@vaadin/master-detail-layout 25.2.0-alpha1 → 25.2.0-alpha2
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 +3 -17
- package/package.json +8 -8
- package/src/styles/vaadin-master-detail-layout-base-styles.js +81 -27
- package/src/vaadin-master-detail-layout.d.ts +41 -15
- package/src/vaadin-master-detail-layout.js +330 -85
- package/web-types.json +4 -4
- package/web-types.lit.json +3 -3
- package/src/styles/vaadin-master-detail-layout-transition-base-styles.js +0 -107
package/custom-elements.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"declarations": [
|
|
24
24
|
{
|
|
25
25
|
"kind": "class",
|
|
26
|
-
"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
|
|
26
|
+
"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### Slots\n\nThe component has two main content areas: the master area (default slot)\nand the detail area (`detail` slot). When the detail doesn't fit next to\nthe master, it is shown as an overlay on top of the master area:\n\n```html\n<vaadin-master-detail-layout>\n <div>Master content</div>\n <div slot=\"detail\">Detail content</div>\n</vaadin-master-detail-layout>\n```\n\nThe component also supports a `detail-placeholder` slot for content shown\nin the detail area when no detail is selected. Unlike the `detail` slot,\nthe placeholder is simply hidden when it doesn't fit next to the master area,\nrather than shown as an overlay:\n\n```html\n<vaadin-master-detail-layout>\n <div>Master content</div>\n <div slot=\"detail-placeholder\">Select an item</div>\n</vaadin-master-detail-layout>\n```\n\n### Styling\n\nThe following shadow DOM parts are available for styling:\n\nPart name | Description\n----------------------|----------------------\n`backdrop` | Backdrop covering the master area in the overlay mode\n`master` | The master area\n`detail` | The detail area\n`detail-placeholder` | The detail placeholder area\n\nThe following state attributes are available for styling:\n\nAttribute | Description\n--------------------------|----------------------\n`expand` | Set to `master`, `detail`, or `both`.\n`orientation` | Set to `horizontal` or `vertical` depending on the orientation.\n`has-detail` | Set when the detail content is provided and visible.\n`has-detail-placeholder` | Set when the detail placeholder content is provided.\n`overlay` | Set when columns don't fit and the detail is shown as an overlay.\n`overlay-containment` | Set to `layout` or `viewport`.\n\nThe following custom CSS properties are available for styling:\n\nCustom CSS property |\n:----------------------------------------------------|\n| `--vaadin-master-detail-layout-border-color` |\n| `--vaadin-master-detail-layout-border-width` |\n| `--vaadin-master-detail-layout-detail-background` |\n| `--vaadin-master-detail-layout-detail-shadow` |\n| `--vaadin-overlay-backdrop-background` |\n\nSee [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.",
|
|
27
27
|
"name": "MasterDetailLayout",
|
|
28
28
|
"members": [
|
|
29
29
|
{
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"type": {
|
|
44
44
|
"text": "string"
|
|
45
45
|
},
|
|
46
|
-
"description": "Controls which column(s) expand to fill available space.\nPossible values: `'master'`, `'detail'`, `'both'`.\nDefaults to `'
|
|
46
|
+
"description": "Controls which column(s) expand to fill available space.\nPossible values: `'master'`, `'detail'`, `'both'`.\nDefaults to `'master'`.",
|
|
47
47
|
"attribute": "expand"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
@@ -95,16 +95,6 @@
|
|
|
95
95
|
},
|
|
96
96
|
"description": "Size (in CSS length units) for the detail area when shown as an\noverlay. When not set, falls back to `detailSize`. Set to `100%`\nto make the detail cover the full layout.",
|
|
97
97
|
"attribute": "overlay-size"
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
"kind": "field",
|
|
101
|
-
"name": "slotStyles",
|
|
102
|
-
"return": {
|
|
103
|
-
"type": {
|
|
104
|
-
"text": "!Array<!CSSResult>"
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
"readonly": true
|
|
108
98
|
}
|
|
109
99
|
],
|
|
110
100
|
"events": [
|
|
@@ -137,7 +127,7 @@
|
|
|
137
127
|
"type": {
|
|
138
128
|
"text": "string"
|
|
139
129
|
},
|
|
140
|
-
"description": "Controls which column(s) expand to fill available space.\nPossible values: `'master'`, `'detail'`, `'both'`.\nDefaults to `'
|
|
130
|
+
"description": "Controls which column(s) expand to fill available space.\nPossible values: `'master'`, `'detail'`, `'both'`.\nDefaults to `'master'`.",
|
|
141
131
|
"fieldName": "expand"
|
|
142
132
|
},
|
|
143
133
|
{
|
|
@@ -182,10 +172,6 @@
|
|
|
182
172
|
}
|
|
183
173
|
],
|
|
184
174
|
"mixins": [
|
|
185
|
-
{
|
|
186
|
-
"name": "SlotStylesMixin",
|
|
187
|
-
"package": "@vaadin/component-base/src/slot-styles-mixin.js"
|
|
188
|
-
},
|
|
189
175
|
{
|
|
190
176
|
"name": "ElementMixin",
|
|
191
177
|
"package": "@vaadin/component-base/src/element-mixin.js"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/master-detail-layout",
|
|
3
|
-
"version": "25.2.0-
|
|
3
|
+
"version": "25.2.0-alpha2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -34,16 +34,16 @@
|
|
|
34
34
|
"web-component"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@vaadin/a11y-base": "25.2.0-
|
|
38
|
-
"@vaadin/component-base": "25.2.0-
|
|
39
|
-
"@vaadin/vaadin-themable-mixin": "25.2.0-
|
|
37
|
+
"@vaadin/a11y-base": "25.2.0-alpha2",
|
|
38
|
+
"@vaadin/component-base": "25.2.0-alpha2",
|
|
39
|
+
"@vaadin/vaadin-themable-mixin": "25.2.0-alpha2",
|
|
40
40
|
"lit": "^3.0.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@vaadin/aura": "25.2.0-
|
|
44
|
-
"@vaadin/chai-plugins": "25.2.0-
|
|
43
|
+
"@vaadin/aura": "25.2.0-alpha2",
|
|
44
|
+
"@vaadin/chai-plugins": "25.2.0-alpha2",
|
|
45
45
|
"@vaadin/testing-helpers": "^2.0.0",
|
|
46
|
-
"@vaadin/vaadin-lumo-styles": "25.2.0-
|
|
46
|
+
"@vaadin/vaadin-lumo-styles": "25.2.0-alpha2",
|
|
47
47
|
"sinon": "^21.0.2"
|
|
48
48
|
},
|
|
49
49
|
"customElements": "custom-elements.json",
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
"web-types.json",
|
|
52
52
|
"web-types.lit.json"
|
|
53
53
|
],
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "34c9b41017bd4896f6e4b250ba50d1dd8535a061"
|
|
55
55
|
}
|
|
@@ -12,6 +12,10 @@ export const masterDetailLayoutStyles = css`
|
|
|
12
12
|
--_detail-size: 15em;
|
|
13
13
|
--_master-column: var(--_master-size) 0;
|
|
14
14
|
--_detail-column: var(--_detail-size) 0;
|
|
15
|
+
--_transition-duration: 0s;
|
|
16
|
+
--_transition-easing: cubic-bezier(0.78, 0, 0.22, 1);
|
|
17
|
+
--_rtl-multiplier: 1;
|
|
18
|
+
--_detail-offscreen: calc(30px * var(--_rtl-multiplier));
|
|
15
19
|
|
|
16
20
|
display: grid;
|
|
17
21
|
box-sizing: border-box;
|
|
@@ -27,39 +31,55 @@ export const masterDetailLayoutStyles = css`
|
|
|
27
31
|
display: none !important;
|
|
28
32
|
}
|
|
29
33
|
|
|
34
|
+
:host([dir='rtl']) {
|
|
35
|
+
--_rtl-multiplier: -1;
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
:host([orientation='vertical']) {
|
|
39
|
+
--_detail-offscreen: 0 30px;
|
|
40
|
+
|
|
31
41
|
grid-template-columns: 100%;
|
|
32
42
|
grid-template-rows: [master-start] var(--_master-column) [detail-start] var(--_detail-column) [detail-end];
|
|
33
43
|
}
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
[part~='detail'] {
|
|
45
|
+
:is(#master, #detail, #detail-placeholder, #outgoing) {
|
|
37
46
|
box-sizing: border-box;
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
|
|
49
|
+
#detail-placeholder {
|
|
50
|
+
display: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:host([has-detail-placeholder]:not([has-detail], [overlay])) #detail-placeholder {
|
|
54
|
+
display: block;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#master {
|
|
41
58
|
grid-column: master-start / detail-start;
|
|
59
|
+
grid-row: 1;
|
|
42
60
|
}
|
|
43
61
|
|
|
44
|
-
|
|
62
|
+
:is(#detail, #detail-placeholder, #outgoing) {
|
|
45
63
|
grid-column: detail-start / detail-end;
|
|
64
|
+
grid-row: 1;
|
|
46
65
|
}
|
|
47
66
|
|
|
48
|
-
:host([orientation='vertical'])
|
|
49
|
-
grid-column:
|
|
67
|
+
:host([orientation='vertical']) #master {
|
|
68
|
+
grid-column: 1;
|
|
50
69
|
grid-row: master-start / detail-start;
|
|
51
70
|
}
|
|
52
71
|
|
|
53
|
-
:host([orientation='vertical'])
|
|
54
|
-
grid-column:
|
|
72
|
+
:host([orientation='vertical']) :is(#detail, #detail-placeholder, #outgoing) {
|
|
73
|
+
grid-column: 1;
|
|
55
74
|
grid-row: detail-start / detail-end;
|
|
56
75
|
}
|
|
57
76
|
|
|
58
|
-
|
|
77
|
+
#backdrop {
|
|
59
78
|
position: absolute;
|
|
60
79
|
inset: 0;
|
|
61
80
|
z-index: 1;
|
|
62
|
-
|
|
81
|
+
opacity: 0;
|
|
82
|
+
pointer-events: none;
|
|
63
83
|
background: var(--vaadin-overlay-backdrop-background, rgba(0, 0, 0, 0.2));
|
|
64
84
|
forced-color-adjust: none;
|
|
65
85
|
}
|
|
@@ -69,8 +89,9 @@ export const masterDetailLayoutStyles = css`
|
|
|
69
89
|
--_master-column: var(--_master-size) 1fr;
|
|
70
90
|
}
|
|
71
91
|
|
|
72
|
-
:host(
|
|
73
|
-
:host([
|
|
92
|
+
:host([keep-detail-column-offscreen]),
|
|
93
|
+
:host([has-detail-placeholder][overlay]),
|
|
94
|
+
:host(:not([has-detail-placeholder], [has-detail])) {
|
|
74
95
|
--_master-column: var(--_master-size) calc(100% - var(--_master-size));
|
|
75
96
|
}
|
|
76
97
|
|
|
@@ -79,54 +100,87 @@ export const masterDetailLayoutStyles = css`
|
|
|
79
100
|
--_detail-column: var(--_detail-size) 1fr;
|
|
80
101
|
}
|
|
81
102
|
|
|
82
|
-
:host([orientation='horizontal']
|
|
103
|
+
:host([orientation='horizontal']) #detail-placeholder,
|
|
104
|
+
:host([orientation='horizontal'][has-detail]:not([overlay])) #detail {
|
|
83
105
|
border-inline-start: var(--vaadin-master-detail-layout-border-width, 1px) solid
|
|
84
106
|
var(--vaadin-master-detail-layout-border-color, var(--vaadin-border-color-secondary));
|
|
85
107
|
}
|
|
86
108
|
|
|
87
|
-
:host([orientation='vertical']
|
|
109
|
+
:host([orientation='vertical']) #detail-placeholder,
|
|
110
|
+
:host([orientation='vertical'][has-detail]:not([overlay])) #detail {
|
|
88
111
|
border-top: var(--vaadin-master-detail-layout-border-width, 1px) solid
|
|
89
112
|
var(--vaadin-master-detail-layout-border-color, var(--vaadin-border-color-secondary));
|
|
90
113
|
}
|
|
91
114
|
|
|
92
|
-
:
|
|
115
|
+
/* Detail transition: off-screen by default, on-screen when has-detail */
|
|
116
|
+
#detail {
|
|
117
|
+
translate: var(--_detail-offscreen);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
:host([has-detail]) #detail {
|
|
121
|
+
translate: none;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#outgoing:not([hidden]) {
|
|
125
|
+
z-index: 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
:host([overlay]) {
|
|
129
|
+
--_detail-offscreen: calc((100% + 30px) * var(--_rtl-multiplier));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
:host([overlay][orientation='vertical']) {
|
|
133
|
+
--_detail-offscreen: 0 calc(100% + 30px);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
:host([has-detail][overlay]) :is(#detail, #outgoing) {
|
|
93
137
|
position: absolute;
|
|
94
138
|
z-index: 2;
|
|
95
139
|
background: var(--vaadin-master-detail-layout-detail-background, var(--vaadin-background-color));
|
|
96
140
|
box-shadow: var(--vaadin-master-detail-layout-detail-shadow, 0 0 20px 0 rgba(0, 0, 0, 0.3));
|
|
97
141
|
grid-column: none;
|
|
142
|
+
grid-row: none;
|
|
98
143
|
}
|
|
99
144
|
|
|
100
|
-
:host([
|
|
101
|
-
|
|
145
|
+
:host([has-detail][overlay]) #backdrop {
|
|
146
|
+
opacity: 1;
|
|
147
|
+
pointer-events: auto;
|
|
102
148
|
}
|
|
103
149
|
|
|
104
|
-
:host([
|
|
150
|
+
:host([has-detail][overlay]:not([orientation='vertical'])) :is(#detail, #outgoing) {
|
|
105
151
|
inset-block: 0;
|
|
106
|
-
width: var(--_overlay-size, var(--_detail-size, min-content));
|
|
107
152
|
inset-inline-end: 0;
|
|
153
|
+
width: var(--_overlay-size, var(--_detail-size, min-content));
|
|
108
154
|
}
|
|
109
155
|
|
|
110
|
-
:host([
|
|
111
|
-
grid-column: auto;
|
|
112
|
-
grid-row: none;
|
|
156
|
+
:host([has-detail][overlay][orientation='vertical']) :is(#detail, #outgoing) {
|
|
113
157
|
inset-inline: 0;
|
|
114
|
-
height: var(--_overlay-size, var(--_detail-size, min-content));
|
|
115
158
|
inset-block-end: 0;
|
|
159
|
+
height: var(--_overlay-size, var(--_detail-size, min-content));
|
|
116
160
|
}
|
|
117
161
|
|
|
118
|
-
:host([
|
|
119
|
-
:host([overflow][overlay-containment='viewport']) [part~='backdrop'] {
|
|
162
|
+
:host([has-detail][overlay][overlay-containment='viewport']) :is(#detail, #outgoing, #backdrop) {
|
|
120
163
|
position: fixed;
|
|
121
164
|
}
|
|
122
165
|
|
|
123
166
|
@media (forced-colors: active) {
|
|
124
|
-
:host([
|
|
167
|
+
:host([has-detail][overlay]) :is(#detail, #outgoing) {
|
|
125
168
|
outline: 3px solid !important;
|
|
126
169
|
}
|
|
127
170
|
|
|
128
|
-
|
|
171
|
+
:is(#detail, #detail-placeholder, #outgoing) {
|
|
129
172
|
background: Canvas !important;
|
|
130
173
|
}
|
|
131
174
|
}
|
|
175
|
+
|
|
176
|
+
/* Enable transitions when motion is allowed */
|
|
177
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
178
|
+
:host(:not([no-animation], [transition='replace'])) {
|
|
179
|
+
--_transition-duration: 200ms;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
:host([overlay]:not([no-animation])) {
|
|
183
|
+
--_transition-duration: 300ms;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
132
186
|
`;
|
|
@@ -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,52 @@ 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
|
-
* `orientation`
|
|
39
|
-
* `has-detail`
|
|
40
|
-
* `
|
|
41
|
-
* `overlay
|
|
60
|
+
* Attribute | Description
|
|
61
|
+
* --------------------------|----------------------
|
|
62
|
+
* `expand` | Set to `master`, `detail`, or `both`.
|
|
63
|
+
* `orientation` | Set to `horizontal` or `vertical` depending on the orientation.
|
|
64
|
+
* `has-detail` | Set when the detail content is provided and visible.
|
|
65
|
+
* `has-detail-placeholder` | Set when the detail placeholder content is provided.
|
|
66
|
+
* `overlay` | Set when columns don't fit and the detail is shown as an overlay.
|
|
67
|
+
* `overlay-containment` | Set to `layout` or `viewport`.
|
|
42
68
|
*
|
|
43
69
|
* The following custom CSS properties are available for styling:
|
|
44
70
|
*
|
|
@@ -55,7 +81,7 @@ export interface MasterDetailLayoutEventMap extends HTMLElementEventMap, MasterD
|
|
|
55
81
|
* @fires {CustomEvent} backdrop-click - Fired when the user clicks the backdrop in the overlay mode.
|
|
56
82
|
* @fires {CustomEvent} detail-escape-press - Fired when the user presses Escape in the detail area.
|
|
57
83
|
*/
|
|
58
|
-
declare class MasterDetailLayout extends
|
|
84
|
+
declare class MasterDetailLayout extends ThemableMixin(ElementMixin(HTMLElement)) {
|
|
59
85
|
/**
|
|
60
86
|
* Size (in CSS length units) to be set on the detail area in
|
|
61
87
|
* the CSS grid layout. If there is not enough space to show
|
|
@@ -106,7 +132,7 @@ declare class MasterDetailLayout extends SlotStylesMixin(ThemableMixin(ElementMi
|
|
|
106
132
|
/**
|
|
107
133
|
* Controls which column(s) expand to fill available space.
|
|
108
134
|
* Possible values: `'master'`, `'detail'`, `'both'`.
|
|
109
|
-
* Defaults to `'
|
|
135
|
+
* Defaults to `'master'`.
|
|
110
136
|
*/
|
|
111
137
|
expand: 'master' | 'detail' | 'both';
|
|
112
138
|
|
|
@@ -8,10 +8,8 @@ import { getFocusableElements } from '@vaadin/a11y-base/src/focus-utils.js';
|
|
|
8
8
|
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
|
|
9
9
|
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
10
10
|
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
|
|
11
|
-
import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
|
|
12
11
|
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
13
12
|
import { masterDetailLayoutStyles } from './styles/vaadin-master-detail-layout-base-styles.js';
|
|
14
|
-
import { masterDetailLayoutTransitionStyles } from './styles/vaadin-master-detail-layout-transition-base-styles.js';
|
|
15
13
|
|
|
16
14
|
function parseTrackSizes(gridTemplate) {
|
|
17
15
|
return gridTemplate
|
|
@@ -27,25 +25,52 @@ function parseTrackSizes(gridTemplate) {
|
|
|
27
25
|
* (or primary) area and a detail (or secondary) area that is displayed next to, or
|
|
28
26
|
* overlaid on top of, the master area, depending on configuration and viewport size.
|
|
29
27
|
*
|
|
28
|
+
* ### Slots
|
|
29
|
+
*
|
|
30
|
+
* The component has two main content areas: the master area (default slot)
|
|
31
|
+
* and the detail area (`detail` slot). When the detail doesn't fit next to
|
|
32
|
+
* the master, it is shown as an overlay on top of the master area:
|
|
33
|
+
*
|
|
34
|
+
* ```html
|
|
35
|
+
* <vaadin-master-detail-layout>
|
|
36
|
+
* <div>Master content</div>
|
|
37
|
+
* <div slot="detail">Detail content</div>
|
|
38
|
+
* </vaadin-master-detail-layout>
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* The component also supports a `detail-placeholder` slot for content shown
|
|
42
|
+
* in the detail area when no detail is selected. Unlike the `detail` slot,
|
|
43
|
+
* the placeholder is simply hidden when it doesn't fit next to the master area,
|
|
44
|
+
* rather than shown as an overlay:
|
|
45
|
+
*
|
|
46
|
+
* ```html
|
|
47
|
+
* <vaadin-master-detail-layout>
|
|
48
|
+
* <div>Master content</div>
|
|
49
|
+
* <div slot="detail-placeholder">Select an item</div>
|
|
50
|
+
* </vaadin-master-detail-layout>
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
30
53
|
* ### Styling
|
|
31
54
|
*
|
|
32
55
|
* The following shadow DOM parts are available for styling:
|
|
33
56
|
*
|
|
34
|
-
* Part name
|
|
35
|
-
*
|
|
36
|
-
* `backdrop`
|
|
37
|
-
* `master`
|
|
38
|
-
* `detail`
|
|
57
|
+
* Part name | Description
|
|
58
|
+
* ----------------------|----------------------
|
|
59
|
+
* `backdrop` | Backdrop covering the master area in the overlay mode
|
|
60
|
+
* `master` | The master area
|
|
61
|
+
* `detail` | The detail area
|
|
62
|
+
* `detail-placeholder` | The detail placeholder area
|
|
39
63
|
*
|
|
40
64
|
* The following state attributes are available for styling:
|
|
41
65
|
*
|
|
42
|
-
* Attribute
|
|
43
|
-
*
|
|
44
|
-
* `expand`
|
|
45
|
-
* `orientation`
|
|
46
|
-
* `has-detail`
|
|
47
|
-
* `
|
|
48
|
-
* `overlay
|
|
66
|
+
* Attribute | Description
|
|
67
|
+
* --------------------------|----------------------
|
|
68
|
+
* `expand` | Set to `master`, `detail`, or `both`.
|
|
69
|
+
* `orientation` | Set to `horizontal` or `vertical` depending on the orientation.
|
|
70
|
+
* `has-detail` | Set when the detail content is provided and visible.
|
|
71
|
+
* `has-detail-placeholder` | Set when the detail placeholder content is provided.
|
|
72
|
+
* `overlay` | Set when columns don't fit and the detail is shown as an overlay.
|
|
73
|
+
* `overlay-containment` | Set to `layout` or `viewport`.
|
|
49
74
|
*
|
|
50
75
|
* The following custom CSS properties are available for styling:
|
|
51
76
|
*
|
|
@@ -66,9 +91,8 @@ function parseTrackSizes(gridTemplate) {
|
|
|
66
91
|
* @extends HTMLElement
|
|
67
92
|
* @mixes ThemableMixin
|
|
68
93
|
* @mixes ElementMixin
|
|
69
|
-
* @mixes SlotStylesMixin
|
|
70
94
|
*/
|
|
71
|
-
class MasterDetailLayout extends
|
|
95
|
+
class MasterDetailLayout extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))) {
|
|
72
96
|
static get is() {
|
|
73
97
|
return 'vaadin-master-detail-layout';
|
|
74
98
|
}
|
|
@@ -151,11 +175,11 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
151
175
|
/**
|
|
152
176
|
* Controls which column(s) expand to fill available space.
|
|
153
177
|
* Possible values: `'master'`, `'detail'`, `'both'`.
|
|
154
|
-
* Defaults to `'
|
|
178
|
+
* Defaults to `'master'`.
|
|
155
179
|
*/
|
|
156
180
|
expand: {
|
|
157
181
|
type: String,
|
|
158
|
-
value: '
|
|
182
|
+
value: 'master',
|
|
159
183
|
reflectToAttribute: true,
|
|
160
184
|
sync: true,
|
|
161
185
|
},
|
|
@@ -168,6 +192,13 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
168
192
|
noAnimation: {
|
|
169
193
|
type: Boolean,
|
|
170
194
|
value: false,
|
|
195
|
+
reflectToAttribute: true,
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/** @private */
|
|
199
|
+
__replacing: {
|
|
200
|
+
type: Boolean,
|
|
201
|
+
sync: true,
|
|
171
202
|
},
|
|
172
203
|
};
|
|
173
204
|
}
|
|
@@ -176,22 +207,20 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
176
207
|
return true;
|
|
177
208
|
}
|
|
178
209
|
|
|
179
|
-
/** @return {!Array<!CSSResult>} */
|
|
180
|
-
get slotStyles() {
|
|
181
|
-
return [masterDetailLayoutTransitionStyles];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
210
|
/** @protected */
|
|
185
211
|
render() {
|
|
186
|
-
const isOverlay = this.hasAttribute('has-detail') && this.hasAttribute('
|
|
212
|
+
const isOverlay = this.hasAttribute('has-detail') && this.hasAttribute('overlay');
|
|
187
213
|
const isViewport = isOverlay && this.overlayContainment === 'viewport';
|
|
188
214
|
const isLayoutContained = isOverlay && !isViewport;
|
|
189
215
|
|
|
190
216
|
return html`
|
|
191
|
-
<div part="backdrop" @click="${this.__onBackdropClick}"></div>
|
|
217
|
+
<div id="backdrop" part="backdrop" @click="${this.__onBackdropClick}"></div>
|
|
192
218
|
<div id="master" part="master" ?inert="${isLayoutContained}">
|
|
193
219
|
<slot @slotchange="${this.__onSlotChange}"></slot>
|
|
194
220
|
</div>
|
|
221
|
+
<div id="outgoing" inert ?hidden="${!this.__replacing}">
|
|
222
|
+
<slot name="detail-outgoing"></slot>
|
|
223
|
+
</div>
|
|
195
224
|
<div
|
|
196
225
|
id="detail"
|
|
197
226
|
part="detail"
|
|
@@ -201,6 +230,9 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
201
230
|
>
|
|
202
231
|
<slot name="detail" @slotchange="${this.__onSlotChange}"></slot>
|
|
203
232
|
</div>
|
|
233
|
+
<div id="detail-placeholder" part="detail-placeholder">
|
|
234
|
+
<slot name="detail-placeholder" @slotchange="${this.__onSlotChange}"></slot>
|
|
235
|
+
</div>
|
|
204
236
|
`;
|
|
205
237
|
}
|
|
206
238
|
|
|
@@ -215,6 +247,7 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
215
247
|
super.disconnectedCallback();
|
|
216
248
|
this.__resizeObserver.disconnect();
|
|
217
249
|
cancelAnimationFrame(this.__resizeRaf);
|
|
250
|
+
this.__endTransition();
|
|
218
251
|
}
|
|
219
252
|
|
|
220
253
|
/** @private */
|
|
@@ -275,20 +308,24 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
275
308
|
* @private
|
|
276
309
|
*/
|
|
277
310
|
__computeLayoutState() {
|
|
278
|
-
const detailContent = this.querySelector('[slot="detail"]');
|
|
311
|
+
const detailContent = this.querySelector(':scope > [slot="detail"]');
|
|
312
|
+
const detailPlaceholder = this.querySelector(':scope > [slot="detail-placeholder"]');
|
|
313
|
+
|
|
279
314
|
const hadDetail = this.hasAttribute('has-detail');
|
|
280
315
|
const hasDetail = detailContent != null && detailContent.checkVisibility();
|
|
281
|
-
const
|
|
316
|
+
const hasDetailPlaceholder = !!detailPlaceholder;
|
|
317
|
+
const hasOverflow = (hasDetail || hasDetailPlaceholder) && this.__checkOverflow();
|
|
318
|
+
|
|
282
319
|
const focusTarget = !hadDetail && hasDetail && hasOverflow ? getFocusableElements(detailContent)[0] : null;
|
|
283
|
-
return { hadDetail, hasDetail, hasOverflow, focusTarget };
|
|
320
|
+
return { hadDetail, hasDetail, hasDetailPlaceholder, hasOverflow, focusTarget };
|
|
284
321
|
}
|
|
285
322
|
|
|
286
323
|
/**
|
|
287
324
|
* Applies layout state to DOM attributes. Pure writes, no reads.
|
|
288
325
|
* @private
|
|
289
326
|
*/
|
|
290
|
-
__applyLayoutState({ hadDetail, hasDetail, hasOverflow, focusTarget }) {
|
|
291
|
-
// Set keep-detail-column-offscreen when detail first appears with
|
|
327
|
+
__applyLayoutState({ hadDetail, hasDetail, hasDetailPlaceholder, hasOverflow, focusTarget }) {
|
|
328
|
+
// Set keep-detail-column-offscreen when detail first appears with overlay
|
|
292
329
|
// to prevent master width from jumping.
|
|
293
330
|
if (!hadDetail && hasDetail && hasOverflow) {
|
|
294
331
|
this.setAttribute('keep-detail-column-offscreen', '');
|
|
@@ -296,15 +333,16 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
296
333
|
this.removeAttribute('keep-detail-column-offscreen');
|
|
297
334
|
}
|
|
298
335
|
|
|
336
|
+
this.toggleAttribute('overlay', hasOverflow);
|
|
299
337
|
this.toggleAttribute('has-detail', hasDetail);
|
|
300
|
-
this.toggleAttribute('
|
|
338
|
+
this.toggleAttribute('has-detail-placeholder', hasDetailPlaceholder);
|
|
301
339
|
|
|
302
340
|
// Re-render to update ARIA attributes (role, aria-modal, inert)
|
|
303
|
-
// which depend on has-detail and
|
|
341
|
+
// which depend on has-detail and overlay state.
|
|
304
342
|
this.requestUpdate();
|
|
305
343
|
|
|
306
344
|
if (focusTarget) {
|
|
307
|
-
focusTarget.focus();
|
|
345
|
+
focusTarget.focus({ preventScroll: true });
|
|
308
346
|
}
|
|
309
347
|
}
|
|
310
348
|
|
|
@@ -342,19 +380,18 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
342
380
|
}
|
|
343
381
|
|
|
344
382
|
/**
|
|
345
|
-
* Sets the detail element to be displayed in the detail area and starts
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
383
|
+
* Sets the detail element to be displayed in the detail area and starts an
|
|
384
|
+
* animated transition for adding, replacing or removing the detail area.
|
|
385
|
+
* The element is added to the DOM and assigned to the `detail` slot. Any
|
|
386
|
+
* previous detail element is removed. When passing null as the element,
|
|
387
|
+
* the current detail element is removed.
|
|
350
388
|
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
* also be skipped using the `skipTransition` parameter.
|
|
389
|
+
* The transition can be skipped using the `skipTransition` parameter or
|
|
390
|
+
* the `noAnimation` property.
|
|
354
391
|
*
|
|
355
392
|
* @param element the new detail element, or null to remove the current detail
|
|
356
393
|
* @param skipTransition whether to skip the transition
|
|
357
|
-
* @
|
|
394
|
+
* @return {Promise<void>}
|
|
358
395
|
* @protected
|
|
359
396
|
*/
|
|
360
397
|
_setDetail(element, skipTransition) {
|
|
@@ -374,13 +411,17 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
374
411
|
}
|
|
375
412
|
};
|
|
376
413
|
|
|
377
|
-
if (skipTransition) {
|
|
414
|
+
if (skipTransition || this.noAnimation) {
|
|
378
415
|
updateSlot();
|
|
416
|
+
queueMicrotask(() => {
|
|
417
|
+
const state = this.__computeLayoutState();
|
|
418
|
+
this.__applyLayoutState(state);
|
|
419
|
+
});
|
|
379
420
|
return Promise.resolve();
|
|
380
421
|
}
|
|
381
422
|
|
|
382
|
-
const
|
|
383
|
-
|
|
423
|
+
const transitionType = this.__getTransitionType(currentDetail, element);
|
|
424
|
+
|
|
384
425
|
return this._startTransition(transitionType, () => {
|
|
385
426
|
// Update the DOM
|
|
386
427
|
updateSlot();
|
|
@@ -390,69 +431,273 @@ class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(Poly
|
|
|
390
431
|
}
|
|
391
432
|
|
|
392
433
|
/**
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
434
|
+
* Determines the transition type for a detail change.
|
|
435
|
+
*
|
|
436
|
+
* Returns 'replace' in two cases:
|
|
437
|
+
* - Swapping one detail for another (standard replace).
|
|
438
|
+
* - Swapping between placeholder and detail in split mode,
|
|
439
|
+
* so the swap appears instant (replace has 0ms duration in split).
|
|
440
|
+
* In overlay mode, placeholder doesn't participate in transitions,
|
|
441
|
+
* so standard 'add'/'remove' are used instead.
|
|
442
|
+
*
|
|
443
|
+
* @param {Element | null} currentDetail
|
|
444
|
+
* @param {Element | null} newDetail
|
|
445
|
+
* @return {string}
|
|
446
|
+
* @private
|
|
447
|
+
*/
|
|
448
|
+
__getTransitionType(currentDetail, newDetail) {
|
|
449
|
+
if (currentDetail && newDetail) {
|
|
450
|
+
return 'replace';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const hasPlaceholder = !!this.querySelector('[slot="detail-placeholder"]');
|
|
454
|
+
if (hasPlaceholder && !this.hasAttribute('overlay')) {
|
|
455
|
+
return 'replace';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return currentDetail ? 'remove' : 'add';
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Starts an animated transition for adding, replacing or removing the
|
|
463
|
+
* detail area using the Web Animations API.
|
|
464
|
+
*
|
|
465
|
+
* For 'remove', the DOM update is deferred until the slide-out completes.
|
|
466
|
+
* For 'add'/'replace', the DOM is updated immediately and the slide-in
|
|
467
|
+
* plays on the new content.
|
|
400
468
|
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
469
|
+
* Animations are interruptible: starting a new transition cancels any
|
|
470
|
+
* in-progress animation and the new animation picks up from the
|
|
471
|
+
* interrupted position (see `__captureDetailState`).
|
|
404
472
|
*
|
|
405
473
|
* @param transitionType
|
|
406
474
|
* @param updateCallback
|
|
407
|
-
* @
|
|
475
|
+
* @return {Promise<void>}
|
|
408
476
|
* @protected
|
|
409
477
|
*/
|
|
410
478
|
_startTransition(transitionType, updateCallback) {
|
|
411
|
-
|
|
412
|
-
if (!useTransition) {
|
|
479
|
+
if (this.noAnimation) {
|
|
413
480
|
updateCallback();
|
|
414
481
|
return Promise.resolve();
|
|
415
482
|
}
|
|
416
483
|
|
|
484
|
+
// Capture mid-flight state before cancelling active animations
|
|
485
|
+
const interrupted = this.__captureDetailState();
|
|
486
|
+
|
|
487
|
+
this.__endTransition();
|
|
488
|
+
|
|
489
|
+
if (transitionType === 'replace') {
|
|
490
|
+
this.__snapshotOutgoing();
|
|
491
|
+
}
|
|
492
|
+
|
|
417
493
|
this.setAttribute('transition', transitionType);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
494
|
+
|
|
495
|
+
if (transitionType !== 'remove') {
|
|
496
|
+
updateCallback();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const opts = this.__getAnimationParams();
|
|
500
|
+
opts.interrupted = interrupted;
|
|
501
|
+
opts.overlay = this.hasAttribute('overlay');
|
|
502
|
+
|
|
503
|
+
return this.__animateTransition(transitionType, opts, updateCallback);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Creates slide animation(s) for the given transition type and returns
|
|
508
|
+
* a promise that resolves when the primary animation completes.
|
|
509
|
+
* A version counter prevents stale callbacks from executing after
|
|
510
|
+
* a newer transition has started.
|
|
511
|
+
*
|
|
512
|
+
* @param {string} transitionType
|
|
513
|
+
* @param {{ offscreen: string, duration: number, easing: string, interrupted?: { translate: string, opacity: string }, overlay?: boolean }} opts
|
|
514
|
+
* @param {Function} updateCallback
|
|
515
|
+
* @return {Promise<void>}
|
|
516
|
+
* @private
|
|
517
|
+
*/
|
|
518
|
+
__animateTransition(transitionType, opts, updateCallback) {
|
|
519
|
+
const version = (this.__transitionVersion = (this.__transitionVersion || 0) + 1);
|
|
520
|
+
|
|
521
|
+
return new Promise((resolve) => {
|
|
522
|
+
this.__transitionResolve = resolve;
|
|
523
|
+
|
|
524
|
+
const onFinish = (callback) => {
|
|
525
|
+
if (this.__transitionVersion === version) {
|
|
526
|
+
if (callback) {
|
|
527
|
+
callback();
|
|
528
|
+
}
|
|
529
|
+
this.__endTransition();
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
if (transitionType === 'remove') {
|
|
534
|
+
this.__slide(this.$.detail, false, opts).then(() => onFinish(updateCallback));
|
|
535
|
+
} else if (transitionType === 'replace') {
|
|
536
|
+
// Outgoing slides out on top (z-index), revealing incoming underneath.
|
|
537
|
+
// In overlay mode, the incoming also slides in simultaneously.
|
|
538
|
+
this.__slide(this.$.outgoing, false, opts).then(() => onFinish());
|
|
539
|
+
if (opts.overlay) {
|
|
540
|
+
this.__slide(this.$.detail, true, { ...opts, interrupted: null });
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
this.__slide(this.$.detail, true, opts).then(() => onFinish());
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Fade backdrop in/out for overlay add/remove (not replace — backdrop stays visible)
|
|
547
|
+
if (opts.overlay && transitionType !== 'replace') {
|
|
548
|
+
const fadeIn = transitionType !== 'remove';
|
|
549
|
+
this.__animate(this.$.backdrop, [{ opacity: fadeIn ? 0 : 1 }, { opacity: fadeIn ? 1 : 0 }], {
|
|
550
|
+
duration: opts.duration,
|
|
551
|
+
easing: 'linear',
|
|
552
|
+
});
|
|
553
|
+
}
|
|
426
554
|
});
|
|
427
|
-
return this.__transition.finished;
|
|
428
555
|
}
|
|
429
556
|
|
|
430
557
|
/**
|
|
431
|
-
* Finishes the current
|
|
432
|
-
*
|
|
433
|
-
* change in the layout.
|
|
558
|
+
* Finishes the current transition by detecting and applying the layout
|
|
559
|
+
* state. This method should be called after the DOM has been updated.
|
|
434
560
|
*
|
|
435
|
-
* @returns {Promise<void>}
|
|
436
561
|
* @protected
|
|
437
562
|
*/
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
563
|
+
_finishTransition() {
|
|
564
|
+
const state = this.__computeLayoutState();
|
|
565
|
+
this.__applyLayoutState(state);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Captures the detail panel's current animated state (translate and
|
|
570
|
+
* opacity). Must be called BEFORE `animation.cancel()`, because
|
|
571
|
+
* cancel removes the animation effect and the element reverts to
|
|
572
|
+
* its CSS resting state.
|
|
573
|
+
*
|
|
574
|
+
* Returns null when there is no active animation.
|
|
575
|
+
*
|
|
576
|
+
* @return {{ translate: string, opacity: string } | null}
|
|
577
|
+
* @private
|
|
578
|
+
*/
|
|
579
|
+
__captureDetailState() {
|
|
580
|
+
if (!this.__activeAnimations || this.__activeAnimations.length === 0) {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
const { translate, opacity } = getComputedStyle(this.$.detail);
|
|
584
|
+
return { translate, opacity };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Reads animation parameters from CSS custom properties. Called once
|
|
589
|
+
* per transition so that animating stays free of layout reads.
|
|
590
|
+
*
|
|
591
|
+
* @return {{ offscreen: string, duration: number, easing: string }}
|
|
592
|
+
* @private
|
|
593
|
+
*/
|
|
594
|
+
__getAnimationParams() {
|
|
595
|
+
const cs = getComputedStyle(this);
|
|
596
|
+
const offscreen = cs.getPropertyValue('--_detail-offscreen').trim();
|
|
597
|
+
const durationStr = cs.getPropertyValue('--_transition-duration').trim();
|
|
598
|
+
const duration = durationStr.endsWith('ms') ? parseFloat(durationStr) : parseFloat(durationStr) * 1000;
|
|
599
|
+
const easing = cs.getPropertyValue('--_transition-easing').trim();
|
|
600
|
+
return { offscreen, duration, easing };
|
|
601
|
+
}
|
|
446
602
|
|
|
447
|
-
|
|
603
|
+
/**
|
|
604
|
+
* Creates a slide animation on the element's `translate` property
|
|
605
|
+
* using the Web Animations API. Returns a promise that resolves when
|
|
606
|
+
* the animation finishes, or immediately if the duration is 0.
|
|
607
|
+
*
|
|
608
|
+
* @param {HTMLElement} element - The element to animate
|
|
609
|
+
* @param {boolean} slideIn - If true, slide in (off-screen → on-screen);
|
|
610
|
+
* otherwise slide out (on-screen → off-screen)
|
|
611
|
+
* @param {{ offscreen: string, duration: number, easing: string, interrupted?: { translate: string, opacity: string }, overlay?: boolean }} opts
|
|
612
|
+
* Animation parameters. `interrupted` overrides the default starting
|
|
613
|
+
* keyframe for interrupted animations (captured mid-flight before cancel).
|
|
614
|
+
* @return {Promise<void>}
|
|
615
|
+
* @private
|
|
616
|
+
*/
|
|
617
|
+
__slide(element, slideIn, { offscreen, duration, easing, interrupted, overlay }) {
|
|
618
|
+
if (!offscreen || duration <= 0) {
|
|
448
619
|
return Promise.resolve();
|
|
449
620
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
621
|
+
|
|
622
|
+
const defaultTranslate = slideIn ? offscreen : 'none';
|
|
623
|
+
const defaultOpacity = !overlay && slideIn ? 0 : 1;
|
|
624
|
+
|
|
625
|
+
const start = interrupted ? interrupted.translate : defaultTranslate;
|
|
626
|
+
const end = slideIn ? 'none' : offscreen;
|
|
627
|
+
|
|
628
|
+
const opacityStart = interrupted ? Number(interrupted.opacity) : defaultOpacity;
|
|
629
|
+
const opacityEnd = !overlay && !slideIn ? 0 : 1;
|
|
630
|
+
|
|
631
|
+
return this.__animate(
|
|
632
|
+
element,
|
|
633
|
+
[
|
|
634
|
+
{ translate: start, opacity: opacityStart },
|
|
635
|
+
{ translate: end, opacity: opacityEnd },
|
|
636
|
+
],
|
|
637
|
+
{ duration, easing },
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Runs a Web Animation on the given element, tracks it for cancellation,
|
|
643
|
+
* and returns a promise that resolves when finished (or swallows the
|
|
644
|
+
* rejection if cancelled).
|
|
645
|
+
*
|
|
646
|
+
* @param {HTMLElement} element
|
|
647
|
+
* @param {Keyframe[]} keyframes
|
|
648
|
+
* @param {KeyframeAnimationOptions} options
|
|
649
|
+
* @return {Promise<void>}
|
|
650
|
+
* @private
|
|
651
|
+
*/
|
|
652
|
+
__animate(element, keyframes, options) {
|
|
653
|
+
const animation = element.animate(keyframes, options);
|
|
654
|
+
|
|
655
|
+
this.__activeAnimations = this.__activeAnimations || [];
|
|
656
|
+
this.__activeAnimations.push(animation);
|
|
657
|
+
|
|
658
|
+
return animation.finished.catch(() => {});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Cancels in-progress animations, cleans up state, and resolves the
|
|
663
|
+
* pending transition promise.
|
|
664
|
+
* @private
|
|
665
|
+
*/
|
|
666
|
+
__endTransition() {
|
|
667
|
+
if (this.__activeAnimations) {
|
|
668
|
+
this.__activeAnimations.forEach((a) => a.cancel());
|
|
669
|
+
this.__activeAnimations = null;
|
|
670
|
+
}
|
|
453
671
|
this.removeAttribute('transition');
|
|
454
|
-
this.
|
|
455
|
-
this.
|
|
672
|
+
this.__clearOutgoing();
|
|
673
|
+
if (this.__transitionResolve) {
|
|
674
|
+
this.__transitionResolve();
|
|
675
|
+
this.__transitionResolve = null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Moves the current detail content to the outgoing slot so it can
|
|
681
|
+
* slide out while the new content slides in. Keeps the element in
|
|
682
|
+
* light DOM so light DOM styles continue to apply.
|
|
683
|
+
* @private
|
|
684
|
+
*/
|
|
685
|
+
__snapshotOutgoing() {
|
|
686
|
+
const currentDetail = this.querySelector('[slot="detail"]');
|
|
687
|
+
if (!currentDetail) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
currentDetail.setAttribute('slot', 'detail-outgoing');
|
|
691
|
+
this.__replacing = true;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Clears the outgoing container after the replace transition completes.
|
|
696
|
+
* @private
|
|
697
|
+
*/
|
|
698
|
+
__clearOutgoing() {
|
|
699
|
+
this.querySelectorAll('[slot="detail-outgoing"]').forEach((el) => el.remove());
|
|
700
|
+
this.__replacing = false;
|
|
456
701
|
}
|
|
457
702
|
|
|
458
703
|
/**
|
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": "25.2.0-
|
|
4
|
+
"version": "25.2.0-alpha2",
|
|
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.\n\n### Styling\n\nThe following shadow DOM parts are available for styling:\n\nPart name
|
|
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### Slots\n\nThe component has two main content areas: the master area (default slot)\nand the detail area (`detail` slot). When the detail doesn't fit next to\nthe master, it is shown as an overlay on top of the master area:\n\n```html\n<vaadin-master-detail-layout>\n <div>Master content</div>\n <div slot=\"detail\">Detail content</div>\n</vaadin-master-detail-layout>\n```\n\nThe component also supports a `detail-placeholder` slot for content shown\nin the detail area when no detail is selected. Unlike the `detail` slot,\nthe placeholder is simply hidden when it doesn't fit next to the master area,\nrather than shown as an overlay:\n\n```html\n<vaadin-master-detail-layout>\n <div>Master content</div>\n <div slot=\"detail-placeholder\">Select an item</div>\n</vaadin-master-detail-layout>\n```\n\n### Styling\n\nThe following shadow DOM parts are available for styling:\n\nPart name | Description\n----------------------|----------------------\n`backdrop` | Backdrop covering the master area in the overlay mode\n`master` | The master area\n`detail` | The detail area\n`detail-placeholder` | The detail placeholder area\n\nThe following state attributes are available for styling:\n\nAttribute | Description\n--------------------------|----------------------\n`expand` | Set to `master`, `detail`, or `both`.\n`orientation` | Set to `horizontal` or `vertical` depending on the orientation.\n`has-detail` | Set when the detail content is provided and visible.\n`has-detail-placeholder` | Set when the detail placeholder content is provided.\n`overlay` | Set when columns don't fit and the detail is shown as an overlay.\n`overlay-containment` | Set to `layout` or `viewport`.\n\nThe following custom CSS properties are available for styling:\n\nCustom CSS property |\n:----------------------------------------------------|\n| `--vaadin-master-detail-layout-border-color` |\n| `--vaadin-master-detail-layout-border-width` |\n| `--vaadin-master-detail-layout-detail-background` |\n| `--vaadin-master-detail-layout-detail-shadow` |\n| `--vaadin-overlay-backdrop-background` |\n\nSee [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.",
|
|
12
12
|
"attributes": [
|
|
13
13
|
{
|
|
14
14
|
"name": "detail-size",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"name": "expand",
|
|
26
|
-
"description": "Controls which column(s) expand to fill available space.\nPossible values: `'master'`, `'detail'`, `'both'`.\nDefaults to `'
|
|
26
|
+
"description": "Controls which column(s) expand to fill available space.\nPossible values: `'master'`, `'detail'`, `'both'`.\nDefaults to `'master'`.",
|
|
27
27
|
"value": {
|
|
28
28
|
"type": [
|
|
29
29
|
"string",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
"name": "expand",
|
|
117
|
-
"description": "Controls which column(s) expand to fill available space.\nPossible values: `'master'`, `'detail'`, `'both'`.\nDefaults to `'
|
|
117
|
+
"description": "Controls which column(s) expand to fill available space.\nPossible values: `'master'`, `'detail'`, `'both'`.\nDefaults to `'master'`.",
|
|
118
118
|
"value": {
|
|
119
119
|
"type": [
|
|
120
120
|
"string",
|
package/web-types.lit.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/web-types",
|
|
3
3
|
"name": "@vaadin/master-detail-layout",
|
|
4
|
-
"version": "25.2.0-
|
|
4
|
+
"version": "25.2.0-alpha2",
|
|
5
5
|
"description-markup": "markdown",
|
|
6
6
|
"framework": "lit",
|
|
7
7
|
"framework-config": {
|
|
@@ -16,7 +16,7 @@
|
|
|
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.\n\n### Styling\n\nThe following shadow DOM parts are available for styling:\n\nPart name
|
|
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### Slots\n\nThe component has two main content areas: the master area (default slot)\nand the detail area (`detail` slot). When the detail doesn't fit next to\nthe master, it is shown as an overlay on top of the master area:\n\n```html\n<vaadin-master-detail-layout>\n <div>Master content</div>\n <div slot=\"detail\">Detail content</div>\n</vaadin-master-detail-layout>\n```\n\nThe component also supports a `detail-placeholder` slot for content shown\nin the detail area when no detail is selected. Unlike the `detail` slot,\nthe placeholder is simply hidden when it doesn't fit next to the master area,\nrather than shown as an overlay:\n\n```html\n<vaadin-master-detail-layout>\n <div>Master content</div>\n <div slot=\"detail-placeholder\">Select an item</div>\n</vaadin-master-detail-layout>\n```\n\n### Styling\n\nThe following shadow DOM parts are available for styling:\n\nPart name | Description\n----------------------|----------------------\n`backdrop` | Backdrop covering the master area in the overlay mode\n`master` | The master area\n`detail` | The detail area\n`detail-placeholder` | The detail placeholder area\n\nThe following state attributes are available for styling:\n\nAttribute | Description\n--------------------------|----------------------\n`expand` | Set to `master`, `detail`, or `both`.\n`orientation` | Set to `horizontal` or `vertical` depending on the orientation.\n`has-detail` | Set when the detail content is provided and visible.\n`has-detail-placeholder` | Set when the detail placeholder content is provided.\n`overlay` | Set when columns don't fit and the detail is shown as an overlay.\n`overlay-containment` | Set to `layout` or `viewport`.\n\nThe following custom CSS properties are available for styling:\n\nCustom CSS property |\n:----------------------------------------------------|\n| `--vaadin-master-detail-layout-border-color` |\n| `--vaadin-master-detail-layout-border-width` |\n| `--vaadin-master-detail-layout-detail-background` |\n| `--vaadin-master-detail-layout-detail-shadow` |\n| `--vaadin-overlay-backdrop-background` |\n\nSee [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.",
|
|
20
20
|
"extension": true,
|
|
21
21
|
"attributes": [
|
|
22
22
|
{
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"name": ".expand",
|
|
38
|
-
"description": "Controls which column(s) expand to fill available space.\nPossible values: `'master'`, `'detail'`, `'both'`.\nDefaults to `'
|
|
38
|
+
"description": "Controls which column(s) expand to fill available space.\nPossible values: `'master'`, `'detail'`, `'both'`.\nDefaults to `'master'`.",
|
|
39
39
|
"value": {
|
|
40
40
|
"kind": "expression"
|
|
41
41
|
}
|
|
@@ -1,107 +0,0 @@
|
|
|
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
|
-
import { css } from 'lit';
|
|
7
|
-
|
|
8
|
-
export const masterDetailLayoutTransitionStyles = css`
|
|
9
|
-
@media (prefers-reduced-motion: no-preference) {
|
|
10
|
-
html {
|
|
11
|
-
--_vaadin-mdl-dir-multiplier: 1;
|
|
12
|
-
--_vaadin-mdl-easing: cubic-bezier(0.78, 0, 0.22, 1);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
html[dir='rtl'] {
|
|
16
|
-
--_vaadin-mdl-dir-multiplier: -1;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
::view-transition-group(vaadin-mdl-backdrop),
|
|
20
|
-
::view-transition-group(vaadin-mdl-master),
|
|
21
|
-
::view-transition-group(vaadin-mdl-detail) {
|
|
22
|
-
animation-duration: 0.4s;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
::view-transition-group(vaadin-mdl-master),
|
|
26
|
-
::view-transition-group(vaadin-mdl-detail) {
|
|
27
|
-
animation-timing-function: var(--_vaadin-mdl-easing);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
::view-transition-image-pair(vaadin-mdl-master),
|
|
31
|
-
::view-transition-image-pair(vaadin-mdl-detail),
|
|
32
|
-
::view-transition-new(vaadin-mdl-master),
|
|
33
|
-
::view-transition-new(vaadin-mdl-detail),
|
|
34
|
-
::view-transition-old(vaadin-mdl-master),
|
|
35
|
-
::view-transition-old(vaadin-mdl-detail) {
|
|
36
|
-
animation-timing-function: inherit;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/* Needed to promote the backdrop on top the master during the transition */
|
|
40
|
-
vaadin-master-detail-layout[transition]::part(backdrop) {
|
|
41
|
-
view-transition-name: vaadin-mdl-backdrop;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
vaadin-master-detail-layout[transition][has-detail]:not([transition='replace']):not([overflow])::part(detail),
|
|
45
|
-
vaadin-master-detail-layout[transition][has-detail][overflow]::part(detail) {
|
|
46
|
-
view-transition-name: vaadin-mdl-detail;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
::view-transition-group(vaadin-mdl-detail) {
|
|
50
|
-
clip-path: inset(0);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
::view-transition-new(vaadin-mdl-detail),
|
|
54
|
-
::view-transition-old(vaadin-mdl-detail) {
|
|
55
|
-
animation-name: vaadin-mdl-detail-slide-in;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
::view-transition-old(vaadin-mdl-detail) {
|
|
59
|
-
animation-direction: reverse;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
@keyframes vaadin-mdl-detail-slide-in {
|
|
63
|
-
0% {
|
|
64
|
-
translate: calc((100% + 30px) * var(--_vaadin-mdl-dir-multiplier));
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
vaadin-master-detail-layout[transition]::part(master) {
|
|
69
|
-
view-transition-name: vaadin-mdl-master;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
::view-transition-new(vaadin-mdl-master),
|
|
73
|
-
::view-transition-old(vaadin-mdl-master) {
|
|
74
|
-
object-fit: none;
|
|
75
|
-
object-position: 0% 0;
|
|
76
|
-
width: 100%;
|
|
77
|
-
height: 100%;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
:dir(rtl)::view-transition-new(vaadin-mdl-master),
|
|
81
|
-
:dir(rtl)::view-transition-old(vaadin-mdl-master) {
|
|
82
|
-
object-position: 100% 0;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/* prettier-ignore */
|
|
86
|
-
vaadin-master-detail-layout[orientation='vertical'][has-detail]:not([overflow])[transition]:not([transition='replace'])::part(detail),
|
|
87
|
-
vaadin-master-detail-layout[orientation='vertical'][has-detail][overflow][transition]::part(detail) {
|
|
88
|
-
view-transition-name: vaadin-mdl-detail;
|
|
89
|
-
view-transition-class: vertical;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
::view-transition-new(vaadin-mdl-detail.vertical),
|
|
93
|
-
::view-transition-old(vaadin-mdl-detail.vertical) {
|
|
94
|
-
animation-name: vaadin-mdl-vertical-detail-slide-in;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
::view-transition-old(vaadin-mdl-detail.vertical) {
|
|
98
|
-
animation-direction: reverse;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
@keyframes vaadin-mdl-vertical-detail-slide-in {
|
|
102
|
-
0% {
|
|
103
|
-
transform: translateY(calc(100% + 30px));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
`;
|