Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions packages/aura/src/components/dialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,8 @@ vaadin-confirm-dialog::part(overlay) {
var(--vaadin-dialog-shadow, var(--vaadin-overlay-shadow, var(--aura-overlay-shadow)));
--aura-surface-level: 2;
--aura-surface-opacity: var(--aura-overlay-surface-opacity);

/* TODO probably should be in base styles */
/* Keeps dialogs on top of MDL view transitions */
view-transition-name: vaadin-dialog;
}

vaadin-confirm-dialog::part(message) {
color: var(--vaadin-text-color-secondary);
}

/* TODO probably should be in base styles */
::view-transition-group(vaadin-dialog) {
border-radius: var(--vaadin-dialog-border-radius, var(--vaadin-radius-l));
z-index: 1;
}
8 changes: 0 additions & 8 deletions packages/aura/src/components/master-detail-layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,3 @@ vaadin-master-detail-layout:not([overflow])::part(detail) {
border-start-end-radius: var(--_app-layout-radius);
border-end-end-radius: var(--_app-layout-radius);
}

/* TODO these end up affecting all MDLs, not just the one directly inside the App Layout */
::view-transition-group(vaadin-mdl-backdrop),
::view-transition-group(vaadin-mdl-master),
::view-transition-group(vaadin-mdl-detail) {
border-radius: var(--_app-layout-radius);
overflow: hidden;
}
19 changes: 0 additions & 19 deletions packages/aura/src/components/notification.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ vaadin-notification-card::part(overlay) {
--aura-surface-level: 3.5;
background: var(--vaadin-notification-background, var(--aura-surface-color));
box-shadow: var(--aura-overlay-outline-shadow), var(--vaadin-notification-shadow, var(--aura-overlay-shadow));

/* TODO probably should be in base styles */
/* Keeps notifications on top of MDL view transitions */
view-transition-name: vaadin-notification;
}

