+
+
+
+
+
+
+
+
+
+ View ${window.mdlCount}
-
-
-
-
-
-
- Open Static Detail
- Open Nested Test View
- View ${window.mdlCount}
-
-
-
-
-
+
+ Open Static Detail
+ Open Nested Test View
${lorem}
@@ -261,7 +254,14 @@
View ${window.mdlCount}
}
_configChange(e) {
- this._mdl[e.currentTarget.className] = e.target.value.match(/auto|none/) ? null : e.target.value;
+ const prop = e.currentTarget.className;
+ const value = e.target.value;
+ // Size properties use null for "auto", other properties always have a value
+ if (prop === 'masterSize' || prop === 'detailSize' || prop === 'overlaySize') {
+ this._mdl[prop] = value === 'auto' ? null : value;
+ } else {
+ this._mdl[prop] = value;
+ }
}
}
diff --git a/packages/aura/src/components/master-detail-layout.css b/packages/aura/src/components/master-detail-layout.css
index 479a6343011..add17507125 100644
--- a/packages/aura/src/components/master-detail-layout.css
+++ b/packages/aura/src/components/master-detail-layout.css
@@ -3,7 +3,7 @@ vaadin-master-detail-layout::part(detail) {
background: var(--aura-surface-color) padding-box;
}
-vaadin-master-detail-layout[drawer]::part(detail) {
+vaadin-master-detail-layout[overflow]::part(detail) {
--aura-surface-opacity: var(--aura-overlay-surface-opacity);
background: var(--aura-surface-color) padding-box;
-webkit-backdrop-filter: var(--aura-overlay-backdrop-filter);
@@ -14,19 +14,19 @@ vaadin-master-detail-layout[drawer]::part(detail) {
var(--aura-shadow-m);
}
-vaadin-master-detail-layout[containment='viewport'][drawer]::part(detail) {
+vaadin-master-detail-layout[overflow][overlay-containment='viewport']::part(detail) {
box-shadow: var(--aura-overlay-shadow);
}
/* TODO could be a built-in variant */
-vaadin-master-detail-layout[theme~='inset-drawer'][drawer]::part(detail),
-vaadin-master-detail-layout[containment='viewport'][drawer]::part(detail) {
+vaadin-master-detail-layout[theme~='inset-drawer'][overflow]::part(detail),
+vaadin-master-detail-layout[overflow][overlay-containment='viewport']::part(detail) {
margin: calc(var(--aura-app-layout-inset) / 2);
border-radius: var(--_app-layout-radius);
}
vaadin-master-detail-layout > vaadin-master-detail-layout,
-vaadin-master-detail-layout:not([drawer])::part(detail) {
+vaadin-master-detail-layout:not([overflow])::part(detail) {
border-start-end-radius: var(--_app-layout-radius);
border-end-end-radius: var(--_app-layout-radius);
}
diff --git a/packages/master-detail-layout/ARCHITECTURE.md b/packages/master-detail-layout/ARCHITECTURE.md
new file mode 100644
index 00000000000..5e93c6ea226
--- /dev/null
+++ b/packages/master-detail-layout/ARCHITECTURE.md
@@ -0,0 +1,121 @@
+# Master-Detail Layout — CSS Grid Architecture
+
+## 4-Column Grid System
+
+The grid uses **4 column tracks** with named lines. Each logical column (master, detail) has a **size track** + an **extra track**:
+
+```
+[master-start] [detail-start] [detail-end]
+```
+
+CSS custom properties:
+
+- `--_master-column: var(--_master-size) 0` — default: fixed size + 0 extra
+- `--_detail-column: var(--_detail-size) 0` — default: fixed size + 0 extra
+- `--_master-size` / `--_detail-size` — default to `30em` / `15em` in `:host`; overridden from JS when `masterSize`/`detailSize` properties are set
+
+Parts use **named grid lines** for placement:
+
+- Master spans `master-start / detail-start` (size + extra)
+- Detail spans `detail-start / detail-end` (size + extra)
+
+### Expand modes
+
+The `expand` attribute controls which extra track(s) become `1fr`:
+
+| `expand` | `--_master-column` | `--_detail-column` |
+| -------- | ------------------ | ------------------ |
+| (none) | `size 0` | `size 0` |
+| `both` | `size 1fr` | `size 1fr` |
+| `master` | `size 1fr` | `size 0` |
+| `detail` | `size 0` | `size 1fr` |
+
+### Vertical orientation
+
+In vertical mode, `grid-template-rows` replaces `grid-template-columns` using the same named lines and variables. Parts switch from `grid-column` to `grid-row` placement.
+
+### Default sizes
+
+`--_master-size` and `--_detail-size` default to `30em` and `15em` respectively in `:host`. When `masterSize`/`detailSize` properties are set, JS overrides these CSS custom properties. When cleared, JS removes the inline style and the defaults apply again.
+
+## Overflow Detection
+
+`__checkOverflow()` reads the first 3 of the 4 computed track sizes: `[masterSize, masterExtra, detailSize]`. The 4th (detail extra) is 0 in overflow scenarios.
+
+**No overflow** when either:
+
+- `Math.floor(masterSize + masterExtra + detailSize) <= Math.floor(hostSize)` (tracks fit; flooring prevents false overflow from sub-pixel track sizes)
+- `masterExtra >= detailSize` (master's extra space can absorb the detail)
+
+The `>=` (not `>`) is intentional: when `preserve-master-width` or `:not([has-detail])` is active, CSS `calc(100% - masterSize)` inflates the master extra track. With this inflation, `masterExtra >= detailSize` is equivalent to `hostSize >= masterSize + detailSize` — the correct no-overflow check. Strict `>` would miss the boundary case where they're equal.
+
+### Read/write separation
+
+Layout detection is split into two methods to avoid forced reflows:
+
+- **`__computeLayoutState()`** — pure reads: `checkVisibility()`, `getComputedStyle()`, `getFocusableElements()`. Called in the ResizeObserver callback where layout is already computed — no forced reflow.
+- **`__applyLayoutState(state)`** — pure writes: toggles `has-detail`, `overflow`, `preserve-master-width`; calls `requestUpdate()` for ARIA; focuses detail. No DOM/style reads.
+
+### ResizeObserver
+
+- **Observes**: host + shadow DOM parts (`master`, `detail`) + direct slotted children (`:scope >` prevents observing nested descendants)
+- 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:
+
+- `position: absolute; grid-column: none` removes detail from grid flow
+- Backdrop becomes visible
+- `overlaySize` (CSS custom property `--_overlay-size`) controls overlay dimensions; falls back to `--_detail-size`
+- `overlayContainment` (`layout`/`viewport`) controls positioning: `absolute` vs `fixed`
+- ARIA: `role="dialog"` on detail, `inert` on master (layout containment), `aria-modal` (viewport containment)
+
+### Overlay positioning
+
+| Orientation | Default | `overlayContainment='viewport'` |
+| ----------- | ---------------------------------------------------- | ------------------------------- |
+| Horizontal | `width: overlaySize/detailSize; inset-inline-end: 0` | `position: fixed` |
+| Vertical | `height: overlaySize/detailSize; inset-block-end: 0` | `position: fixed` |
+
+Setting `overlaySize` to `100%` makes the detail cover the full layout (replaces old "full" mode).
+
+## preserve-master-width
+
+Prevents the master from jumping when the detail overlay first appears.
+
+**Problem**: When detail appears with overflow, the detail becomes absolute (out of grid flow). The master's `1fr` extra track expands since the detail's `1fr` is gone, causing a visual jump.
+
+**Solution**: Replace `1fr` with `calc(100% - masterSize)` to keep the master at full host width. Same rule applies when `has-detail` is not set. For `expand='detail'`, the same compensation is applied when no detail is present to prevent the detail tracks from reserving space.
+
+```css
+:host([expand='both']:is(:not([has-detail]), [preserve-master-width])),
+:host([expand='master']:is(:not([has-detail]), [preserve-master-width])),
+:host([expand='detail']:not([has-detail])) {
+ --_master-column: var(--_master-size) calc(100% - var(--_master-size));
+}
+```
+
+Set when detail first appears with overflow, cleared when detail is removed or overflow resolves.
+
+## View Transitions
+
+Uses the CSS View Transitions API (`document.startViewTransition`):
+
+- `_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`
+
+## Test Patterns
+
+- **`onceResized(layout)`** (`test/helpers.js`): `nextResize()` + `nextRender()` — waits for ResizeObserver + rAF write phase in `__onResize()`
+- **Split mode sizing**: measure part elements directly (not `gridTemplateColumns` which has 4 columns)
+- **Vertical tests**: integrated into each test file under `describe('vertical')` suites
+- **Feature flag**: `window.Vaadin.featureFlags.masterDetailLayoutComponent = true` required before import
diff --git a/packages/master-detail-layout/src/styles/vaadin-master-detail-layout-base-styles.js b/packages/master-detail-layout/src/styles/vaadin-master-detail-layout-base-styles.js
index 84f0b04af63..3c1ed30c3a6 100644
--- a/packages/master-detail-layout/src/styles/vaadin-master-detail-layout-base-styles.js
+++ b/packages/master-detail-layout/src/styles/vaadin-master-detail-layout-base-styles.js
@@ -7,17 +7,20 @@ import '@vaadin/component-base/src/styles/style-props.js';
import { css } from 'lit';
export const masterDetailLayoutStyles = css`
- /* Layout and positioning styles */
-
:host {
- display: flex;
+ --_master-size: 30em;
+ --_detail-size: 15em;
+ --_master-column: var(--_master-size) 0;
+ --_detail-column: var(--_detail-size) 0;
+
+ display: grid;
box-sizing: border-box;
height: 100%;
- max-width: 100%;
- max-height: 100%;
- position: relative; /* Keep the positioning context stable across all modes */
- z-index: 0; /* Create a new stacking context, don't let "layout contained" detail element stack outside it */
+ position: relative;
+ z-index: 0;
overflow: hidden;
+ grid-template-columns: [master-start] var(--_master-column) [detail-start] var(--_detail-column) [detail-end];
+ grid-template-rows: 100%;
}
:host([hidden]) {
@@ -25,171 +28,105 @@ export const masterDetailLayoutStyles = css`
}
:host([orientation='vertical']) {
- flex-direction: column;
- }
-
- [part='_detail-internal'] {
- display: contents;
- justify-content: end;
- /* Disable pointer events for the detail wrapper to allow clicks to pass through to the backdrop */
- pointer-events: none;
- }
-
- [part='detail'] {
- /* Re-enable pointer events for the actual detail content */
- pointer-events: auto;
- }
-
- :host([orientation='vertical']) [part='_detail-internal'] {
- align-items: end;
- }
-
- :host(:is([drawer], [stack])) [part='_detail-internal'],
- :host(:is([drawer], [stack])[has-detail]) [part='backdrop'] {
- display: flex;
- position: absolute;
- z-index: 1;
- inset: 0;
- overscroll-behavior: contain;
- }
-
- :host(:not([has-detail])) [part='_detail-internal'],
- [part='backdrop'] {
- display: none;
- }
-
- :host([orientation='horizontal'][drawer]) [part='detail'] {
- margin-inline-start: 50px;
- }
-
- :host([orientation='vertical'][drawer]) [part='detail'] {
- margin-top: 50px;
- }
-
- :host(:is([drawer], [stack])[containment='viewport']) :is([part='_detail-internal'], [part='backdrop']) {
- position: fixed;
+ grid-template-columns: 100%;
+ grid-template-rows: [master-start] var(--_master-column) [detail-start] var(--_detail-column) [detail-end];
}
- :host(:is([drawer], [stack])[containment='viewport']) [part='detail'] {
- padding-top: var(--safe-area-inset-top);
- padding-bottom: var(--safe-area-inset-bottom);
- }
-
- :host([containment='viewport']:dir(ltr)) [part='detail'] {
- padding-right: var(--safe-area-inset-right);
- }
-
- :host([containment='viewport']:dir(rtl)) [part='detail'] {
- padding-left: var(--safe-area-inset-left);
- }
-
- :host([stack][containment='viewport']) [part='detail'] {
- padding-left: var(--safe-area-inset-left);
- padding-right: var(--safe-area-inset-right);
- }
-
- /* Sizing styles */
-
- [part] {
+ [part~='master'],
+ [part~='detail'] {
box-sizing: border-box;
- max-width: 100%;
- max-height: 100%;
- }
-
- /* No fixed size */
- :host(:not([has-master-size])) [part='master'],
- :host(:not([has-detail-size]):not([drawer], [stack])) [part='detail'] {
- flex-grow: 1;
- flex-basis: 50%;
}
- /* Fixed size */
- :host([has-master-size]) [part='master'],
- :host([has-detail-size]) [part='detail'] {
- flex-shrink: 0;
+ [part~='master'] {
+ grid-column: master-start / detail-start;
}
- :host([orientation='horizontal'][has-master-size][has-detail]) [part='master'] {
- width: var(--_master-size);
+ [part~='detail'] {
+ grid-column: detail-start / detail-end;
}
- :host([orientation='vertical'][has-master-size][has-detail]) [part='master'] {
- height: var(--_master-size);
+ :host([orientation='vertical']) [part~='master'] {
+ grid-column: auto;
+ grid-row: master-start / detail-start;
}
- :host([orientation='horizontal'][has-detail-size]:not([stack])) [part='detail'] {
- width: var(--_detail-size);
+ :host([orientation='vertical']) [part~='detail'] {
+ grid-column: auto;
+ grid-row: detail-start / detail-end;
}
- :host([orientation='vertical'][has-detail-size]:not([stack])) [part='detail'] {
- height: var(--_detail-size);
- }
-
- :host([has-master-size][has-detail-size]) [part='master'] {
- flex-grow: 1;
- flex-basis: var(--_master-size);
+ [part~='backdrop'] {
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+ display: none;
+ background: var(--vaadin-overlay-backdrop-background, rgba(0, 0, 0, 0.2));
+ forced-color-adjust: none;
}
- :host([has-master-size][has-detail-size]:not([drawer], [stack])) [part='detail'] {
- flex-grow: 1;
- flex-basis: var(--_detail-size);
+ :host([expand='both']),
+ :host([expand='master']) {
+ --_master-column: var(--_master-size) 1fr;
}
- /* Min size */
- :host([orientation='horizontal'][has-master-min-size]) [part='master'] {
- min-width: min(100%, var(--_master-min-size));
+ :host([expand='both']:is(:not([has-detail]), [preserve-master-width])),
+ :host([expand='master']:is(:not([has-detail]), [preserve-master-width])),
+ :host([expand='detail']:not([has-detail])) {
+ --_master-column: var(--_master-size) calc(100% - var(--_master-size));
}
- :host([orientation='vertical'][has-master-min-size]) [part='master'] {
- min-height: min(100%, var(--_master-min-size));
+ :host([expand='both']),
+ :host([expand='detail']) {
+ --_detail-column: var(--_detail-size) 1fr;
}
- :host([orientation='horizontal'][has-detail-min-size]) [part='detail'] {
- min-width: min(100%, var(--_detail-min-size));
+ :host([orientation='horizontal'][has-detail]:not([overflow])) [part~='detail'] {
+ border-inline-start: var(--vaadin-master-detail-layout-border-width, 1px) solid
+ var(--vaadin-master-detail-layout-border-color, var(--vaadin-border-color-secondary));
}
- :host([orientation='vertical'][has-detail-min-size]) [part='detail'] {
- min-height: min(100%, var(--_detail-min-size));
+ :host([orientation='vertical'][has-detail]:not([overflow])) [part~='detail'] {
+ border-top: var(--vaadin-master-detail-layout-border-width, 1px) solid
+ var(--vaadin-master-detail-layout-border-color, var(--vaadin-border-color-secondary));
}
- :host([drawer]) [part='master'],
- :host([stack]) [part] {
- width: 100% !important;
- height: 100% !important;
- min-width: auto !important;
- min-height: auto !important;
- max-width: 100% !important;
- max-height: 100% !important;
+ :host([overflow]) [part~='detail'] {
+ position: absolute;
+ z-index: 2;
+ background: var(--vaadin-master-detail-layout-detail-background, var(--vaadin-background-color));
+ box-shadow: var(--vaadin-master-detail-layout-detail-shadow, 0 0 20px 0 rgba(0, 0, 0, 0.3));
+ grid-column: none;
}
- /* Decorative/visual styles */
-
- [part='backdrop'] {
- background: var(--vaadin-overlay-backdrop-background, rgba(0, 0, 0, 0.2));
- forced-color-adjust: none;
+ :host([overflow]) [part~='backdrop'] {
+ display: block;
}
- :host(:is([drawer], [stack])) [part='detail'] {
- background: var(--vaadin-master-detail-layout-detail-background, var(--vaadin-background-color));
- box-shadow: var(--vaadin-master-detail-layout-detail-shadow, 0 0 20px 0 rgba(0, 0, 0, 0.3));
+ :host([overflow]:not([orientation='vertical'])) [part~='detail'] {
+ inset-block: 0;
+ width: var(--_overlay-size, var(--_detail-size, min-content));
+ inset-inline-end: 0;
}
- :host([orientation='horizontal']:not([drawer], [stack])) [part='detail'] {
- border-inline-start: var(--vaadin-master-detail-layout-border-width, 1px) solid
- var(--vaadin-master-detail-layout-border-color, var(--vaadin-border-color-secondary));
+ :host([overflow][orientation='vertical']) [part~='detail'] {
+ grid-column: auto;
+ grid-row: none;
+ inset-inline: 0;
+ height: var(--_overlay-size, var(--_detail-size, min-content));
+ inset-block-end: 0;
}
- :host([orientation='vertical']:not([drawer], [stack])) [part='detail'] {
- border-top: var(--vaadin-master-detail-layout-border-width, 1px) solid
- var(--vaadin-master-detail-layout-border-color, var(--vaadin-border-color-secondary));
+ :host([overflow][overlay-containment='viewport']) [part~='detail'],
+ :host([overflow][overlay-containment='viewport']) [part~='backdrop'] {
+ position: fixed;
}
@media (forced-colors: active) {
- :host(:is([drawer], [stack])) [part='detail'] {
+ :host([overflow]) [part~='detail'] {
outline: 3px solid !important;
}
- [part='detail'] {
+ [part~='detail'] {
background: Canvas !important;
}
}
diff --git a/packages/master-detail-layout/src/styles/vaadin-master-detail-layout-transition-base-styles.js b/packages/master-detail-layout/src/styles/vaadin-master-detail-layout-transition-base-styles.js
index 61c4f2fcbb7..9d911c74067 100644
--- a/packages/master-detail-layout/src/styles/vaadin-master-detail-layout-transition-base-styles.js
+++ b/packages/master-detail-layout/src/styles/vaadin-master-detail-layout-transition-base-styles.js
@@ -9,14 +9,11 @@ export const masterDetailLayoutTransitionStyles = css`
@media (prefers-reduced-motion: no-preference) {
html {
--_vaadin-mdl-dir-multiplier: 1;
- --_vaadin-mdl-stack-master-offset: 20%;
- --_vaadin-mdl-stack-master-clip-path: inset(0 0 0 var(--_vaadin-mdl-stack-master-offset));
--_vaadin-mdl-easing: cubic-bezier(0.78, 0, 0.22, 1);
}
html[dir='rtl'] {
--_vaadin-mdl-dir-multiplier: -1;
- --_vaadin-mdl-stack-master-clip-path: inset(0 var(--_vaadin-mdl-stack-master-offset) 0 0);
}
::view-transition-group(vaadin-mdl-backdrop),
@@ -44,8 +41,8 @@ export const masterDetailLayoutTransitionStyles = css`
view-transition-name: vaadin-mdl-backdrop;
}
- vaadin-master-detail-layout[transition]:not([transition='replace']):not([drawer], [stack])::part(detail),
- vaadin-master-detail-layout[transition]:is([drawer], [stack])::part(_detail-internal) {
+ vaadin-master-detail-layout[transition][has-detail]:not([transition='replace']):not([overflow])::part(detail),
+ vaadin-master-detail-layout[transition][has-detail][overflow]::part(detail) {
view-transition-name: vaadin-mdl-detail;
}
@@ -68,23 +65,10 @@ export const masterDetailLayoutTransitionStyles = css`
}
}
- vaadin-master-detail-layout[orientation='horizontal'][stack][has-detail]::part(master) {
- translate: calc(var(--_vaadin-mdl-stack-master-offset) * var(--_vaadin-mdl-dir-multiplier) * -1);
- opacity: 0;
- }
-
vaadin-master-detail-layout[transition]::part(master) {
view-transition-name: vaadin-mdl-master;
}
- vaadin-master-detail-layout[orientation='horizontal'][stack][transition='add']::part(master) {
- view-transition-class: stack-add;
- }
-
- vaadin-master-detail-layout[orientation='horizontal'][stack][transition='remove']::part(master) {
- view-transition-class: stack-remove;
- }
-
::view-transition-new(vaadin-mdl-master),
::view-transition-old(vaadin-mdl-master) {
object-fit: none;
@@ -98,33 +82,9 @@ export const masterDetailLayoutTransitionStyles = css`
object-position: 100% 0;
}
- ::view-transition-new(vaadin-mdl-master.stack-remove),
- ::view-transition-old(vaadin-mdl-master.stack-remove) {
- animation-name: vaadin-mdl-master-stack-remove;
- clip-path: var(--_vaadin-mdl-stack-master-clip-path);
- }
-
- @keyframes vaadin-mdl-master-stack-remove {
- 100% {
- clip-path: inset(0);
- }
- }
-
- ::view-transition-new(vaadin-mdl-master.stack-add),
- ::view-transition-old(vaadin-mdl-master.stack-add) {
- animation-name: vaadin-mdl-master-stack-add;
- clip-path: inset(0);
- }
-
- @keyframes vaadin-mdl-master-stack-add {
- 100% {
- clip-path: var(--_vaadin-mdl-stack-master-clip-path);
- }
- }
-
/* prettier-ignore */
- vaadin-master-detail-layout[orientation='vertical']:not([drawer], [stack])[transition]:not([transition='replace'])::part(detail),
- vaadin-master-detail-layout[orientation='vertical']:is([drawer], [stack])[transition]::part(_detail-internal) {
+ vaadin-master-detail-layout[orientation='vertical'][has-detail]:not([overflow])[transition]:not([transition='replace'])::part(detail),
+ vaadin-master-detail-layout[orientation='vertical'][has-detail][overflow][transition]::part(detail) {
view-transition-name: vaadin-mdl-detail;
view-transition-class: vertical;
}
diff --git a/packages/master-detail-layout/src/vaadin-master-detail-layout.d.ts b/packages/master-detail-layout/src/vaadin-master-detail-layout.d.ts
index 5acf2aec541..c3e9d8decb2 100644
--- a/packages/master-detail-layout/src/vaadin-master-detail-layout.d.ts
+++ b/packages/master-detail-layout/src/vaadin-master-detail-layout.d.ts
@@ -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 { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
@@ -27,19 +26,19 @@ export interface MasterDetailLayoutEventMap extends HTMLElementEventMap, MasterD
*
* Part name | Description
* ---------------|----------------------
- * `backdrop` | Backdrop covering the master area in the drawer mode
+ * `backdrop` | Backdrop covering the master area in the overlay mode
* `master` | The master area
* `detail` | The detail area
*
* The following state attributes are available for styling:
*
- * Attribute | Description
- * ---------------| -----------
- * `containment` | Set to `layout` or `viewport` depending on the containment.
- * `orientation` | Set to `horizontal` or `vertical` depending on the orientation.
- * `has-detail` | Set when the detail content is provided.
- * `drawer` | Set when the layout is using the drawer mode.
- * `stack` | Set when the layout is using the stack mode.
+ * Attribute | Description
+ * ----------------------|----------------------
+ * `expand` | Set to `master`, `detail`, or `both`.
+ * `orientation` | Set to `horizontal` or `vertical` depending on the orientation.
+ * `has-detail` | Set when the detail content is provided and visible.
+ * `overflow` | Set when columns don't fit and the detail is shown as an overlay.
+ * `overlay-containment` | Set to `layout` or `viewport`.
*
* The following custom CSS properties are available for styling:
*
@@ -53,53 +52,38 @@ export interface MasterDetailLayoutEventMap extends HTMLElementEventMap, MasterD
*
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
*
- * @fires {CustomEvent} backdrop-click - Fired when the user clicks the backdrop in the drawer mode.
+ * @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(ResizeMixin(ThemableMixin(ElementMixin(HTMLElement)))) {
+declare class MasterDetailLayout extends SlotStylesMixin(ThemableMixin(ElementMixin(HTMLElement))) {
/**
- * Fixed size (in CSS length units) to be set on the detail area.
- * When specified, it prevents the detail area from growing or
- * shrinking. If there is not enough space to show master and detail
- * areas next to each other, the details are shown as an overlay:
- * either as drawer or stack, depending on the `stackOverlay` property.
+ * 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
+ * master and detail areas next to each other, the detail area
+ * is shown as an overlay. Defaults to 15em.
*
* @attr {string} detail-size
*/
detailSize: string | null | undefined;
/**
- * Minimum size (in CSS length units) to be set on the detail area.
- * When specified, it prevents the detail area from shrinking below
- * this size. If there is not enough space to show master and detail
- * areas next to each other, the details are shown as an overlay:
- * either as drawer or stack, depending on the `stackOverlay` property.
- *
- * @attr {string} detail-min-size
- */
- detailMinSize: string | null | undefined;
-
- /**
- * Fixed size (in CSS length units) to be set on the master area.
- * When specified, it prevents the master area from growing or
- * shrinking. If there is not enough space to show master and detail
- * areas next to each other, the details are shown as an overlay:
- * either as drawer or stack, depending on the `stackOverlay` property.
+ * Size (in CSS length units) to be set on the master area in
+ * the CSS grid layout. If there is not enough space to show
+ * master and detail areas next to each other, the detail area
+ * is shown as an overlay. Defaults to 30em.
*
* @attr {string} master-size
*/
masterSize: string | null | undefined;
/**
- * Minimum size (in CSS length units) to be set on the master area.
- * When specified, it prevents the master area from shrinking below
- * this size. If there is not enough space to show master and detail
- * areas next to each other, the details are shown as an overlay:
- * either as drawer or stack, depending on the `stackOverlay` property.
+ * Size (in CSS length units) for the detail area when shown as an
+ * overlay. When not set, falls back to `detailSize`. Set to `100%`
+ * to make the detail cover the full layout.
*
- * @attr {string} master-min-size
+ * @attr {string} overlay-size
*/
- masterMinSize: string | null | undefined;
+ overlaySize: string | null | undefined;
/**
* Define how master and detail areas are shown next to each other,
@@ -109,38 +93,22 @@ declare class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ThemableMix
*/
orientation: 'horizontal' | 'vertical';
- /**
- * When specified, forces the details to be shown as an overlay
- * (either as drawer or stack), even if there is enough space for
- * master and detail to be shown next to each other using the default
- * (split) mode.
- *
- * In order to enforce the stack mode, use this property together with
- * `stackOverlay` property and set both to `true`.
- *
- * @attr {boolean} force-overlay
- */
- forceOverlay: boolean;
-
/**
* Defines the containment of the detail area when the layout is in
* overlay mode. When set to `layout`, the overlay is confined to the
* layout. When set to `viewport`, the overlay is confined to the
* browser's viewport. Defaults to `layout`.
+ *
+ * @attr {string} overlay-containment
*/
- containment: 'layout' | 'viewport';
+ overlayContainment: 'layout' | 'viewport';
/**
- * When true, the layout in the overlay mode is rendered as a stack,
- * making detail area fully cover the master area. Otherwise, it is
- * rendered as a drawer and has a visual backdrop.
- *
- * In order to enforce the stack mode, use this property together with
- * `forceOverlay` property and set both to `true`.
- *
- * @attr {string} stack-threshold
+ * Controls which column(s) expand to fill available space.
+ * Possible values: `'master'`, `'detail'`, `'both'`.
+ * Defaults to `'both'`.
*/
- stackOverlay: boolean;
+ expand: 'master' | 'detail' | 'both';
/**
* When true, the layout does not use animated transitions for the detail area.
diff --git a/packages/master-detail-layout/src/vaadin-master-detail-layout.js b/packages/master-detail-layout/src/vaadin-master-detail-layout.js
index 1a18f126489..a1bf9b4259d 100644
--- a/packages/master-detail-layout/src/vaadin-master-detail-layout.js
+++ b/packages/master-detail-layout/src/vaadin-master-detail-layout.js
@@ -8,12 +8,20 @@ import { getFocusableElements } from '@vaadin/a11y-base/src/focus-utils.js';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
-import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { masterDetailLayoutStyles } from './styles/vaadin-master-detail-layout-base-styles.js';
import { masterDetailLayoutTransitionStyles } from './styles/vaadin-master-detail-layout-transition-base-styles.js';
+function parseTrackSizes(gridTemplate) {
+ return gridTemplate
+ .replace(/\[[^\]]+\]/gu, '')
+ .replace(/\s+/gu, ' ')
+ .trim()
+ .split(' ')
+ .map(parseFloat);
+}
+
/**
* `` is a web component for building UIs with a master
* (or primary) area and a detail (or secondary) area that is displayed next to, or
@@ -25,19 +33,19 @@ import { masterDetailLayoutTransitionStyles } from './styles/vaadin-master-detai
*
* Part name | Description
* ---------------|----------------------
- * `backdrop` | Backdrop covering the master area in the drawer mode
+ * `backdrop` | Backdrop covering the master area in the overlay mode
* `master` | The master area
* `detail` | The detail area
*
* The following state attributes are available for styling:
*
- * Attribute | Description
- * ---------------| -----------
- * `containment` | Set to `layout` or `viewport` depending on the containment.
- * `orientation` | Set to `horizontal` or `vertical` depending on the orientation.
- * `has-detail` | Set when the detail content is provided.
- * `drawer` | Set when the layout is using the drawer mode.
- * `stack` | Set when the layout is using the stack mode.
+ * Attribute | Description
+ * ----------------------|----------------------
+ * `expand` | Set to `master`, `detail`, or `both`.
+ * `orientation` | Set to `horizontal` or `vertical` depending on the orientation.
+ * `has-detail` | Set when the detail content is provided and visible.
+ * `overflow` | Set when columns don't fit and the detail is shown as an overlay.
+ * `overlay-containment` | Set to `layout` or `viewport`.
*
* The following custom CSS properties are available for styling:
*
@@ -51,17 +59,16 @@ import { masterDetailLayoutTransitionStyles } from './styles/vaadin-master-detai
*
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
*
- * @fires {CustomEvent} backdrop-click - Fired when the user clicks the backdrop in the drawer mode.
+ * @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.
*
* @customElement vaadin-master-detail-layout
* @extends HTMLElement
* @mixes ThemableMixin
* @mixes ElementMixin
- * @mixes ResizeMixin
* @mixes SlotStylesMixin
*/
-class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement))))) {
+class MasterDetailLayout extends SlotStylesMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement)))) {
static get is() {
return 'vaadin-master-detail-layout';
}
@@ -73,11 +80,10 @@ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(Themab
static get properties() {
return {
/**
- * Fixed size (in CSS length units) to be set on the detail area.
- * When specified, it prevents the detail area from growing or
- * shrinking. If there is not enough space to show master and detail
- * areas next to each other, the details are shown as an overlay:
- * either as drawer or stack, depending on the `stackOverlay` property.
+ * 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
+ * master and detail areas next to each other, the detail area
+ * is shown as an overlay. Defaults to 15em.
*
* @attr {string} detail-size
*/
@@ -88,26 +94,10 @@ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(Themab
},
/**
- * Minimum size (in CSS length units) to be set on the detail area.
- * When specified, it prevents the detail area from shrinking below
- * this size. If there is not enough space to show master and detail
- * areas next to each other, the details are shown as an overlay:
- * either as drawer or stack, depending on the `stackOverlay` property.
- *
- * @attr {string} detail-min-size
- */
- detailMinSize: {
- type: String,
- sync: true,
- observer: '__detailMinSizeChanged',
- },
-
- /**
- * Fixed size (in CSS length units) to be set on the master area.
- * When specified, it prevents the master area from growing or
- * shrinking. If there is not enough space to show master and detail
- * areas next to each other, the details are shown as an overlay:
- * either as drawer or stack, depending on the `stackOverlay` property.
+ * Size (in CSS length units) to be set on the master area in
+ * the CSS grid layout. If there is not enough space to show
+ * master and detail areas next to each other, the detail area
+ * is shown as an overlay. Defaults to 30em.
*
* @attr {string} master-size
*/
@@ -118,18 +108,16 @@ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(Themab
},
/**
- * Minimum size (in CSS length units) to be set on the master area.
- * When specified, it prevents the master area from shrinking below
- * this size. If there is not enough space to show master and detail
- * areas next to each other, the details are shown as an overlay:
- * either as drawer or stack, depending on the `stackOverlay` property.
+ * Size (in CSS length units) for the detail area when shown as an
+ * overlay. When not set, falls back to `detailSize`. Set to `100%`
+ * to make the detail cover the full layout.
*
- * @attr {string} master-min-size
+ * @attr {string} overlay-size
*/
- masterMinSize: {
+ overlaySize: {
type: String,
sync: true,
- observer: '__masterMinSizeChanged',
+ observer: '__overlaySizeChanged',
},
/**
@@ -142,25 +130,6 @@ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(Themab
type: String,
value: 'horizontal',
reflectToAttribute: true,
- observer: '__orientationChanged',
- sync: true,
- },
-
- /**
- * When specified, forces the details to be shown as an overlay
- * (either as drawer or stack), even if there is enough space for
- * master and detail to be shown next to each other using the default
- * (split) mode.
- *
- * In order to enforce the stack mode, use this property together with
- * `stackOverlay` property and set both to `true`.
- *
- * @attr {boolean} force-overlay
- */
- forceOverlay: {
- type: Boolean,
- value: false,
- observer: '__forceOverlayChanged',
sync: true,
},
@@ -169,8 +138,10 @@ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(Themab
* overlay mode. When set to `layout`, the overlay is confined to the
* layout. When set to `viewport`, the overlay is confined to the
* browser's viewport. Defaults to `layout`.
+ *
+ * @attr {string} overlay-containment
*/
- containment: {
+ overlayContainment: {
type: String,
value: 'layout',
reflectToAttribute: true,
@@ -178,19 +149,14 @@ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(Themab
},
/**
- * When true, the layout in the overlay mode is rendered as a stack,
- * making detail area fully cover the master area. Otherwise, it is
- * rendered as a drawer and has a visual backdrop.
- *
- * In order to enforce the stack mode, use this property together with
- * `forceOverlay` property and set both to `true`.
- *
- * @attr {string} stack-threshold
+ * Controls which column(s) expand to fill available space.
+ * Possible values: `'master'`, `'detail'`, `'both'`.
+ * Defaults to `'both'`.
*/
- stackOverlay: {
- type: Boolean,
- value: false,
- observer: '__stackOverlayChanged',
+ expand: {
+ type: String,
+ value: 'both',
+ reflectToAttribute: true,
sync: true,
},
@@ -203,39 +169,6 @@ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(Themab
type: Boolean,
value: false,
},
-
- /**
- * When true, the component uses the drawer mode. This property is read-only.
- * @protected
- */
- _drawer: {
- type: Boolean,
- attribute: 'drawer',
- reflectToAttribute: true,
- sync: true,
- },
-
- /**
- * When true, the component uses the stack mode. This property is read-only.
- * @protected
- */
- _stack: {
- type: Boolean,
- attribute: 'stack',
- reflectToAttribute: true,
- sync: true,
- },
-
- /**
- * When true, the component has the detail content provided.
- * @protected
- */
- _hasDetail: {
- type: Boolean,
- attribute: 'has-detail',
- reflectToAttribute: true,
- sync: true,
- },
};
}
@@ -243,194 +176,168 @@ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(Themab
return true;
}
- /** @override */
+ /** @return {!Array} */
get slotStyles() {
return [masterDetailLayoutTransitionStyles];
}
/** @protected */
render() {
+ const isOverlay = this.hasAttribute('has-detail') && this.hasAttribute('overflow');
+ const isViewport = isOverlay && this.overlayContainment === 'viewport';
+ const isLayoutContained = isOverlay && !isViewport;
+
return html`
+
+
+
-
-
-
-
-
-
+
`;
}
- /** @private */
- __onDetailSlotChange(e) {
- const children = e.target.assignedNodes();
-
- this._hasDetail = children.length > 0;
- this.__detectLayoutMode();
-
- // Move focus to the detail area when it is added to the DOM,
- // in case if the layout is using drawer or stack mode.
- if ((this._drawer || this._stack) && children.length > 0) {
- const focusables = getFocusableElements(children[0]);
- if (focusables.length) {
- focusables[0].focus();
- }
- }
+ /** @protected */
+ connectedCallback() {
+ super.connectedCallback();
+ this.__initResizeObserver();
}
- /** @private */
- __onBackdropClick() {
- this.dispatchEvent(new CustomEvent('backdrop-click'));
+ /** @protected */
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.__resizeObserver.disconnect();
+ cancelAnimationFrame(this.__resizeRaf);
}
/** @private */
- __onDetailKeydown(event) {
- if (event.key === 'Escape' && !event.defaultPrevented) {
- // Prevent firing on parent layout when using nested layouts
- event.preventDefault();
- this.dispatchEvent(new CustomEvent('detail-escape-press'));
- }
- }
-
- /**
- * @protected
- * @override
- */
- _onResize() {
- this.__detectLayoutMode();
+ __masterSizeChanged(size, oldSize) {
+ this.__updateStyleProperty('master-size', size, oldSize);
}
/** @private */
__detailSizeChanged(size, oldSize) {
this.__updateStyleProperty('detail-size', size, oldSize);
- this.__detectLayoutMode();
}
/** @private */
- __detailMinSizeChanged(size, oldSize) {
- this.__updateStyleProperty('detail-min-size', size, oldSize);
- this.__detectLayoutMode();
+ __overlaySizeChanged(size, oldSize) {
+ this.__updateStyleProperty('overlay-size', size, oldSize);
}
/** @private */
- __masterSizeChanged(size, oldSize) {
- this.__updateStyleProperty('master-size', size, oldSize);
- this.__detectLayoutMode();
+ __updateStyleProperty(prop, size, oldSize) {
+ if (size) {
+ this.style.setProperty(`--_${prop}`, size);
+ } else if (oldSize) {
+ this.style.removeProperty(`--_${prop}`);
+ }
}
/** @private */
- __masterMinSizeChanged(size, oldSize) {
- this.__updateStyleProperty('master-min-size', size, oldSize);
- this.__detectLayoutMode();
+ __onSlotChange() {
+ this.__initResizeObserver();
}
/** @private */
- __orientationChanged(orientation, oldOrientation) {
- if (orientation || oldOrientation) {
- this.__detectLayoutMode();
- }
+ __initResizeObserver() {
+ this.__resizeObserver = this.__resizeObserver || new ResizeObserver(() => this.__onResize());
+ this.__resizeObserver.disconnect();
+
+ const children = this.querySelectorAll(':scope > [slot="detail"], :scope >:not([slot])');
+ [this, this.$.master, this.$.detail, ...children].forEach((node) => {
+ this.__resizeObserver.observe(node);
+ });
}
- /** @private */
- __forceOverlayChanged(forceOverlay, oldForceOverlay) {
- if (forceOverlay || oldForceOverlay) {
- this.__detectLayoutMode();
- }
+ /**
+ * Called by the ResizeObserver. Reads layout state synchronously (no forced
+ * reflow since layout is already computed), then defers writes to rAF.
+ * Cancels any pending rAF so the write phase always uses the latest state.
+ * @private
+ */
+ __onResize() {
+ const state = this.__computeLayoutState();
+ cancelAnimationFrame(this.__resizeRaf);
+ this.__resizeRaf = requestAnimationFrame(() => this.__applyLayoutState(state));
}
- /** @private */
- __stackOverlayChanged(stackOverlay, oldStackOverlay) {
- if (stackOverlay || oldStackOverlay) {
- this.__detectLayoutMode();
- }
+ /**
+ * Reads DOM/style state needed for layout detection. Safe to call in
+ * ResizeObserver callback where layout is already computed (no forced reflow).
+ * @private
+ */
+ __computeLayoutState() {
+ const detailContent = this.querySelector('[slot="detail"]');
+ const hadDetail = this.hasAttribute('has-detail');
+ const hasDetail = detailContent != null && detailContent.checkVisibility();
+ const hasOverflow = hasDetail && this.__checkOverflow();
+ const focusTarget = !hadDetail && hasDetail && hasOverflow ? getFocusableElements(detailContent)[0] : null;
+ return { hadDetail, hasDetail, hasOverflow, focusTarget };
}
- /** @private */
- __updateStyleProperty(prop, size, oldSize) {
- if (size) {
- this.style.setProperty(`--_${prop}`, size);
- } else if (oldSize) {
- this.style.removeProperty(`--_${prop}`);
+ /**
+ * Applies layout state to DOM attributes. Pure writes, no reads.
+ * @private
+ */
+ __applyLayoutState({ hadDetail, hasDetail, hasOverflow, focusTarget }) {
+ // Set preserve-master-width when detail first appears with overflow
+ // to prevent master width from jumping.
+ if (!hadDetail && hasDetail && hasOverflow) {
+ this.setAttribute('preserve-master-width', '');
+ } else if (!hasDetail || !hasOverflow) {
+ this.removeAttribute('preserve-master-width');
}
- this.toggleAttribute(`has-${prop}`, !!size);
- }
+ this.toggleAttribute('has-detail', hasDetail);
+ this.toggleAttribute('overflow', hasOverflow);
- /** @private */
- __setOverlayMode(value) {
- if (this.stackOverlay) {
- this._stack = value;
- } else {
- this._drawer = value;
+ // Re-render to update ARIA attributes (role, aria-modal, inert)
+ // which depend on has-detail and overflow state.
+ this.requestUpdate();
+
+ if (focusTarget) {
+ focusTarget.focus();
}
}
/** @private */
- __detectLayoutMode() {
- this._drawer = false;
- this._stack = false;
+ __checkOverflow() {
+ const isVertical = this.orientation === 'vertical';
+ const computedStyle = getComputedStyle(this);
- if (this.forceOverlay) {
- this.__setOverlayMode(true);
- return;
- }
+ const hostSize = parseFloat(computedStyle[isVertical ? 'height' : 'width']);
+ const [masterSize, masterExtra, detailSize] = parseTrackSizes(
+ computedStyle[isVertical ? 'gridTemplateRows' : 'gridTemplateColumns'],
+ );
- if (!this._hasDetail) {
- return;
+ if (Math.floor(masterSize + masterExtra + detailSize) <= Math.floor(hostSize)) {
+ return false;
}
-
- if (this.orientation === 'vertical') {
- this.__detectVerticalMode();
- } else {
- this.__detectHorizontalMode();
+ if (Math.floor(masterExtra) >= Math.floor(detailSize)) {
+ return false;
}
+ return true;
}
/** @private */
- __detectHorizontalMode() {
- const detailWidth = this.$.detail.offsetWidth;
-
- // Detect minimum width needed by master content. Use max-width to ensure
- // the layout can switch back to split mode once there is enough space.
- // If there is master size or min-size set, use that instead to force the
- // overlay mode by setting `masterSize` / `masterMinSize` to 100%/
- this.$.master.style.maxWidth = this.masterSize || this.masterMinSize || 'min-content';
- const masterWidth = this.$.master.offsetWidth;
- this.$.master.style.maxWidth = '';
-
- // If the combined minimum size of both the master and the detail content
- // exceeds the size of the layout, the layout changes to the overlay mode.
- this.__setOverlayMode(this.offsetWidth < masterWidth + detailWidth);
-
- // Toggling the overlay resizes master content, which can cause document
- // scroll bar to appear or disappear, and trigger another resize of the
- // layout which can affect previous measurements and end up in horizontal
- // scroll. Check if that is the case and if so, preserve the overlay mode.
- if (this.offsetWidth < this.scrollWidth) {
- this.__setOverlayMode(true);
- }
+ __onBackdropClick() {
+ this.dispatchEvent(new CustomEvent('backdrop-click'));
}
/** @private */
- __detectVerticalMode() {
- const masterHeight = this.$.master.clientHeight;
-
- // If the combined minimum size of both the master and the detail content
- // exceeds the available height, the layout changes to the overlay mode.
- if (this.offsetHeight < masterHeight + this.$.detail.clientHeight) {
- this.__setOverlayMode(true);
+ __onDetailKeydown(event) {
+ if (event.key === 'Escape' && !event.defaultPrevented) {
+ // Prevent firing on parent layout when using nested layouts
+ event.preventDefault();
+ this.dispatchEvent(new CustomEvent('detail-escape-press'));
}
}
@@ -529,10 +436,13 @@ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(Themab
* @protected
*/
async _finishTransition() {
- // Detect new layout mode after DOM has been updated.
- // The detection is wrapped in queueMicroTask in order to allow custom Lit elements to render before measurement.
- // https://github.com/vaadin/web-components/issues/8969
- queueMicrotask(() => this.__detectLayoutMode());
+ // Detect layout mode before resolving the transition, so the browser's
+ // "new" snapshot includes the correct overlay state. The microtask runs
+ // before the Promise resolution propagates to startViewTransition.
+ queueMicrotask(() => {
+ const state = this.__computeLayoutState();
+ this.__applyLayoutState(state);
+ });
if (!this.__transition) {
return Promise.resolve();
@@ -547,7 +457,7 @@ class MasterDetailLayout extends SlotStylesMixin(ResizeMixin(ElementMixin(Themab
/**
* @event backdrop-click
- * Fired when the user clicks the backdrop in the drawer mode.
+ * Fired when the user clicks the backdrop in the overlay mode.
*/
/**
diff --git a/packages/master-detail-layout/test/aria.test.js b/packages/master-detail-layout/test/aria.test.js
index dcde6c70826..bda24cce834 100644
--- a/packages/master-detail-layout/test/aria.test.js
+++ b/packages/master-detail-layout/test/aria.test.js
@@ -1,8 +1,9 @@
import { expect } from '@vaadin/chai-plugins';
-import { fixtureSync, nextRender } from '@vaadin/testing-helpers';
+import { fixtureSync } from '@vaadin/testing-helpers';
import '../src/vaadin-master-detail-layout.js';
import './helpers/master-content.js';
import './helpers/detail-content.js';
+import { onceResized } from './helpers.js';
window.Vaadin ||= {};
window.Vaadin.featureFlags ||= {};
@@ -13,65 +14,50 @@ describe('ARIA', () => {
beforeEach(async () => {
layout = fixtureSync(`
-
+
`);
- await nextRender();
+ await onceResized(layout);
master = layout.shadowRoot.querySelector('[part="master"]');
detail = layout.shadowRoot.querySelector('[part="detail"]');
});
- it('should set role to dialog on the detail part in the drawer mode', () => {
- layout.forceOverlay = true;
+ it('should set role="dialog" on detail in overlay mode', () => {
+ expect(layout.hasAttribute('overflow')).to.be.true;
expect(detail.getAttribute('role')).to.equal('dialog');
-
- layout.forceOverlay = false;
- expect(detail.hasAttribute('role')).to.be.false;
});
- it('should set role to dialog on the detail part in the stack mode', () => {
- layout.forceOverlay = true;
- layout.stackOverlay = true;
- expect(detail.getAttribute('role')).to.equal('dialog');
-
- layout.forceOverlay = false;
-
+ it('should remove role="dialog" when switching from overlay to split mode', async () => {
+ layout.style.width = '800px';
+ await onceResized(layout);
expect(detail.hasAttribute('role')).to.be.false;
});
- it('should set aria-model on the detail part with the viewport containment', () => {
- layout.forceOverlay = true;
- layout.containment = 'viewport';
+ it('should set aria-modal on detail with viewport containment', async () => {
+ layout.overlayContainment = 'viewport';
+ await onceResized(layout);
expect(detail.getAttribute('aria-modal')).to.equal('true');
+ });
- layout.containment = 'layout';
+ it('should not set aria-modal on detail with layout containment', () => {
expect(detail.hasAttribute('aria-modal')).to.be.false;
});
- it('should set inert on the master part with the layout containment', () => {
- layout.forceOverlay = true;
- layout.containment = 'layout';
+ it('should set inert on master with layout containment', () => {
expect(master.hasAttribute('inert')).to.be.true;
+ });
- layout.containment = 'viewport';
+ it('should not set inert on master with viewport containment', async () => {
+ layout.overlayContainment = 'viewport';
+ await onceResized(layout);
expect(master.hasAttribute('inert')).to.be.false;
});
- it('should not set inert on the master part with the detail removed', async () => {
- layout.forceOverlay = true;
- layout.containment = 'layout';
-
- const detailContent = layout.querySelector('[slot="detail"]');
- detailContent.remove();
- await nextRender();
-
+ it('should not set inert on master when detail is removed', async () => {
+ layout.querySelector('[slot="detail"]').remove();
+ await onceResized(layout);
expect(master.hasAttribute('inert')).to.be.false;
-
- layout.appendChild(detailContent);
- await nextRender();
-
- expect(master.hasAttribute('inert')).to.be.true;
});
});
diff --git a/packages/master-detail-layout/test/dom/__snapshots__/master-detail-layout.test.snap.js b/packages/master-detail-layout/test/dom/__snapshots__/master-detail-layout.test.snap.js
index 67aa1ccf036..9398705028e 100644
--- a/packages/master-detail-layout/test/dom/__snapshots__/master-detail-layout.test.snap.js
+++ b/packages/master-detail-layout/test/dom/__snapshots__/master-detail-layout.test.snap.js
@@ -1,5 +1,47 @@
/* @web/test-runner snapshot v1 */
export const snapshots = {};
+snapshots["vaadin-master-detail-layout host default"] =
+`
+
+`;
+/* end snapshot vaadin-master-detail-layout host default */
+
+snapshots["vaadin-master-detail-layout host masterSize"] =
+`
+
+`;
+/* end snapshot vaadin-master-detail-layout host masterSize */
+
+snapshots["vaadin-master-detail-layout host detailSize"] =
+`
+
+`;
+/* end snapshot vaadin-master-detail-layout host detailSize */
+
+snapshots["vaadin-master-detail-layout host masterSize and detailSize"] =
+`
+
+`;
+/* end snapshot vaadin-master-detail-layout host masterSize and detailSize */
snapshots["vaadin-master-detail-layout shadow default"] =
`