diff --git a/UPGRADE.md b/UPGRADE.md index 2e7437f0..353a10db 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -75,7 +75,9 @@ over a colour, use the role directly. - `` rows are 48px with 16px sides; a selected item draws a trailing check unless it has `icon-right`; a long menu scrolls. - `` renders a close button by default (`:with-close-button="false"` to drop it, ignored - on a `pane` or when Escape and the scrim are off) and left-aligns its `actions` in a 72px row. + on a `standard` sheet or when Escape and the scrim are off) and left-aligns its `actions` in a + 72px row. `` is removed; use `` for the second pane of a + list-detail layout. - `` opens at `50dvh` (was `90dvh`); any `height` is capped at `calc(100dvh - 72px)`. - `` `padding` defaults to `16`; `layout="full-screen"` scrolls vertically with diff --git a/resources/boost/guidelines/material-3.blade.php b/resources/boost/guidelines/material-3.blade.php index d5b52a09..d27ee217 100644 --- a/resources/boost/guidelines/material-3.blade.php +++ b/resources/boost/guidelines/material-3.blade.php @@ -41,7 +41,7 @@ Every view in this application is Material 3 Expressive (m3.material.io), throug ### Layout and breakpoints - Widths are M3's window size classes, the only variants that compile: compact below 600px (the default), `medium:` 600, `expanded:` 840, `large:` 1200, `extra-large:` 1600, and `max-medium:` … for "below". In scripts, `from()` and `upTo()` from `resources/js/breakpoints.js`. -- What changes per class: compact — navigation bar, one pane, full-screen dialogs, a bottom sheet for choices; medium — collapsed rail, one pane; expanded — rail (collapsible), two panes, menus and basic dialogs; large and extra-large — the rail expanded, two panes, a third only at extra-large as a side sheet. `` does this; content lives in panes (``), never beside the rail by hand. +- What changes per class: compact — navigation bar, one pane, full-screen dialogs, a bottom sheet for choices; medium — collapsed rail, one pane; expanded — rail (collapsible), two panes, menus and basic dialogs; large and extra-large — the rail expanded, two panes, a third only at extra-large as a side sheet. `` does this; content lives in panes (``, `` for a list's second pane), never beside the rail by hand. - Margins are 16px below `medium` and 24px from it; spacing sits on the 4px grid, as padding and gaps on the parent, with margins only between layout regions. A fixed pane is 360px (expanded) or 412px (large); a side sheet at most 400px. - Write logical properties (`ps-*`, `me-*`, `start-*`, `text-start`); directional icons mirror in RTL; charts and media controls stay LTR. Keep controls inside the safe area (`--material-safe-*`). diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 7e52658b..ddb4bff7 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -539,9 +539,9 @@ Props: `title`, `subtitle`, `icon` (centred hero icon), `separator` (draw the di ### `` -An M3 side sheet, bound like ``; `close()` in scope. Props: `title`, `subtitle`, `separator`, `side` (`end` default, `start`), `width` (`25rem`), `with-close-button` (**default true** — M3 requires a close affordance; `:with-close-button="false"` is ignored when Escape or the scrim is off, and on a pane), `close-on-escape` (default true), `without-backdrop-close`, `actions` slot (**left**-aligned in a 72dp row, which is what the side-sheet spec says; a dialog's are trailing-aligned). `pane` (with `pane-width`, `22.5rem` — M3's 360dp fixed pane) turns it into a second pane from `expanded`, where M3 shows two panes: render it after the list inside `
`. Escape leaves a pane open unless `pane-close-on-escape`. Its body is a size container — lay out inside with `@md:` (a *container* query), never a window class. +An M3 side sheet, bound like ``; `close()` in scope. Props: `title`, `subtitle`, `separator`, `side` (`end` default, `start`), `width` (`25rem`), `with-close-button` (**default true** — M3 requires a close affordance; `:with-close-button="false"` is ignored when Escape or the scrim is off, or on a `standard` sheet), `close-on-escape` (default true), `without-backdrop-close`, `actions` slot (**left**-aligned in a 72dp row, which is what the side-sheet spec says; a dialog's are trailing-aligned). For the second pane of a list-detail layout use `` instead — `` no longer has a `pane` prop. Its body is a size container — lay out inside with `@md:` (a *container* query), never a window class. -`standard` is M3's other side-sheet variant, and it is not the same thing as `pane`. A **pane** is the list-detail companion: it shows what the list beside it has selected, 360dp wide on `surface-container` with a large corner. A **standard** sheet is supplementary content beside the primary content — filters, details, a list of actions: co-planar from `expanded`, flat on `surface` with 0dp elevation and no corner, the window's full height, an outline-variant rule down its inner edge instead of a scrim, nothing inert and no focus trap. Below `expanded` it is the modal sheet. Capped at M3's 400dp whatever `width` says, and it always draws the close button. Render it in the same `expanded:flex` row as a pane. +`standard` is M3's other side-sheet variant: supplementary content beside the primary content — filters, details, a list of actions — co-planar from `expanded`, flat on `surface` with 0dp elevation and no corner, the window's full height, an outline-variant rule down its inner edge instead of a scrim, nothing inert and no focus trap. Below `expanded` it is the modal sheet. Capped at M3's 400dp whatever `width` says, and it always draws the close button. Render it beside its content in a row that only lays out side by side from `expanded` (``, or a caller's own row). ### `` diff --git a/resources/boost/skills/material-3-design/SKILL.md b/resources/boost/skills/material-3-design/SKILL.md index f73fa234..e0f73454 100644 --- a/resources/boost/skills/material-3-design/SKILL.md +++ b/resources/boost/skills/material-3-design/SKILL.md @@ -215,7 +215,7 @@ Layout keys on the width of the window, in M3's five window size classes; they a | Large | 1200–1599px | `large:` | rail expanded | 2; a fixed pane 412px | basic dialog; menu | 24px | | Extra-large | 1600px and up | `extra-large:` | rail expanded | 2, or 3 with a standard side sheet (at most 400px) | basic dialog; menu | 24px | -- `` implements the navigation column; `` is the second pane of a list-detail layout from `expanded:`, a supporting pane (360px, beside the focus pane) from `expanded:` and below it before that. Moving up a class, ask what to reveal, divide into panes, resize, reposition or swap — never swap a component for one that does not do the same job. +- `` implements the navigation column; `` is the second pane of a list-detail layout from `expanded:`, `` puts a supporting pane (360px, beside the focus pane) from `expanded:` and below it before that. Moving up a class, ask what to reveal, divide into panes, resize, reposition or swap — never swap a component for one that does not do the same job. - Scaffold: bars (app bar at the top, navigation bar at the bottom: 3–5 destinations), rails (the navigation rail, toolbars, the FAB, on the leading edge), panes (all content), around a safety region that stays clear of the device's own chrome (`--material-safe-top|bottom|left|right`). - Canonical layouts: feed (a grid of cards that gains columns per class), list-detail (one pane on compact, two from expanded; a back button only in single-pane mode, a selected row only in two-pane mode), supporting pane (two thirds focus, one third support). - Bidirectionality: write logical properties (`ps-4`, `me-2`, `start-0`, `text-start`, `border-s`); leading and trailing icons swap, directional icons (back, send) mirror, the rail moves to the right; charts, media controls, clocks and Hebrew progress bars stay left-to-right. diff --git a/resources/css/components.css b/resources/css/components.css index 5479e500..8cca514c 100644 --- a/resources/css/components.css +++ b/resources/css/components.css @@ -62,5 +62,6 @@ @import './components/list-item.css'; @import './components/list.css'; @import './components/modal.css'; +@import './components/drawer.css'; /* Navigation */ diff --git a/resources/css/components/drawer.css b/resources/css/components/drawer.css new file mode 100644 index 00000000..7d1b6f11 --- /dev/null +++ b/resources/css/components/drawer.css @@ -0,0 +1,226 @@ +/* + * ``: M3's side sheet, modal by default and co-planar as `standard` from 840px + * (docs/reference/m3/components-actions-communication-containment.md § Side sheets → Specs; no + * androidx Compose token file exists for it — Compose support is "Unavailable" per the site — so + * the numbers come from the specs page and, for the standard variant's 0dp elevation, + * material-components-android's `SideSheet.md`). + * + * [data-md-drawer] the wrapper; data-md-open, data-md-standard + * [data-md-drawer-scrim] modal only (hidden by JS once the sheet is wide/standard) + * [data-md-drawer-sheet] surface-container-low, elevation 1; data-md-side + * [data-md-drawer-head] title/subtitle/close, and the separator's divider + * [data-md-drawer-head-row], [data-md-drawer-heading], [data-md-drawer-title], + * [data-md-drawer-subtitle], [data-md-drawer-close] + * [data-md-drawer-body] a size container, so its contents lay out by the sheet's own + * width (`@md:`), never the viewport's + * [data-md-drawer-actions] left-aligned, 72dp (C-10 — a dialog's are trailing-aligned) + * + * 400px cap, 24dp start/end padding, a large corner on the inner edge only, 56dp side-sheet + * width start — all from the specs table. It enters on the emphasized-decelerate easing rather + * than a spring, because a sheet anchored to an edge that overshot would open a gap; below + * `expanded` it slides, and once `standard` makes it co-planar it only fades (an in-flow box does + * not need to travel). + * + * A `standard` sheet drops the scrim, the trap and the elevation, and swaps surface-container-low + * for `surface` with an outline-variant rule down its inner edge, in place of the scrim + * (its anatomy's "Divider (optional)"). Below `expanded` (840px) it is the modal sheet — M3 caps + * a side sheet at 400dp, and a 600px window has too little room left beside one, so the switch + * sits at `expanded` rather than `medium`. `data-md-drawer-collapsed` (the view, from the + * `wide` — window ≥ 840px — Alpine state) takes the standard sheet out of the layout entirely + * while it is closed there, so the content beside it fills the space. + */ + +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@import './button.css'; +@import './divider.css'; + +@layer material.components { + [data-md-drawer-scrim] { + position: fixed; + inset: 0; + z-index: 40; + background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent); + transition-property: opacity, display; + transition-duration: var(--md-sys-motion-effects-default-duration); + transition-timing-function: var(--md-sys-motion-effects-default); + transition-behavior: allow-discrete; + } + + @starting-style { + [data-md-drawer-scrim] { + opacity: 0; + } + } + + [data-md-drawer-sheet] { + position: fixed; + top: var(--material-safe-top, env(safe-area-inset-top)); + bottom: 0; + z-index: 50; + display: flex; + width: 100%; + flex-direction: column; + overflow-y: auto; + padding: var(--md-sys-measurement-space300); + background-color: var(--md-sys-color-surface-container-low); + color: var(--md-sys-color-on-surface); + box-shadow: var(--md-sys-elevation-1); + translate: 100% 0; + opacity: 0; + transition-property: translate, opacity, display; + transition-duration: var(--md-sys-motion-effects-default-duration); + transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate); + transition-behavior: allow-discrete; + } + + [data-md-drawer-sheet][data-md-side='end'] { + inset-inline-end: 0; + } + + [data-md-drawer-sheet][data-md-side='start'] { + inset-inline-start: 0; + translate: -100% 0; + } + + [data-md-drawer][data-md-open] [data-md-drawer-sheet] { + translate: 0 0; + opacity: 1; + transition-duration: var(--md-sys-motion-spatial-default-duration); + transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate); + } + + @starting-style { + [data-md-drawer][data-md-open] [data-md-drawer-sheet] { + translate: 100% 0; + opacity: 0; + } + + [data-md-drawer][data-md-open] [data-md-drawer-sheet][data-md-side='start'] { + translate: -100% 0; + } + } + + @media (width >= 600px) { + [data-md-drawer-sheet] { + width: var(--sheet-width); + max-width: calc(100vw - 64px); + } + + [data-md-drawer-sheet][data-md-side='end'] { + border-start-start-radius: var(--md-sys-shape-corner-lg); + border-end-start-radius: var(--md-sys-shape-corner-lg); + } + + [data-md-drawer-sheet][data-md-side='start'] { + border-start-end-radius: var(--md-sys-shape-corner-lg); + border-end-end-radius: var(--md-sys-shape-corner-lg); + } + } + + /* Co-planar from expanded: sticky beside the content, no scrim, no elevation, flat on + `surface` with an outline-variant rule down the inner edge instead. It only fades here — + an in-flow box does not travel like an overlay does. */ + @media (width >= 840px) { + [data-md-drawer][data-md-standard] { + position: sticky; + top: 0; + height: 100dvh; + flex-shrink: 0; + align-self: flex-start; + } + + [data-md-drawer][data-md-standard][data-md-drawer-collapsed] { + display: none; + } + + [data-md-drawer][data-md-standard] [data-md-drawer-sheet] { + position: relative; + top: 0; + z-index: auto; + height: 100%; + translate: 0 0; + border-radius: var(--md-sys-shape-corner-none); + background-color: var(--md-sys-color-surface); + box-shadow: none; + opacity: 0; + transition-property: opacity, display; + transition-duration: var(--md-sys-motion-effects-default-duration); + transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate); + } + + [data-md-drawer][data-md-standard][data-md-open] [data-md-drawer-sheet] { + opacity: 1; + transition-duration: var(--md-sys-motion-spatial-default-duration); + transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate); + } + + @starting-style { + [data-md-drawer][data-md-standard][data-md-open] [data-md-drawer-sheet] { + opacity: 0; + } + } + + [data-md-drawer][data-md-standard] [data-md-drawer-sheet][data-md-side='end'] { + border-inline-start: 1px solid var(--md-sys-color-outline-variant); + } + + [data-md-drawer][data-md-standard] [data-md-drawer-sheet][data-md-side='start'] { + border-inline-end: 1px solid var(--md-sys-color-outline-variant); + } + } + + [data-md-drawer-head] { + margin-bottom: var(--md-sys-measurement-space200); + } + + [data-md-drawer-head-row] { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + } + + [data-md-drawer-heading] { + min-width: 0; + } + + [data-md-drawer-title] { + font: var(--md-sys-typescale-title-lg); + letter-spacing: var(--md-sys-typescale-title-lg-tracking); + } + + [data-md-drawer-subtitle] { + margin-top: var(--md-sys-measurement-space50); + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-body-md); + letter-spacing: var(--md-sys-typescale-body-md-tracking); + } + + [data-md-drawer-close] { + display: inline-flex; + flex-shrink: 0; + margin-top: calc(-1 * var(--md-sys-measurement-space100)); + margin-inline-end: -12px; + } + + [data-md-drawer-head] > [data-md-divider] { + margin-top: var(--md-sys-measurement-space200); + } + + [data-md-drawer-body] { + container-type: inline-size; + flex: 1 1 0%; + } + + [data-md-drawer-actions] { + display: flex; + min-height: var(--md-sys-measurement-space900); + flex-shrink: 0; + flex-wrap: wrap; + align-items: center; + justify-content: flex-start; + gap: var(--md-sys-measurement-space100); + padding-top: var(--md-sys-measurement-space200); + } +} diff --git a/resources/views/components/drawer.blade.php b/resources/views/components/drawer.blade.php index f8dd493e..a35ae6d3 100644 --- a/resources/views/components/drawer.blade.php +++ b/resources/views/components/drawer.blade.php @@ -1,58 +1,49 @@ -{{-- An M3 side sheet: detail or controls that slide in from the edge over a scrim; on a phone it is - the whole screen. With `pane`, from `expanded` (840px) it is a second pane beside the list - instead — M3 shows two panes for list-detail from expanded upwards, and puts a supporting pane - on the side of the focus pane at expanded, 360dp wide - (docs/reference/m3/foundations-supplement.md § Canonical layout examples). +{{-- An M3 side sheet: detail or controls that slide in from the edge over a scrim; on a phone it + is the whole screen. - The open state is the Livewire property in `wire:model` (entangled live, so a detail held in the - URL follows a close) — a flag or an id — and closing writes back `false` or `null`. Without - `wire:model` it reads and writes `open` in the Alpine scope around it. `close()` is in scope for - anything inside the sheet, so it can draw its own close button. + The open state is the Livewire property in `wire:model` (entangled live, so a detail held in + the URL follows a close) — a flag or an id — and closing writes back `false` or `null`. + Without `wire:model` it reads and writes `open` in the Alpine scope around it. `close()` is in + scope for anything inside the sheet, so it can draw its own close button. As a sheet it is modal: the page inert and still (`x-trap.inert.noscroll`), and it enters on emphasized decelerate rather than a spring — a sheet anchored to the edge that overshot would - open a gap. M3's modal side sheet: surface-container-low, a large corner on its inner edge, - elevation 1; `side` `end` (the default) or `start`; `width` from `medium` (a caller's `w-*` - would race the sheet's own — a compact window gets the full-bleed sheet). + open a gap. M3's modal side sheet (NavigationDrawerTokens.kt-adjacent side-sheet sources; + docs/reference/m3/components-actions-communication-containment.md § Side sheets): surface- + container-low, a large corner on its inner edge, elevation 1; `side` `end` (the default) or + `start`; `width` from `medium` (a caller's `class` would race the sheet's own — a compact + window gets the full-bleed sheet). As a **standard** side sheet (`standard`, from `expanded`) it is M3's other variant: co-planar with the content rather than over it — no scrim, no focus trap, nothing inert, 0dp elevation (material-components-android's `SideSheet.md`: "standard side sheet elevation = 0dp, coplanar"), `surface` rather than surface-container-low, no corner, and an outline-variant divider down its inner edge in place of the scrim. It sits in the page flow beside the - content, spans the window's height and scrolls on its own. M3 calls the standard sheet - "supplementary surfaces mainly for medium to expanded breakpoints" and the modal one - "preferred at compact breakpoints"; the library switches at `expanded` (840px) rather than at - medium, because M3 also caps a side sheet at 400dp and a 600px window has too little left - beside one — below `expanded` a `standard` sheet is the modal sheet - (docs/reference/m3/components-actions-communication-containment.md § Side sheets). + content, spans the window's height and scrolls on its own; below `expanded` (840px) it is the + modal sheet — M3 calls the standard sheet "supplementary surfaces mainly for medium to + expanded breakpoints" and the modal one "preferred at compact breakpoints", and the switch + sits at `expanded` rather than `medium` because M3 also caps a side sheet at 400dp and a + 600px window has too little left beside one. - `standard` and `pane` are not the same thing and neither replaces the other: a **pane** is the - second pane of a list-detail layout — it shows what the list beside it has selected, is - `22.5rem` wide (M3's 360dp fixed pane), sticks under the top of the viewport and sits on - `surface-container` with a large corner. A **standard** sheet is supplementary content beside - the primary content — filters, details, a list of actions — full height, flat on `surface`, - divided from the content by a rule. Pass one or the other; `pane` wins if both are given. - - As a pane (`pane`, from `expanded`) nothing is covered: the page renders the drawer after its - list in an `expanded:flex expanded:items-start expanded:gap-6` row, the drawer sticks under the - top of the viewport, the - list stays usable and another row swaps what it shows — no scrim, no trap, no inert page. While - closed it takes no room. `pane-width` sizes the pane (`22.5rem`, M3's 360dp fixed pane). Escape closes - the sheet but leaves a pane open, since the page beside it is still in use; `pane-close-on-escape` - closes the pane on Escape too, for a pane that is a transient detail. The body is - a size container, so its contents lay out by the room the sheet or pane actually has (`@md:`), - never by the viewport. + For the second pane of a list-detail layout use `` instead (step 35's canonical + layout); the two are not the same thing — a pane shows what the list beside it selected, a + standard sheet is supplementary content (filters, details, a list of actions) beside the + primary content, divided from it by a rule rather than being part of its selection. M3 **requires** a close affordance on a side sheet — without one nobody can predict the sheet's open/close flow or tell whether it is transient or permanent (docs/reference/m3/components-actions-communication-containment.md § Side sheets → - Accessibility) — so `with-close-button` is on by default, and `:with-close-button="false"` is - ignored where nothing else closes the sheet: Escape off, the scrim off, or a pane or a - standard sheet, which have neither from `expanded`. + Accessibility, C-04) — so `with-close-button` is on by default, and + `:with-close-button="false"` is ignored where nothing else closes the sheet: Escape off, the + scrim off, or `standard`, which has neither from `expanded`. maryUI's API, kept: `title`, `subtitle`, `separator`, `with-close-button`, `close-on-escape`, - `without-backdrop-close`, `right` (ignored; use `side`), and an `actions` slot. --}} + `without-backdrop-close`, `right` (ignored; use `side`), and an `actions` slot. + + The root renders `data-md-drawer`, `data-md-open` and `data-md-standard`; the sheet is + `data-md-drawer-sheet` with `data-md-side`, its parts `data-md-drawer-head`/-head-row/ + -heading/-title/-subtitle/-close/-body/-actions — drawn by resources/css/components/ + drawer.css, which imports button.css and divider.css for what the view renders. --}} @props([ 'title' => null, @@ -64,23 +55,14 @@ 'closeOnEscape' => true, 'withoutBackdropClose' => false, 'width' => '25rem', - 'pane' => false, - 'paneWidth' => '22.5rem', - 'paneCloseOnEscape' => false, 'standard' => false, ]) @php $model = $attributes->wire('model')->value() ?: null; $id = $attributes->get('id') ?? 'material-sheet-'.substr(md5($model.'|'.$title), 0, 10); - $start = $side === 'start'; - - // One or the other: a pane is the list-detail companion, a standard sheet is supplementary - // content beside the primary content. - $standard = $standard && ! $pane; - - // Both stop being modal from `expanded`, so both watch the window for that width. - $wide = $pane || $standard; + $side = $side === 'start' ? 'start' : 'end'; + $standard = (bool) $standard; // The side-sheet specs table caps the sheet at 400dp, which is where `width` already starts; // a wider one is the modal sheet's to take, not the co-planar standard sheet's. @@ -89,7 +71,7 @@ // M3 requires a close affordance; the prop can only ever add one, never take away the last // way out of the sheet. A standard sheet keeps no scrim and no trap from `expanded`, and // Escape leaves it open there, so it always draws one. - $closeButton = $withCloseButton || ! $closeOnEscape || $withoutBackdropClose || $standard || ($pane && ! $paneCloseOnEscape); + $closeButton = $withCloseButton || ! $closeOnEscape || $withoutBackdropClose || $standard; @endphp
wire('model')).live, @endif wide: false, close() { this.open = typeof this.open === 'boolean' ? false : null; }, - @if ($wide) + @if ($standard) init() { const query = window.matchMedia('(width >= 52.5rem)'); this.wide = query.matches; @@ -105,98 +87,62 @@ }, @endif }" - @if ($closeOnEscape) x-on:keydown.window.escape="{{ $paneCloseOnEscape ? 'if (open) close()' : 'if (open && ! wide) close()' }}" @endif - data-sheet="{{ $id }}" - @if ($pane) - x-bind:class="! open && 'expanded:hidden'" - class="expanded:sticky expanded:top-[calc(var(--material-safe-top,env(safe-area-inset-top))+1.25rem)] expanded:shrink-0 expanded:self-start" - data-pane - @elseif ($standard) - {{-- A standard sheet spans the window's height beside the content and takes no room while - closed; sticky, so it stays put as the page scrolls past it. --}} - x-bind:class="! open && 'expanded:hidden'" - class="expanded:sticky expanded:top-0 expanded:h-dvh expanded:shrink-0 expanded:self-start" - data-standard - @endif + @if ($closeOnEscape) x-on:keydown.window.escape="if (open && ! wide) close()" @endif + x-bind:data-md-open="open ? '' : null" + x-bind:data-md-drawer-collapsed="(! open && wide) ? '' : null" + data-md-drawer + @if ($standard) data-md-standard @endif > -
$wide]) - aria-hidden="true" - >
+
diff --git a/tests/Feature/Components/ContainmentStylesheetsTest.php b/tests/Feature/Components/ContainmentStylesheetsTest.php index 2e838141..81fd783c 100644 --- a/tests/Feature/Components/ContainmentStylesheetsTest.php +++ b/tests/Feature/Components/ContainmentStylesheetsTest.php @@ -22,6 +22,7 @@ dataset('containment components', [ 'list', 'list-item', 'modal', + 'drawer', ]); /** diff --git a/tests/Feature/Components/OverlayTest.php b/tests/Feature/Components/OverlayTest.php index ab50d500..8568ebba 100644 --- a/tests/Feature/Components/OverlayTest.php +++ b/tests/Feature/Components/OverlayTest.php @@ -174,41 +174,27 @@ it('gives a full-screen dialog M3\'s 56px header and action bar', function () { ->toContain('min-height: var(--md-sys-measurement-space700);'); }); -it('leaves a pane open on Escape unless it is asked to close then too', function () { - expect((string) $this->blade('Body')) - ->toContain('x-on:keydown.window.escape="if (open && ! wide) close()"') - ->and((string) $this->blade('Body')) - ->toContain('x-on:keydown.window.escape="if (open) close()"') - ->and((string) $this->blade('Body')) - ->not->toContain('keydown.window.escape'); -}); - it('always offers a way out of a side sheet, as M3 requires', function () { expect((string) $this->blade('Body'))->toContain('aria-label="Close"') ->and((string) $this->blade('Body'))->not->toContain('aria-label="Close"') ->and((string) $this->blade('Body'))->toContain('aria-label="Close"') ->and((string) $this->blade('Body'))->toContain('aria-label="Close"') - ->and((string) $this->blade('Body'))->toContain('aria-label="Close"') - ->and((string) $this->blade('Body'))->not->toContain('aria-label="Close"'); + ->and((string) $this->blade('Body'))->toContain('aria-label="Close"'); }); it('puts a side sheet\'s actions on the left, in M3\'s 72dp row', function () { expect((string) $this->blade('Body')) - ->toContain('flex min-h-18 shrink-0 flex-wrap items-center justify-start gap-2 pt-4'); + ->toMatch('//'); }); -it('slides a side sheet in from either edge, and is a pane from expanded when asked', function () { +it('slides a side sheet in from either edge', function () { expect((string) $this->blade('Body')) ->toContain('x-trap.inert.noscroll="open && ! wide"') - ->toContain('end-0 medium:rounded-s-corner-lg') - ->toContain('bg-surface-container-low') + ->toContain('data-md-side="end"') + ->toContain('data-md-drawer-sheet') ->toContain('role="dialog"') ->toContain('aria-label="Close"') - ->and((string) $this->blade('Body'))->toContain('start-0 medium:rounded-e-corner-lg') - ->and((string) $this->blade('Body')) - ->toContain('data-pane') - ->toContain('--pane-width: 28rem') - ->toContain("matchMedia('(width >= 52.5rem)')"); + ->and((string) $this->blade('Body'))->toContain('data-md-side="start"'); }); it('draws a modal bottom sheet with a drag handle, or a standard one without a scrim', function () { @@ -234,24 +220,37 @@ it('is M3\'s standard side sheet from expanded, and the modal one below', functi $html = (string) $this->blade('Body'); expect($html) - ->toContain('data-standard') - ->toContain('expanded:sticky expanded:top-0 expanded:h-dvh expanded:shrink-0 expanded:self-start') - ->toContain('expanded:relative expanded:top-0 expanded:z-auto expanded:h-full expanded:rounded-corner-none expanded:bg-surface expanded:shadow-none') - ->toContain('expanded:border-s expanded:border-outline-variant') + ->toContain('data-md-standard') ->toContain('--sheet-width: min(25rem, 25rem)') ->toContain("matchMedia('(width >= 52.5rem)')") ->toContain('x-trap.inert.noscroll="open && ! wide"') - ->toContain('bg-scrim/32 expanded:hidden') ->toContain('aria-label="Close"') ->and((string) $this->blade('Body')) - ->toContain('expanded:border-e expanded:border-outline-variant') + ->toContain('data-md-side="start"') ->toContain('--sheet-width: min(30rem, 25rem)') - ->and((string) $this->blade('Body')) - ->toContain('data-pane') - ->not->toContain('data-standard') ->and((string) $this->blade('Body')) - ->not->toContain('data-standard') - ->not->toContain('expanded:hidden'); + ->not->toContain('data-md-standard'); + + expect(File::get(__DIR__.'/../../../resources/css/components/drawer.css')) + ->toContain('[data-md-drawer][data-md-standard]') + ->toContain('[data-md-drawer][data-md-standard][data-md-drawer-collapsed]') + ->toContain('[data-md-drawer][data-md-standard] [data-md-drawer-sheet][data-md-side=\'end\']') + ->toContain('[data-md-drawer][data-md-standard] [data-md-drawer-sheet][data-md-side=\'start\']') + ->toContain('box-shadow: none;'); +}); + +it('draws the drawer from a stylesheet imported from the containment block, with the pane prop gone', function () { + $view = File::get(__DIR__.'/../../../resources/views/components/drawer.blade.php'); + + expect($view) + ->not->toContain("'pane'") + ->not->toContain('paneWidth') + ->not->toContain('paneCloseOnEscape') + ->not->toContain('pane-width') + ->not->toContain('pane-close-on-escape') + ->not->toContain('data-pane') + ->and(File::get(__DIR__.'/../../../resources/css/components.css')) + ->toContain("@import './components/drawer.css';"); }); it('gives a bottom sheet M3\'s preset heights, cycled from the drag handle', function () {