vaadin-notification-card:is(
Expand Down Expand Up @@ -44,21 +40,6 @@ vaadin-notification-card:is(
var(--vaadin-notification-shadow, var(--aura-shadow-m));
}

::view-transition-group(vaadin-notification) {
/* Keep on top of MDL view-transition elements */
z-index: 1;
/* The backdrop-filter from vaadin-notification-card::part(overlay) is copied here, so we need to clip it with the same border radius */
border-radius: var(--vaadin-notification-border-radius, var(--vaadin-radius-l));
}

/* In Safari, the backdrop-filter is copied to transition-group pseudo element but also retained in the new/old pseudo elements */
/* Removing it from the transition-group makes it look better */
@supports (background: -webkit-named-image(i)) {
::view-transition-group(vaadin-notification) {
backdrop-filter: none;
}
}

vaadin-notification-card vaadin-card {
--vaadin-card-border-width: 0px;
--vaadin-card-gap: var(--vaadin-gap-xs) var(--vaadin-gap-s);
Expand Down
52 changes: 41 additions & 11 deletions packages/master-detail-layout/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,6 @@ Layout detection is split into two methods to avoid forced reflows:
- ResizeObserver callback: calls `__computeLayoutState()` (read), cancels any pending rAF via `cancelAnimationFrame`, then defers `__applyLayoutState()` (write) via `requestAnimationFrame`. Cancelling ensures the write phase always uses the latest state when multiple callbacks fire per frame.
- **Property observers** (`masterSize`/`detailSize`) only update CSS custom properties — ResizeObserver picks up the resulting size changes automatically

### View transitions

`_finishTransition()` uses `queueMicrotask` to call both `__computeLayoutState()` + `__applyLayoutState()` synchronously. The microtask runs before the Promise resolution propagates to `startViewTransition`, ensuring the "new" snapshot captures the correct overlay state (backdrop, absolute positioning). The `getComputedStyle` read in the microtask does cause a forced reflow, but this is unavoidable for correct transition snapshots.

## Overlay Modes

When `overflow` AND `has-detail` are both set, the detail becomes an overlay:
Expand Down Expand Up @@ -100,15 +96,49 @@ When no detail is present, master's extra track is set to `calc(100% - masterSiz

Set when detail first appears with overflow, cleared when detail is removed or overflow resolves.

## View Transitions
## Detail Animations

Detail panel transitions use the Web Animations API (`element.animate()`) on `translate` and `opacity`. This works inside shadow roots (unlike the View Transitions API).

### CSS custom properties

Animation parameters are driven by CSS custom properties, read once per transition to avoid repeated layout reads:

- `--_detail-offscreen` — off-screen translate value. Defaults to `30px` (subtle slide in split mode), overridden to `calc((100% + 30px))` in overlay mode (full panel slide). Vertical orientation uses the Y axis.
- `--_transition-duration` — defaults to `0s`, enabled via `@media (prefers-reduced-motion: no-preference)`: 200ms for split mode, 300ms for overlay mode. Replace transitions in split mode use 0ms (no slide, just instant swap).
- `--_transition-easing` — cubic-bezier easing

CSS handles resting states: `translate: var(--_detail-offscreen)` on `#detail` by default, overridden to `translate: none` by `:host([has-detail])`. RTL is supported via `--_rtl-multiplier`.

### Transition types

- **Add**: DOM is updated first (new element inserted, `has-detail` set), then the detail slides in from off-screen. In split mode, also fades from opacity 0 → 1.
- **Remove**: the detail slides out to off-screen first (in split mode, also fades to opacity 0), then the DOM is updated (element removed, `has-detail` cleared) on `animation.finished`
- **Replace** (overlay): old content is reassigned to `slot="detail-outgoing"` (stays in light DOM so styles continue to apply), then old slides out while new slides in simultaneously
- **Replace** (split): old content moves to outgoing slot. The outgoing slides out with fade on top (`z-index: 1`), revealing the incoming at full opacity underneath.

The `noAnimation` property (reflected as `no-animation` attribute) skips all animations. Animations are also disabled when `--_transition-duration` resolves to `0s`.

### Transition flow

1. **Capture interrupted state** — read the detail panel's current `translate` and `opacity` via `getComputedStyle()` _before_ cancelling any in-progress animation (see "Smooth interruption" below)
2. **Cancel previous** — cancel in-progress animations, clean up state, resolve the pending promise
3. **Snapshot outgoing** — reassign old content to the outgoing slot (replace only)
4. **DOM update** — run the update callback, apply layout state (add/replace only; remove defers this to step 6)
5. **Animate** — create Web Animations on `translate` and `opacity`
6. **Finish** — on `animation.finished`, clean up the `transition` attribute and resolve the promise. For remove, the deferred DOM update runs here

A version counter guards step 6: if a newer transition has started since step 5, the stale finish callback is ignored.

### Smooth interruption

`animation.cancel()` removes the animation effect and the element reverts to its CSS resting state — causing a visual jump. To avoid this, the current `translate` and `opacity` values are read via `getComputedStyle()` _before_ cancelling. These captured mid-flight values become the starting keyframe of the new animation, so the panel changes direction and opacity smoothly from where it actually is.

For `replace` interruptions, the captured state is applied to the outgoing element (since the interrupted content moves from the detail slot to the outgoing slot).

Uses the CSS View Transitions API (`document.startViewTransition`):
### Outgoing container

- `_setDetail(element, skipTransition)` — adds/replaces/removes detail with animation
- `_startTransition(transitionType, updateCallback)` — starts a named transition
- `_finishTransition()` — calls `__computeLayoutState()` + `__applyLayoutState()` via `queueMicrotask` (see read/write separation above)
- `noAnimation` property disables transitions
- Styles injected via `SlotStylesMixin`
The `#outgoing` shadow DOM element with `<slot name="detail-outgoing">` enables replace animations. Old content is moved to this slot (light DOM reassignment preserves user styles), animated out, then removed on completion. The outgoing has `z-index: 1` to paint on top of the incoming during the transition.

## Test Patterns

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export const masterDetailLayoutStyles = css`
--_detail-size: 15em;
--_master-column: var(--_master-size) 0;
--_detail-column: var(--_detail-size) 0;
--_transition-duration: 0s;
--_transition-easing: cubic-bezier(0.78, 0, 0.22, 1);
--_rtl-multiplier: 1;
--_detail-offscreen: calc(30px * var(--_rtl-multiplier));

display: grid;
box-sizing: border-box;
Expand All @@ -27,21 +31,26 @@ export const masterDetailLayoutStyles = css`
display: none !important;
}

:host([dir='rtl']) {
--_rtl-multiplier: -1;
}

:host([orientation='vertical']) {
--_detail-offscreen: 0 30px;

grid-template-columns: 100%;
grid-template-rows: [master-start] var(--_master-column) [detail-start] var(--_detail-column) [detail-end];
}

#master,
#detail {
:is(#master, #detail, #outgoing) {
box-sizing: border-box;
}

#master {
grid-column: master-start / detail-start;
}

