Skip to content

Commit ce8dc03

Browse files
authoredMar 25, 2025
feat: add method for starting view transitions (#8859)
* add method for triggering transitions * add tests * cleanup global css * don't start transition if details didn't change * add JSDoc * use SlotStylesMixin for injecting global styles * remove null fallback * remove unnecessary toString call * fix CSS linter errors * add missing copyright * address review comments
1 parent 77fde54 commit ce8dc03

5 files changed

+335
-2
lines changed
 

‎dev/master-detail-layout.html

+19
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848
<vaadin-checkbox id="maxHeight" label="Use max-height on the host"></vaadin-checkbox>
4949
<vaadin-checkbox id="forceOverlay" label="Force overlay"></vaadin-checkbox>
5050
<vaadin-checkbox id="stack" label="Set stack threshold"></vaadin-checkbox>
51+
<button id="set-small-detail">Set small detail</button>
52+
<button id="set-large-detail">Set large detail</button>
53+
<button id="clear-detail">Clear detail</button>
5154
</p>
5255

5356
<vaadin-master-detail-layout>
@@ -119,6 +122,22 @@
119122
document.querySelector('#stack').addEventListener('change', (e) => {
120123
layout.stackThreshold = e.target.checked ? '30rem' : null;
121124
});
125+
126+
document.querySelector('#set-small-detail').addEventListener('click', () => {
127+
const detail = document.createElement('detail-content');
128+
detail.style.width = '200px';
129+
layout.setDetail(detail);
130+
});
131+
132+
document.querySelector('#set-large-detail').addEventListener('click', () => {
133+
const detail = document.createElement('detail-content');
134+
detail.style.width = '600px';
135+
layout.setDetail(detail);
136+
});
137+
138+
document.querySelector('#clear-detail').addEventListener('click', () => {
139+
layout.setDetail(null);
140+
});
122141
</script>
123142
</body>
124143
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
`;

‎packages/master-detail-layout/src/vaadin-master-detail-layout.d.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
*/
66
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
77
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
8+
import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
89
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
910

1011
/**
1112
* `<vaadin-master-detail-layout>` is a web component for building UIs with a master
1213
* (or primary) area and a detail (or secondary) area that is displayed next to, or
1314
* overlaid on top of, the master area, depending on configuration and viewport size.
1415
*/
15-
declare class MasterDetailLayout extends ResizeMixin(ThemableMixin(ElementMixin(HTMLElement))) {
16+
declare class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ThemableMixin(ElementMixin(HTMLElement)))) {
1617
/**
1718
* Fixed size (in CSS length units) to be set on the detail area.
1819
* When specified, it prevents the detail area from growing or
@@ -85,6 +86,18 @@ declare class MasterDetailLayout extends ResizeMixin(ThemableMixin(ElementMixin(
8586
* @attr {string} stack-threshold
8687
*/
8788
stackThreshold: string | null | undefined;
89+
90+
/**
91+
* Sets the detail element to be displayed in the detail area and starts a
92+
* view transition that animates adding, replacing or removing the detail
93+
* area. During the view transition, the element is added to the DOM and
94+
* assigned to the `detail` slot. Any previous detail element is removed.
95+
* When passing null as the element, the current detail element is removed.
96+
*
97+
* If the browser does not support view transitions, the respective updates
98+
* are applied immediately without starting a transition.
99+
*/
100+
setDetail(detail: HTMLElement | null): Promise<void>;
88101
}
89102

90103
declare global {

‎packages/master-detail-layout/src/vaadin-master-detail-layout.js

+51-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import { defineCustomElement } from '@vaadin/component-base/src/define.js';
99
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
1010
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
1111
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
12+
import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
1213
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
14+
import { transitionStyles } from './vaadin-master-detail-layout-transition-styles.js';
1315

1416
/**
1517
* `<vaadin-master-detail-layout>` is a web component for building UIs with a master
@@ -21,8 +23,9 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
2123
* @mixes ThemableMixin
2224
* @mixes ElementMixin
2325
* @mixes ResizeMixin
26+
* @mixes SlotStylesMixin
2427
*/
25-
class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement)))) {
28+
class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement))))) {
2629
static get is() {
2730
return 'vaadin-master-detail-layout';
2831
}
@@ -306,6 +309,11 @@ class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitM
306309
};
307310
}
308311

312+
/** @override */
313+
get slotStyles() {
314+
return [transitionStyles];
315+
}
316+
309317
/** @protected */
310318
render() {
311319
return html`
@@ -485,6 +493,48 @@ class MasterDetailLayout extends ResizeMixin(ElementMixin(ThemableMixin(PolylitM
485493
const { backgroundPositionY } = getComputedStyle(this.$.master, '::before');
486494
return parseFloat(backgroundPositionY);
487495
}
496+
497+
/**
498+
* Sets the detail element to be displayed in the detail area and starts a
499+
* view transition that animates adding, replacing or removing the detail
500+
* area. During the view transition, the element is added to the DOM and
501+
* assigned to the `detail` slot. Any previous detail element is removed.
502+
* When passing null as the element, the current detail element is removed.
503+
*
504+
* If the browser does not support view transitions, the respective updates
505+
* are applied immediately without starting a transition.
506+
*
507+
* @param element the new detail element, or null to remove the current detail
508+
* @returns {Promise<void>}
509+
*/
510+
async setDetail(element) {
511+
// Don't start a transition if detail didn't change
512+
const currentDetail = this.querySelector('[slot="detail"]');
513+
if ((element || null) === currentDetail) {
514+
return;
515+
}
516+
517+
const updateSlot = () => {
518+
// Remove old content
519+
this.querySelectorAll('[slot="detail"]').forEach((oldElement) => oldElement.remove());
520+
// Add new content
521+
if (element) {
522+
element.setAttribute('slot', 'detail');
523+
this.appendChild(element);
524+
}
525+
};
526+
527+
if (typeof document.startViewTransition === 'function') {
528+
const hasDetail = !!currentDetail;
529+
const transitionType = hasDetail && element ? 'replace' : hasDetail ? 'remove' : 'add';
530+
this.setAttribute('transition', transitionType);
531+
const transition = document.startViewTransition(updateSlot);
532+
await transition.finished;
533+
this.removeAttribute('transition');
534+
} else {
535+
updateSlot();
536+
}
537+
}
488538
}
489539

490540
defineCustomElement(MasterDetailLayout);

0 commit comments

Comments
 (0)