#detail {
:is(#detail, #outgoing) {
grid-column: detail-start / detail-end;
}

Expand All @@ -50,7 +59,7 @@ export const masterDetailLayoutStyles = css`
grid-row: master-start / detail-start;
}

:host([orientation='vertical']) #detail {
:host([orientation='vertical']) :is(#detail, #outgoing) {
grid-column: auto;
grid-row: detail-start / detail-end;
}
Expand Down Expand Up @@ -89,7 +98,36 @@ export const masterDetailLayoutStyles = css`
var(--vaadin-master-detail-layout-border-color, var(--vaadin-border-color-secondary));
}

:host([overflow]) #detail {
/* Detail transition: off-screen by default, on-screen when has-detail */
#detail {
translate: var(--_detail-offscreen);
}

:host([has-detail]) #detail {
translate: none;
}

:host(:not([orientation='vertical'])[transition='replace']) :is(#detail, #outgoing) {
grid-row: 1 / -1;
}

:host([orientation='vertical'][transition='replace']) :is(#detail, #outgoing) {
grid-column: 1 / -1;
}

#outgoing:not([hidden]) {
z-index: 1;
}

:host([overflow]) {
--_detail-offscreen: calc((100% + 30px) * var(--_rtl-multiplier));
}

:host([overflow][orientation='vertical']) {
--_detail-offscreen: 0 calc(100% + 30px);
}

:host([overflow]) :is(#detail, #outgoing) {
position: absolute;
z-index: 2;
background: var(--vaadin-master-detail-layout-detail-background, var(--vaadin-background-color));
Expand All @@ -101,32 +139,43 @@ export const masterDetailLayoutStyles = css`
display: block;
}

:host([overflow]:not([orientation='vertical'])) #detail {
:host([overflow]:not([orientation='vertical'])) :is(#detail, #outgoing) {
inset-block: 0;
width: var(--_overlay-size, var(--_detail-size, min-content));
inset-inline-end: 0;
}

:host([overflow][orientation='vertical']) #detail {
:host([overflow][orientation='vertical']) :is(#detail, #outgoing) {
grid-column: auto;
grid-row: none;
inset-inline: 0;
height: var(--_overlay-size, var(--_detail-size, min-content));
inset-block-end: 0;
}

:host([overflow][overlay-containment='viewport']) #detail,
:host([overflow][overlay-containment='viewport']) :is(#detail, #outgoing),
:host([overflow][overlay-containment='viewport']) [part~='backdrop'] {
position: fixed;
}

@media (forced-colors: active) {
:host([overflow]) #detail {
:host([overflow]) :is(#detail, #outgoing) {
outline: 3px solid !important;
}

#detail {
:is(#detail, #outgoing) {
background: Canvas !important;
}
}

/* Enable transitions when motion is allowed */
@media (prefers-reduced-motion: no-preference) {
:host(:not([no-animation], [transition='replace'])) {
--_transition-duration: 200ms;
}

:host([overflow]:not([no-animation])) {
--_transition-duration: 300ms;
}
}
`;

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';

export interface MasterDetailLayoutCustomEventMap {
Expand Down Expand Up @@ -55,7 +54,7 @@ export interface MasterDetailLayoutEventMap extends HTMLElementEventMap, MasterD
* @fires {CustomEvent} backdrop-click - Fired when the user clicks the backdrop in the overlay mode.
* @fires {CustomEvent} detail-escape-press - Fired when the user presses Escape in the detail area.
*/
declare class MasterDetailLayout extends SlotStylesMixin(ThemableMixin(ElementMixin(HTMLElement))) {
declare class MasterDetailLayout extends ThemableMixin(ElementMixin(HTMLElement)) {
/**
* Size (in CSS length units) to be set on the detail area in
* the CSS grid layout. If there is not enough space to show
Expand Down
Loading
Loading