Sweep up navigation's last data-navigation-* references
Plan step 36 (navigation group, second batch, cleanup). Every leftover data-navigation-*, data-tall, data-hide-on-scroll, data-hidden, data-width, data-align, data-divider, data-fill and component-level data-open reference outside the rewritten stylesheets and views moves to data-md-*, in the files the two component commits left alone: tests/Feature/Components/ScaffoldTest.php (the scaffold's own rewrite is a later batch, but it renders the bar and the rail today and asserts their hooks); tests/Browser/NavigationTest.php, ShowcaseTest.php and ColourProfilesTest.php (selectors only — document-level attributes, data-rail, data-rail-auto and data-app-shell*, keep their names). NavigationTest.php also gets the browser tests the plan owed this group (docs/plans/material-3-browser-tests.md § Navigation): a tall bar's vertical layout at a width where the short bar would go horizontal; hide-on-scroll never firing while a pinned snackbar is on screen; a narrow rail's 80px width and centred destinations; a rail that hides when collapsed leaving the layout and coming back only through an application's own menu button. resources/css/components/navigation.css and its import in tailwind.css are deleted now that nothing imports the file any more (the previous commit's message said this already happened there; it did not — this is where it actually lands). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
25549374ec
commit
cbd7d01c62
@@ -983,7 +983,7 @@ M3 Expressive's navigation rail: collapsed (96px, icon over label) or expanded (
|
||||
|
||||
- `mode`: `collapsed`, `expanded`, `collapsible` (default: expanded until its menu button collapses it; the choice is `$store.rail`, remembered and applied before the first paint), `modal` (collapsed in the layout; the menu button or `$store.rail.show()` opens it expanded over a scrim, focus held until Escape, the scrim or leaving the page), `adaptive` (`<x-scaffold>`'s, one rail per window size class: hidden and opened as a modal on a compact window, collapsed and opened as a modal at `medium`, a standard rail from `expanded` — collapsed there, expanded from `large`).
|
||||
- Props: `label` ("Main"), `width` (expanded width, `16rem`, held between 220 and 360px — or the word `narrow` for M3's other *collapsed* width, 80px against the default 96, where the items are their icons alone; the labels stay in the accessibility tree and a narrow rail still expands to 16rem), `align` (`top` default, or `center` for M3's centred destinations — preferred on a tablet; the menu button, brand and FAB stay at the top and the footer at the foot), `hide-when-collapsed` (M3's immersive expanded behaviour, `collapsible` and `adaptive` only: collapsing the rail takes it out of the layout instead of narrowing it, and `$store.rail.show()` brings it back expanded over a scrim — so put a menu button in the app bar; the rail's own button then docks it again. Not below `medium` for a collapsible rail nor at `medium` for an adaptive one, where the window rather than the visitor collapses it and M3's collapsed rail may never hide), `menu` (the menu button; on by default for `collapsible`, `modal`, `adaptive`), `divider` (M3's optional vertical divider on the page's side — use it when the page scrolls under a fixed rail), `fill` (`false` for a transparent container, which M3 allows while the items keep 3:1 contrast). Slots: `brand` (beside the menu button, expanded only), `header` (one `<x-fab label icon>`, which the rail morphs into an extended FAB and back as it expands — it also rests at elevation 0, as M3 asks of a nested FAB), the destinations (the only part that scrolls), `footer`. In a flex row the rail sticks to the top of the viewport.
|
||||
- Anything else inside a rail takes both shapes with the `rail-collapsed:` variant, true while that rail is drawn collapsed for whatever reason: `<span class="rail-collapsed:hidden">…expanded only…</span>`, `<span class="hidden rail-collapsed:inline-flex">…collapsed only…</span>`. Put the variant on a wrapper, never on a component. Nothing that shows while collapsed may be wider than 96px.
|
||||
- Anything else inside a rail can take both shapes by matching the rail's own state directly — `data-md-navigation-rail`'s value, `:not([data-md-open])`, and the window band each mode collapses in, the same selectors `resources/css/components/navigation-rail.css` uses for every branch of "collapsed" (that file, not this one, is where the numbers live). The Tailwind `rail-collapsed:` variant this used to teach no longer exists: the package's own CSS is plain now. Nothing that shows while collapsed may be wider than 96px.
|
||||
- A `collapsible` rail is held to the collapsed 96px below `medium` (600px), where M3 says to use a navigation bar rather than a standard rail. `collapsed` and `expanded` are fixed-width by design: wrap one in a `medium:` element if it must not show on a phone.
|
||||
- `<x-navigation-rail-item>`: the same props as `<x-navigation-bar-item>`. `<x-navigation-rail-section label="…">`: a group with a heading that shows only while the rail is expanded; it names the group for screen readers either way.
|
||||
- `$store.rail`: `collapsed`, `toggle()`, `collapse()`, `expand()` (the remembered choice; `auto` is true while nothing is stored, so an adaptive rail takes its window size class's default instead, and the first choice clears it), `open`, `show()`, `hide()` (the modal rail; closed on every `wire:navigate`). `config/livewire-material.php` → `rail.default` (`expanded` or `collapsed`) and `rail.storage_key` (`material-rail`).
|
||||
|
||||
@@ -1,844 +0,0 @@
|
||||
/*
|
||||
* M3 Expressive navigation: the flexible navigation bar and the navigation rail — collapsed,
|
||||
* expanded and modal.
|
||||
*
|
||||
* Values from androidx Compose Material 3 (Apache-2.0) at androidx-main
|
||||
* 27cf9a7d5788aa0f5f2d8b6699ce279560daf326: tokens/NavigationBarTokens.kt,
|
||||
* NavigationBarVerticalItemTokens.kt, NavigationBarHorizontalItemTokens.kt,
|
||||
* NavigationRailCollapsedTokens.kt, NavigationRailExpandedTokens.kt,
|
||||
* NavigationRailBaselineItemTokens.kt, NavigationRailVerticalItemTokens.kt,
|
||||
* NavigationRailHorizontalItemTokens.kt, NavigationRailColorTokens.kt, and the layout in
|
||||
* ShortNavigationBar.kt, WideNavigationRail.kt and NavigationItem.kt.
|
||||
*
|
||||
* [data-navigation-bar] surface-container, 64px (data-tall: 80px), the bottom
|
||||
* safe area under it
|
||||
* [data-navigation-bar-items] equal widths; centred from a 600px-wide short bar
|
||||
* [data-navigation-bar-item] data-active
|
||||
* [data-navigation-pill] icon and label; the indicator itself from 600px
|
||||
* [data-navigation-indicator] the 56×32 indicator around the icon below 600px
|
||||
* [data-navigation-label]
|
||||
*
|
||||
* [data-navigation-rail="collapsed|expanded|collapsible|modal|adaptive"] data-open
|
||||
* [data-navigation-rail-scrim] modal and adaptive rails
|
||||
* [data-navigation-rail-panel] the <nav>
|
||||
* [data-navigation-rail-header] menu button, brand, FAB — never scrolls
|
||||
* [data-navigation-rail-destinations] scrolls when the window is too short
|
||||
* [data-navigation-rail-section] a heading (expanded only) and its items
|
||||
* [data-navigation-rail-item] data-active; the full-width pill when expanded
|
||||
* [data-navigation-indicator] the 56×32 indicator when collapsed
|
||||
* [data-navigation-label]
|
||||
* [data-navigation-rail-footer] never scrolls
|
||||
*
|
||||
* `rail-collapsed:` matches a rail, and everything in it, while it is drawn collapsed — whatever
|
||||
* made it so: its mode, the visitor's choice on <html data-rail> (set before the first paint by
|
||||
* <x-theme-script>), or a window below `expanded` (840px) for the adaptive rail. A rail item is written once
|
||||
* and takes both shapes from it; so can anything an application puts in a rail
|
||||
* (`<span class="rail-collapsed:hidden">`).
|
||||
*
|
||||
* The two widths the adaptive rail turns on are M3's window size classes, not Tailwind's screens
|
||||
* (docs/reference/m3/foundations.md § Layout, foundations-supplement.md § Breakpoints): 37.5rem
|
||||
* (600px) is the compact/medium boundary — below it the navigation bar carries the destinations and
|
||||
* the rail takes no room in the layout; 52.5rem (840px) is `expanded`, where M3 asks for a standard
|
||||
* rail in the layout rather than one that opens over a scrim; 75rem (1200px) is `large`, where the
|
||||
* rail starts expanded instead of collapsed. The bar's own item layout is a *container* query at
|
||||
* the same 37.5rem, so a bar in a narrow column lays out by its own width.
|
||||
*
|
||||
* Everything here is in `@layer components` except the blocks after it, which reach past the
|
||||
* component into what an application wrote — the bottom offset <x-scaffold> publishes, a FAB put
|
||||
* in the rail: those have to beat a utility, and a layer never does.
|
||||
*/
|
||||
|
||||
/* Every branch below reads `:not([data-open])`: a rail open over a scrim is drawn expanded whatever
|
||||
made it collapsed, which only `hide-when-collapsed` can bring about outside the modal modes. */
|
||||
@custom-variant rail-collapsed {
|
||||
&:where([data-navigation-rail='collapsed'], [data-navigation-rail='collapsed'] *) {
|
||||
@slot;
|
||||
}
|
||||
|
||||
&:where([data-rail='collapsed'] [data-navigation-rail='collapsible']:not([data-open]), [data-rail='collapsed'] [data-navigation-rail='collapsible']:not([data-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
|
||||
&:where([data-navigation-rail='modal']:not([data-open]), [data-navigation-rail='modal']:not([data-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
|
||||
/* Below `medium` a 220–360px rail would be most of the screen, so a rail whose width is the
|
||||
visitor's choice is held collapsed there whatever they chose. */
|
||||
@media (width < 37.5rem) {
|
||||
&:where([data-navigation-rail='collapsible']:not([data-open]), [data-navigation-rail='collapsible']:not([data-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 52.5rem) {
|
||||
&:where([data-navigation-rail='adaptive']:not([data-open]), [data-navigation-rail='adaptive']:not([data-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
/* Expanded (840–1199): a standard rail, collapsed unless the visitor expanded it. `data-rail`
|
||||
alone cannot say that — it carries `rail.default` for a visitor who never chose — so
|
||||
`data-rail-auto`, which <x-theme-script> sets while nothing is stored, stands for "no
|
||||
choice yet" and the class's own default applies. */
|
||||
@media (52.5rem <= width < 75rem) {
|
||||
&:where(:is([data-rail='collapsed'], [data-rail-auto]) [data-navigation-rail='adaptive']:not([data-open]), :is([data-rail='collapsed'], [data-rail-auto]) [data-navigation-rail='adaptive']:not([data-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
/* Large and extra-large (from 1200): expanded to begin with, which is what M3 prefers there. */
|
||||
@media (width >= 75rem) {
|
||||
&:where([data-rail='collapsed'] [data-navigation-rail='adaptive']:not([data-open]), [data-rail='collapsed'] [data-navigation-rail='adaptive']:not([data-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* ---------------------------------------------------------------- the navigation bar */
|
||||
|
||||
[data-navigation-bar] {
|
||||
container-type: inline-size;
|
||||
padding-inline: var(--material-safe-left, env(safe-area-inset-left)) var(--material-safe-right, env(safe-area-inset-right));
|
||||
padding-bottom: var(--material-safe-bottom, env(safe-area-inset-bottom));
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-navigation-bar-items] {
|
||||
display: flex;
|
||||
min-height: 4rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
/* The tall configuration: NavigationBarTokens.TallContainerHeight, 80dp. It keeps the vertical
|
||||
item layout at every width — the horizontal one below is the short bar's alone — so the
|
||||
label always sits under its icon, which is what the extra 16px is for. */
|
||||
[data-navigation-bar][data-tall] [data-navigation-bar-items] {
|
||||
min-height: 5rem;
|
||||
}
|
||||
|
||||
/* "Scrolling: hides on scroll-down, reappears on scroll-up" (the reference's Navigation bar
|
||||
§ Behaviour). It slides out of the window on the default spatial spring, which reduced
|
||||
motion zeroes to an instant swap along with every other duration token. Whether it may hide
|
||||
at all is resources/js/navigation.js's call: never while a bottom sheet, a drawer or a
|
||||
snackbar is on screen, since those are anchored to the bar's edge. */
|
||||
[data-navigation-bar][data-hide-on-scroll] {
|
||||
transition: translate var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
|
||||
}
|
||||
|
||||
[data-navigation-bar][data-hide-on-scroll][data-hidden] {
|
||||
translate: 0 100%;
|
||||
}
|
||||
|
||||
[data-navigation-bar-item] {
|
||||
--navigation-layer: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-block: 0.375rem;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
[data-navigation-bar-item] [data-navigation-pill] {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
}
|
||||
|
||||
[data-navigation-bar-item] [data-navigation-label] {
|
||||
max-width: 100%;
|
||||
padding-inline: 0.25rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-navigation-bar-item][data-active] {
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
|
||||
/* From 600dp (M3's medium window), icon and label side by side in a 40px indicator, and the
|
||||
items centred with the padding ShortNavigationBar's Centered arrangement computes. The
|
||||
short bar only: a tall bar is the vertical layout at every width, so every rule here that
|
||||
the vertical layout also has an opinion about names the short bar. */
|
||||
@container (width >= 37.5rem) {
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-items] {
|
||||
width: calc(10% * (var(--navigation-bar-count, 7) + 3));
|
||||
min-width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-items]:has(> :last-child:nth-child(1)) { --navigation-bar-count: 1; }
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-items]:has(> :last-child:nth-child(2)) { --navigation-bar-count: 2; }
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-items]:has(> :last-child:nth-child(3)) { --navigation-bar-count: 3; }
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-items]:has(> :last-child:nth-child(4)) { --navigation-bar-count: 4; }
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-items]:has(> :last-child:nth-child(5)) { --navigation-bar-count: 5; }
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-items]:has(> :last-child:nth-child(6)) { --navigation-bar-count: 6; }
|
||||
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item] {
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
/* The label stays label-medium: NavigationBarTokens.LabelTextFont is the bar's only label
|
||||
token, and Compose's ShortNavigationBarItem passes it for both icon positions. (The
|
||||
*rail's* horizontal item is label-large — NavigationRailHorizontalItemTokens — which is
|
||||
a different component.) */
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item] [data-navigation-pill] {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
flex-direction: row;
|
||||
height: 2.5rem;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item] [data-navigation-label] {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item][data-active] {
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item][data-active] [data-navigation-pill] {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- the navigation rail */
|
||||
|
||||
[data-navigation-rail] {
|
||||
--navigation-rail-expanded-width: clamp(13.75rem, var(--navigation-rail-width, 16rem), 22.5rem);
|
||||
--navigation-rail-collapsed-width: 6rem;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: var(--navigation-rail-expanded-width);
|
||||
transition: width var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
|
||||
|
||||
@variant rail-collapsed {
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
}
|
||||
}
|
||||
|
||||
/* M3's other collapsed width: NavigationRailCollapsedTokens.NarrowContainerWidth, 80dp against
|
||||
the default 96. A variable rather than a width here, so the rules below — which each have
|
||||
their own reason to name a collapsed rail's width — do not have to know about it. */
|
||||
[data-navigation-rail][data-width='narrow'] {
|
||||
--navigation-rail-collapsed-width: 5rem;
|
||||
}
|
||||
|
||||
/* "Expanded behavior | Hide when collapsed" (the reference's Navigation rail § Variants and
|
||||
configurations): instead of narrowing, the rail leaves the layout entirely — for an
|
||||
immersive page — and comes back over it when a menu button somewhere else, the app bar's,
|
||||
calls `$store.rail.show()`. It is the *expanded* rail that hides, which is why what comes
|
||||
back is the expanded rail: "collapsed rail may not hide", and the two bands where a window
|
||||
size class rather than the visitor collapses one are given back below. */
|
||||
[data-navigation-rail][data-hide-when-collapsed] {
|
||||
@variant rail-collapsed {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* There is no room for a label under the 56px indicator at 80px, so a narrow rail is the icons
|
||||
alone. The label is taken out of the drawing, not out of the page: it is what names the
|
||||
destination for a screen reader. */
|
||||
[data-navigation-rail][data-width='narrow'] [data-navigation-rail-item] [data-navigation-label] {
|
||||
@variant rail-collapsed {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* A modal rail keeps its collapsed width in the layout while it is open over it, as Compose's
|
||||
ModalWideNavigationRail does; the adaptive rail does below `expanded`, and takes no room on a compact window. */
|
||||
[data-navigation-rail='modal'] {
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
}
|
||||
|
||||
@media (width < 52.5rem) {
|
||||
[data-navigation-rail='adaptive'] {
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 37.5rem) {
|
||||
[data-navigation-rail='adaptive'] {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
/* "Compact → don't use a standard rail, use a navigation bar." A `collapsible` rail takes
|
||||
the width the visitor chose, which on a 360px screen would be two-thirds of it, so it
|
||||
is floored at its collapsed width. `collapsed` and `expanded` mean what they say and are
|
||||
left alone: wrap one in a `medium:` element if it must not show on a phone. */
|
||||
[data-navigation-rail='collapsible'] {
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
}
|
||||
}
|
||||
|
||||
/* `--material-banner` is the height of a bar pinned across the top of the window — what
|
||||
<x-scaffold>'s `banner` slot holds, if the application made it sticky. The rail sticks
|
||||
under it rather than behind it; 0 without one. */
|
||||
[data-navigation-rail-panel] {
|
||||
position: sticky;
|
||||
top: var(--material-banner, 0px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: calc(100dvh - var(--material-banner, 0px));
|
||||
/* Clip, not hide: no scroll container, so the destinations below can still scroll and
|
||||
nothing sticky breaks. What only an expanded rail draws — a label, the brand — is drawn
|
||||
at once when the rail expands, while the width is still growing; the clip keeps it
|
||||
from spilling over the page for those frames. */
|
||||
overflow-x: clip;
|
||||
padding-bottom: var(--material-safe-bottom, env(safe-area-inset-bottom));
|
||||
background-color: var(--md-sys-color-surface);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
transition:
|
||||
width var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast),
|
||||
background-color var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default);
|
||||
}
|
||||
|
||||
/* M3's two optional container treatments: "optional vertical divider separates rail from
|
||||
content, placed on the content-adjacent edge" — also its answer to a page scrolling under a
|
||||
fixed rail — and "container fill can be turned off (transparent) as long as items keep ≥3:1
|
||||
contrast". Neither applies to a rail open over a scrim, which is a surface over the page. */
|
||||
[data-navigation-rail][data-divider]:not([data-open]) > [data-navigation-rail-panel] {
|
||||
border-inline-end: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
[data-navigation-rail][data-fill='false']:not([data-open]) > [data-navigation-rail-panel] {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Open: expanded over a scrim, in surface-container with a large corner at its inner edge. */
|
||||
[data-navigation-rail][data-open] > [data-navigation-rail-panel] {
|
||||
position: fixed;
|
||||
inset-block: 0;
|
||||
inset-inline-start: 0;
|
||||
z-index: 50;
|
||||
width: var(--navigation-rail-expanded-width);
|
||||
max-width: calc(100vw - 3.5rem);
|
||||
height: 100dvh;
|
||||
max-height: none;
|
||||
border-start-end-radius: var(--md-sys-shape-corner-lg);
|
||||
border-end-end-radius: var(--md-sys-shape-corner-lg);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
}
|
||||
|
||||
/* On a compact window there is no collapsed rail to grow out of: the open rail slides in from the edge,
|
||||
on emphasized decelerate rather than a spring, which would overshoot and open a gap. */
|
||||
@media (width < 37.5rem) {
|
||||
[data-navigation-rail='adaptive'] > [data-navigation-rail-panel] {
|
||||
position: fixed;
|
||||
inset-block: 0;
|
||||
inset-inline-start: 0;
|
||||
z-index: 50;
|
||||
display: none;
|
||||
width: var(--navigation-rail-expanded-width);
|
||||
max-width: calc(100vw - 3.5rem);
|
||||
height: 100dvh;
|
||||
max-height: none;
|
||||
border-start-end-radius: var(--md-sys-shape-corner-lg);
|
||||
border-end-end-radius: var(--md-sys-shape-corner-lg);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
translate: -100% 0;
|
||||
transition:
|
||||
translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate),
|
||||
display var(--md-sys-motion-effects-default-duration) allow-discrete;
|
||||
|
||||
&:dir(rtl) {
|
||||
translate: 100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail='adaptive'][data-open] > [data-navigation-rail-panel] {
|
||||
display: flex;
|
||||
translate: 0 0;
|
||||
transition:
|
||||
translate var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-easing-emphasized-decelerate),
|
||||
display var(--md-sys-motion-spatial-default-duration) allow-discrete;
|
||||
|
||||
@starting-style {
|
||||
translate: -100% 0;
|
||||
}
|
||||
|
||||
&:dir(rtl) {
|
||||
translate: 0 0;
|
||||
|
||||
@starting-style {
|
||||
translate: 100% 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* A rail that hides when collapsed is in the same position: nothing is left in the layout for
|
||||
it to grow out of, so it takes the compact window's slide-in. The whole treatment is under
|
||||
the `rail-collapsed` variant, which stops the moment the rail is open — that is what makes
|
||||
it the expanded rail that comes back, on the rule above. */
|
||||
[data-navigation-rail][data-hide-when-collapsed] > [data-navigation-rail-panel] {
|
||||
@variant rail-collapsed {
|
||||
display: none;
|
||||
translate: -100% 0;
|
||||
transition:
|
||||
translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate),
|
||||
display var(--md-sys-motion-effects-default-duration) allow-discrete;
|
||||
|
||||
&:dir(rtl) {
|
||||
translate: 100% 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail][data-hide-when-collapsed][data-open] > [data-navigation-rail-panel] {
|
||||
display: flex;
|
||||
translate: 0 0;
|
||||
transition:
|
||||
translate var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-easing-emphasized-decelerate),
|
||||
display var(--md-sys-motion-spatial-default-duration) allow-discrete;
|
||||
|
||||
@starting-style {
|
||||
translate: -100% 0;
|
||||
}
|
||||
|
||||
&:dir(rtl) {
|
||||
translate: 0 0;
|
||||
|
||||
@starting-style {
|
||||
translate: 100% 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* The two bands the configuration does not reach, because there it is the window and not the
|
||||
visitor that collapses the rail: below `medium` for a collapsible rail — where a rail is
|
||||
held at its collapsed width anyway — and `medium` itself for the adaptive one, which M3
|
||||
gives a collapsed rail. (Below `medium` the adaptive rail is already away, over the page,
|
||||
and the block above it says so.) resources/js/navigation.js reads the same two numbers, so
|
||||
the menu button and the drawing agree at every width. */
|
||||
@media (width < 37.5rem) {
|
||||
[data-navigation-rail='collapsible'][data-hide-when-collapsed] {
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
}
|
||||
|
||||
[data-navigation-rail='collapsible'][data-hide-when-collapsed] > [data-navigation-rail-panel] {
|
||||
display: flex;
|
||||
translate: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (37.5rem <= width < 52.5rem) {
|
||||
[data-navigation-rail='adaptive'][data-hide-when-collapsed] {
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
}
|
||||
|
||||
[data-navigation-rail='adaptive'][data-hide-when-collapsed] > [data-navigation-rail-panel] {
|
||||
display: flex;
|
||||
translate: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-scrim] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
display: none;
|
||||
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default),
|
||||
display var(--md-sys-motion-effects-default-duration) allow-discrete;
|
||||
}
|
||||
|
||||
[data-navigation-rail][data-open] > [data-navigation-rail-scrim] {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 44px above the header (TopSpace), 40px under it (HeaderSpaceMinimum) — 32 here and 8 as
|
||||
the destinations' own padding, which keeps the first item's focus ring inside the scroller. */
|
||||
[data-navigation-rail-header] {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding-top: calc(var(--material-safe-top, env(safe-area-inset-top)) + 2.75rem);
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
[data-navigation-rail-panel] > [data-navigation-rail-destinations]:first-child {
|
||||
padding-top: calc(var(--material-safe-top, env(safe-area-inset-top)) + 2.75rem);
|
||||
}
|
||||
|
||||
[data-navigation-rail-destinations] {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-block: 0.5rem;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
[data-navigation-rail-destinations],
|
||||
[data-navigation-rail-section],
|
||||
[data-navigation-rail-footer] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@variant rail-collapsed {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-footer] {
|
||||
flex-shrink: 0;
|
||||
padding-block: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
/* "Alignment: top or center; on tablets prefer center alignment for reach. Menu icon and FAB
|
||||
are always top-aligned" (docs/reference/m3/components-navigation-selection-inputs.md
|
||||
§ Navigation rail). Only the destinations move, then — the header and the footer stay at
|
||||
their ends of the rail — and `safe` puts them back at the top the moment there are more of
|
||||
them than fit, where centring would otherwise push the first one out of reach above the
|
||||
scroller. */
|
||||
[data-navigation-rail][data-align='center'] [data-navigation-rail-destinations] {
|
||||
justify-content: safe center;
|
||||
}
|
||||
|
||||
[data-navigation-rail-heading] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 3rem;
|
||||
padding-inline: 2.25rem 1.25rem;
|
||||
overflow: hidden;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-title-sm);
|
||||
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@variant rail-collapsed {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-section]:not(:first-child) {
|
||||
@variant rail-collapsed {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Expanded: a 56px full-width pill, icon and label 8px apart, label-large. */
|
||||
[data-navigation-rail-item] {
|
||||
--navigation-layer: 0;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
height: 3.5rem;
|
||||
margin-inline: 1.25rem;
|
||||
padding-inline: 1rem;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
text-align: start;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
/* Collapsed: the icon in its 56×32 indicator over a label-medium label, 64px tall. */
|
||||
@variant rail-collapsed {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
height: auto;
|
||||
min-height: 4rem;
|
||||
margin-inline: 0;
|
||||
padding: 0.375rem 0.25rem;
|
||||
border-radius: 0;
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-item] [data-navigation-label] {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@variant rail-collapsed {
|
||||
flex: none;
|
||||
display: -webkit-box;
|
||||
max-width: 100%;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-item][data-active] {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
|
||||
@variant rail-collapsed {
|
||||
background-image: none;
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-item] [data-navigation-indicator] {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
|
||||
@variant rail-collapsed {
|
||||
width: 3.5rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- both: indicator, states */
|
||||
|
||||
[data-navigation-bar-item] [data-navigation-indicator] {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3.5rem;
|
||||
height: 2rem;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
/* The vertical layout — a compact bar, and a tall one at any width — fills the 56×32 box
|
||||
behind the icon; the horizontal layout fills the pill around icon and label instead, so it
|
||||
takes the fill off the box again. */
|
||||
[data-navigation-bar-item][data-active] [data-navigation-indicator] {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
@container (width >= 37.5rem) {
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item][data-active] [data-navigation-indicator] {
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-item][data-active] [data-navigation-indicator] {
|
||||
@variant rail-collapsed {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
}
|
||||
|
||||
/* The active indicator is a secondary-container fill painted as a background image, so it can
|
||||
grow out of its centre without stretching the icon: when a page arrives through
|
||||
wire:navigate, resources/js/navigation.js starts it at zero width for a moment, and it
|
||||
springs open on the default spatial spring, as Compose's indicator does when the selection
|
||||
changes (NavigationItem.kt). A full page load draws it at once; reduced motion zeroes it. */
|
||||
[data-navigation-bar-item] :is([data-navigation-indicator], [data-navigation-pill]),
|
||||
[data-navigation-rail-item],
|
||||
[data-navigation-rail-item] [data-navigation-indicator] {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
transition: background-size var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
|
||||
}
|
||||
|
||||
/* The state layer covers only the indicator: the pill when the indicator holds the label
|
||||
too, the 56×32 shape when it holds the icon alone. Hover only where a pointer can hover. */
|
||||
@media (hover: hover) {
|
||||
:is([data-navigation-bar-item], [data-navigation-rail-item]):hover {
|
||||
--navigation-layer: 0.08;
|
||||
}
|
||||
}
|
||||
|
||||
:is([data-navigation-bar-item], [data-navigation-rail-item]):is(:focus-visible, :active) {
|
||||
--navigation-layer: 0.1;
|
||||
}
|
||||
|
||||
[data-navigation-bar-item] [data-navigation-indicator]::before,
|
||||
[data-navigation-bar-item] [data-navigation-pill]::before,
|
||||
[data-navigation-rail-item] [data-navigation-indicator]::before,
|
||||
[data-navigation-rail-item]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
border-radius: inherit;
|
||||
background-color: var(--md-sys-color-on-surface);
|
||||
opacity: var(--navigation-layer);
|
||||
pointer-events: none;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
/* A navigation item's state layer is on-secondary-container, active or not: the only
|
||||
state-layer tokens M3 states for one are NavigationRailColorTokens', which give that colour
|
||||
to all six. The rail follows them two blocks below; the bar follows them here, over its
|
||||
secondary-container pill. */
|
||||
[data-navigation-bar-item] :is([data-navigation-indicator], [data-navigation-pill])::before {
|
||||
background-color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-navigation-bar-item] [data-navigation-pill]::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@container (width >= 37.5rem) {
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item] [data-navigation-pill]::before {
|
||||
display: block;
|
||||
}
|
||||
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item] [data-navigation-indicator]::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* The rail item's layer is on the item while expanded and on its indicator while collapsed. A
|
||||
variant cannot follow a pseudo-element in a selector, so the item hands the layer on through
|
||||
variables; each rule below outweighs its line in the list above. */
|
||||
[data-navigation-rail-item] {
|
||||
--navigation-item-layer: var(--navigation-layer);
|
||||
--navigation-indicator-layer: 0;
|
||||
|
||||
@variant rail-collapsed {
|
||||
--navigation-item-layer: 0;
|
||||
--navigation-indicator-layer: var(--navigation-layer);
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-item][data-navigation-rail-item]::before {
|
||||
background-color: var(--md-sys-color-on-secondary-container);
|
||||
opacity: var(--navigation-item-layer);
|
||||
}
|
||||
|
||||
[data-navigation-rail-item][data-navigation-rail-item] [data-navigation-indicator]::before {
|
||||
background-color: var(--md-sys-color-on-secondary-container);
|
||||
opacity: var(--navigation-indicator-layer);
|
||||
}
|
||||
|
||||
/* M3's focus indicator, 3px of secondary 2px out, around the same shape. */
|
||||
[data-navigation-bar-item]:focus-visible [data-navigation-indicator] {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@container (width >= 37.5rem) {
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item]:focus-visible [data-navigation-indicator] {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item]:focus-visible [data-navigation-pill] {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-item]:focus-visible {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
|
||||
@variant rail-collapsed {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-item]:focus-visible [data-navigation-indicator] {
|
||||
@variant rail-collapsed {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* A bar that has slid off the bottom of the window is no longer there to clear, so the offset
|
||||
* everything pinned to the bottom reads drops to the bottom safe area and whatever the application
|
||||
* has docked on the bar — a `fab` button, the snackbar and the page's own bottom padding all follow
|
||||
* it down and come back up with the bar. Unlayered on purpose: <x-scaffold> publishes the variable
|
||||
* with a utility, and a rule in any layer loses to one. */
|
||||
[data-app-shell]:has([data-navigation-bar][data-hide-on-scroll][data-hidden]) {
|
||||
--material-bottom-bar: calc(var(--material-safe-bottom, env(safe-area-inset-bottom)) + var(--material-bottom-extra, 0px));
|
||||
}
|
||||
|
||||
/* A FAB in the rail's header rests at elevation 0, not the 3 a standalone FAB has
|
||||
* (docs/reference/m3/components-navigation-selection-inputs.md § Navigation rail: "when nested
|
||||
* within another component, such as the navigation rail, the FAB's resting elevation should be
|
||||
* level 0"). Unlayered on purpose: the FAB draws its shadow with a utility, and a rule in any
|
||||
* layer loses to a utility — the same reason toolbar.css gives for its vibrant recolouring. */
|
||||
[data-navigation-rail-header] [data-md-fab],
|
||||
[data-navigation-rail-header] [data-md-fab]:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* "Rail transitions from collapsed to expanded should animate the FAB into and out of an extended
|
||||
* FAB" — so the header takes one <x-fab label icon> and morphs it, rather than swapping two by
|
||||
* `display`, which showed the collapsed one at once while the rail's width was still springing.
|
||||
* The label's width springs on the rail's own spatial spring; with the gap closed and the extended
|
||||
* FAB's own minimum width off, `aspect-ratio` squares what is left against the height its size
|
||||
* already sets — 56, 80 or 96px, the three FAB sizes — so no number is repeated here. The label
|
||||
* stays in the accessibility tree at both widths, so the FAB keeps its name. Unlayered for the
|
||||
* same reason as the rule above: `gap-2` and `min-w-20` are utilities, and a layer would lose to
|
||||
* them. The cap is there because a transition needs a length to travel to; a rail FAB's label is a
|
||||
* word or two. */
|
||||
[data-navigation-rail-header] [data-md-fab] > span {
|
||||
max-width: 16rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
max-width var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default),
|
||||
opacity var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default);
|
||||
|
||||
@variant rail-collapsed {
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-navigation-rail-header] [data-md-fab] {
|
||||
@variant rail-collapsed {
|
||||
min-width: 0;
|
||||
aspect-ratio: 1;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,3 @@
|
||||
|
||||
@import './tokens/theme.css';
|
||||
@import './tokens/utilities.css';
|
||||
|
||||
/* Navigation — leaving in step 36 */
|
||||
@import './components/navigation.css';
|
||||
|
||||
@@ -73,7 +73,7 @@ it('previews a profile from the picker and the showcase menu, and keeps it throu
|
||||
->click('[data-scheme-preview="teal"]')
|
||||
->assertScript("document.documentElement.getAttribute('data-scheme') === 'teal'");
|
||||
|
||||
$page->click('[data-navigation-rail-panel] a[href$="/material/buttons"]')
|
||||
$page->click('[data-md-navigation-rail-panel] a[href$="/material/buttons"]')
|
||||
->assertScript("location.pathname.endsWith('/material/buttons')")
|
||||
->assertScript("document.documentElement.getAttribute('data-scheme') === 'teal'")
|
||||
->assertNoJavaScriptErrors();
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
const RAIL = "document.querySelector('[data-navigation-rail]')";
|
||||
const RAIL_PANEL = "document.querySelector('[data-navigation-rail-panel]')";
|
||||
const RAIL = "document.querySelector('[data-md-navigation-rail]')";
|
||||
const RAIL_PANEL = "document.querySelector('[data-md-navigation-rail-panel]')";
|
||||
const BOTTOM_BAR = "document.querySelector('[data-app-shell-bar]')";
|
||||
|
||||
function navigationReady(mixed $page): mixed
|
||||
@@ -20,7 +20,7 @@ function shellPage(int $width = 1280, int $height = 900, string $path = '/materi
|
||||
// of its queries are M3 window size classes: `expanded` (840px) says the rail is standard
|
||||
// rather than modal, `large` (1200px) that it starts expanded rather than collapsed.
|
||||
return navigationReady(visit($path)->resize($width, $height))
|
||||
->assertScript("window.eval(\"(() => { const rail = document.querySelector('[data-navigation-rail]'); if (! rail || rail.dataset.navigationRail !== 'adaptive') return true; const state = Alpine.\$data(rail); return state.wide === window.matchMedia('(width >= 840px)').matches && state.roomy === window.matchMedia('(width >= 1200px)').matches; })()\")");
|
||||
->assertScript("window.eval(\"(() => { const rail = document.querySelector('[data-md-navigation-rail]'); if (! rail || rail.dataset.mdNavigationRail !== 'adaptive') return true; const state = Alpine.\$data(rail); return state.wide === window.matchMedia('(width >= 840px)').matches && state.roomy === window.matchMedia('(width >= 1200px)').matches; })()\")");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +46,7 @@ function firstPaintProbe(): void
|
||||
<script>
|
||||
window.firstPaint = {
|
||||
rail: document.documentElement.getAttribute('data-rail'),
|
||||
width: getComputedStyle(document.querySelector('[data-navigation-rail]')).width,
|
||||
width: getComputedStyle(document.querySelector('[data-md-navigation-rail]')).width,
|
||||
alpine: typeof window.Alpine,
|
||||
};
|
||||
</script>
|
||||
@@ -67,10 +67,10 @@ it('shows the navigation bar on a compact window and marks the current destinati
|
||||
shellPage(599, 860)
|
||||
->assertScript('getComputedStyle('.BOTTOM_BAR.").display !== 'none'")
|
||||
->assertScript('getComputedStyle('.RAIL_PANEL.").display === 'none'")
|
||||
->assertScript(BOTTOM_BAR.".querySelectorAll('[data-navigation-bar-item]').length === 4")
|
||||
->assertScript(BOTTOM_BAR.".querySelectorAll('[data-md-navigation-bar-item]').length === 4")
|
||||
->assertScript(BOTTOM_BAR.".querySelector('[aria-current=\"page\"]').textContent.includes('Inbox')")
|
||||
->assertScript('getComputedStyle('.BOTTOM_BAR.".querySelector('[aria-current=\"page\"] [data-navigation-indicator]')).backgroundImage !== 'none'")
|
||||
->assertScript('getComputedStyle('.BOTTOM_BAR.".querySelector('[data-navigation-bar-item]:not([aria-current]) [data-navigation-indicator]')).backgroundImage === 'none'")
|
||||
->assertScript('getComputedStyle('.BOTTOM_BAR.".querySelector('[aria-current=\"page\"] [data-md-navigation-indicator]')).backgroundImage !== 'none'")
|
||||
->assertScript('getComputedStyle('.BOTTOM_BAR.".querySelector('[data-md-navigation-bar-item]:not([aria-current]) [data-md-navigation-indicator]')).backgroundImage === 'none'")
|
||||
->assertNoJavaScriptErrors();
|
||||
});
|
||||
|
||||
@@ -83,9 +83,9 @@ it('collapses the rail across the medium class, icon over label, from its first
|
||||
shellPage(839)
|
||||
->assertScript(railWidth(96))
|
||||
->assertScript('getComputedStyle('.BOTTOM_BAR.").display === 'none'")
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-navigation-rail-item]')).flexDirection === 'column'")
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-navigation-rail-heading]')).display === 'none'")
|
||||
->assertAttribute('[data-navigation-rail-menu]', 'aria-expanded', 'false');
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-md-navigation-rail-item]')).flexDirection === 'column'")
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-md-navigation-rail-heading]')).display === 'none'")
|
||||
->assertAttribute('[data-md-navigation-rail-menu]', 'aria-expanded', 'false');
|
||||
});
|
||||
|
||||
it('starts the rail collapsed across the expanded class and expands it in the layout', function () {
|
||||
@@ -100,17 +100,17 @@ it('starts the rail collapsed across the expanded class and expands it in the la
|
||||
$page = shellPage(840)
|
||||
->assertScript(railWidth(96))
|
||||
->assertScript('getComputedStyle('.BOTTOM_BAR.").display === 'none'")
|
||||
->assertAttribute('[data-navigation-rail-menu]', 'aria-expanded', 'false');
|
||||
->assertAttribute('[data-md-navigation-rail-menu]', 'aria-expanded', 'false');
|
||||
|
||||
$page->click('[data-navigation-rail-menu]')
|
||||
$page->click('[data-md-navigation-rail-menu]')
|
||||
->assertScript(railWidth(256))
|
||||
->assertScript('! '.RAIL.".hasAttribute('data-open')")
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-navigation-rail-scrim]')).display === 'none'")
|
||||
->assertScript('! '.RAIL.".hasAttribute('data-md-open')")
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-md-navigation-rail-scrim]')).display === 'none'")
|
||||
->assertScript("document.getElementById('content').closest('[aria-hidden=\"true\"]') === null")
|
||||
->assertScript("document.documentElement.getAttribute('data-rail') === 'expanded'")
|
||||
->assertScript("! document.documentElement.hasAttribute('data-rail-auto')")
|
||||
->assertScript("localStorage.getItem('material-rail') === 'expanded'")
|
||||
->assertAttribute('[data-navigation-rail-menu]', 'aria-expanded', 'true')
|
||||
->assertAttribute('[data-md-navigation-rail-menu]', 'aria-expanded', 'true')
|
||||
->assertNoJavaScriptErrors();
|
||||
|
||||
// The choice outlives the reload, and holds to the top of the class.
|
||||
@@ -126,12 +126,12 @@ it('expands the rail from large, and keeps it collapsed across a reload from the
|
||||
$page = shellPage(1200, 900, '/rail-probe')
|
||||
->assertScript(railWidth(256))
|
||||
->assertScript("window.eval('window.firstPaint.width') === '256px'")
|
||||
->assertAttribute('[data-navigation-rail-menu]', 'aria-expanded', 'true');
|
||||
->assertAttribute('[data-md-navigation-rail-menu]', 'aria-expanded', 'true');
|
||||
|
||||
$page->click('[data-navigation-rail-menu]')
|
||||
$page->click('[data-md-navigation-rail-menu]')
|
||||
->assertScript("document.documentElement.getAttribute('data-rail') === 'collapsed'")
|
||||
->assertScript("localStorage.getItem('material-rail') === 'collapsed'")
|
||||
->assertAttribute('[data-navigation-rail-menu]', 'aria-expanded', 'false')
|
||||
->assertAttribute('[data-md-navigation-rail-menu]', 'aria-expanded', 'false')
|
||||
->assertScript(railWidth(96));
|
||||
|
||||
navigationReady($page->refresh())
|
||||
@@ -152,7 +152,7 @@ it('expands the rail by default at extra-large', function () {
|
||||
shellPage(1600, 900)
|
||||
->assertScript(railWidth(256))
|
||||
->assertScript('getComputedStyle('.BOTTOM_BAR.").display === 'none'")
|
||||
->assertAttribute('[data-navigation-rail-menu]', 'aria-expanded', 'true')
|
||||
->assertAttribute('[data-md-navigation-rail-menu]', 'aria-expanded', 'true')
|
||||
->assertNoJavaScriptErrors();
|
||||
});
|
||||
|
||||
@@ -160,26 +160,26 @@ it('opens the modal rail from its menu button below expanded, holding focus unti
|
||||
$page = shellPage(839);
|
||||
|
||||
// From the keyboard: WebKit neither focuses a button on click nor returns focus to one that never had it.
|
||||
$page->script("document.querySelector('[data-navigation-rail-menu]').focus()");
|
||||
$page->keys('[data-navigation-rail-menu]', 'Enter')
|
||||
->assertScript(RAIL.".hasAttribute('data-open')")
|
||||
$page->script("document.querySelector('[data-md-navigation-rail-menu]').focus()");
|
||||
$page->keys('[data-md-navigation-rail-menu]', 'Enter')
|
||||
->assertScript(RAIL.".hasAttribute('data-md-open')")
|
||||
->assertScript('Math.round('.RAIL_PANEL.'.getBoundingClientRect().width) === 256')
|
||||
->assertScript(railWidth(96))
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-navigation-rail-heading]')).display !== 'none'")
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-md-navigation-rail-heading]')).display !== 'none'")
|
||||
->assertScript(RAIL_PANEL.'.contains(document.activeElement)')
|
||||
->assertScript("document.getElementById('content').closest('[aria-hidden=\"true\"]') !== null")
|
||||
->assertAttribute('[data-navigation-rail-menu]', 'aria-expanded', 'true');
|
||||
->assertAttribute('[data-md-navigation-rail-menu]', 'aria-expanded', 'true');
|
||||
|
||||
$page->keys(':focus', 'Escape')
|
||||
->assertScript('! '.RAIL.".hasAttribute('data-open')")
|
||||
->assertScript('! '.RAIL.".hasAttribute('data-md-open')")
|
||||
->assertScript("document.getElementById('content').closest('[aria-hidden=\"true\"]') === null")
|
||||
->assertScript("document.activeElement === document.querySelector('[data-navigation-rail-menu]')");
|
||||
->assertScript("document.activeElement === document.querySelector('[data-md-navigation-rail-menu]')");
|
||||
|
||||
$page->keys('[data-navigation-rail-menu]', 'Enter')
|
||||
->assertScript(RAIL.".hasAttribute('data-open')");
|
||||
$page->keys('[data-md-navigation-rail-menu]', 'Enter')
|
||||
->assertScript(RAIL.".hasAttribute('data-md-open')");
|
||||
|
||||
$page->click('[data-navigation-rail-scrim]')
|
||||
->assertScript('! '.RAIL.".hasAttribute('data-open')")
|
||||
$page->click('[data-md-navigation-rail-scrim]')
|
||||
->assertScript('! '.RAIL.".hasAttribute('data-md-open')")
|
||||
->assertScript("localStorage.getItem('material-rail') === null");
|
||||
});
|
||||
|
||||
@@ -187,32 +187,32 @@ it('slides the modal rail in on a compact window from the app bar\'s menu button
|
||||
$page = shellPage(599, 860);
|
||||
|
||||
$page->click('@shell-menu')
|
||||
->assertScript(RAIL.".hasAttribute('data-open')")
|
||||
->assertScript(RAIL.".hasAttribute('data-md-open')")
|
||||
->assertScript('getComputedStyle('.RAIL_PANEL.").display === 'flex'")
|
||||
->assertScript('Math.round('.RAIL_PANEL.'.getBoundingClientRect().left) === 0')
|
||||
->assertScript(RAIL_PANEL.'.contains(document.activeElement)');
|
||||
|
||||
$page->keys(':focus', 'Escape')
|
||||
->assertScript('! '.RAIL.".hasAttribute('data-open')")
|
||||
->assertScript('! '.RAIL.".hasAttribute('data-md-open')")
|
||||
->assertScript('getComputedStyle('.RAIL_PANEL.").display === 'none'");
|
||||
});
|
||||
|
||||
it('keeps the rail and the theme through wire:navigate and moves aria-current', function () {
|
||||
$page = shellPage(1200);
|
||||
|
||||
$page->click('[data-navigation-rail-menu]')
|
||||
$page->click('[data-md-navigation-rail-menu]')
|
||||
->assertScript("document.documentElement.getAttribute('data-rail') === 'collapsed'");
|
||||
|
||||
$page->script("window.eval(\"Alpine.store('theme').set('dark'); window.samePage = true\")");
|
||||
|
||||
$page->click('[data-navigation-rail-panel] a[href$="/material/shell/starred"]')
|
||||
$page->click('[data-md-navigation-rail-panel] a[href$="/material/shell/starred"]')
|
||||
->assertSeeIn('@shell-page', 'This is the starred page.')
|
||||
->assertScript("window.eval('window.samePage') === true")
|
||||
->assertScript("document.documentElement.getAttribute('data-rail') === 'collapsed'")
|
||||
->assertScript("document.documentElement.getAttribute('data-theme') === 'dark'")
|
||||
->assertScript(railWidth(96))
|
||||
->assertScript("document.querySelector('[data-navigation-rail-panel] [aria-current=\"page\"]').getAttribute('href').endsWith('/material/shell/starred')")
|
||||
->assertScript("document.querySelectorAll('[data-navigation-rail-panel] [aria-current=\"page\"]').length === 1")
|
||||
->assertScript("document.querySelector('[data-md-navigation-rail-panel] [aria-current=\"page\"]').getAttribute('href').endsWith('/material/shell/starred')")
|
||||
->assertScript("document.querySelectorAll('[data-md-navigation-rail-panel] [aria-current=\"page\"]').length === 1")
|
||||
->assertNoJavaScriptErrors();
|
||||
});
|
||||
|
||||
@@ -258,3 +258,114 @@ it('lifts the snackbar and the page above something docked on the phone\'s bar',
|
||||
$page->assertScript("{$snackbarBottom} === 120")
|
||||
->assertScript("{$contentPadding} === 104");
|
||||
});
|
||||
|
||||
/**
|
||||
* A page for the navigation bar and rail props the shell's own adaptive rail never exercises:
|
||||
* `tall`, `hide-on-scroll` (with a pinned snackbar), a narrow centred rail, and a rail that hides
|
||||
* when collapsed. A wide, fixed viewport throughout, so none of M3's breakpoints interfere with
|
||||
* what each example's own props already decide (docs/plans/material-3-browser-tests.md § Navigation).
|
||||
*/
|
||||
function navigationExtrasProbe(): mixed
|
||||
{
|
||||
Route::middleware('web')->get('/navigation-extras-probe', fn () => Blade::render(<<<'BLADE'
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<x-theme-script />
|
||||
@vite(config('livewire-material.showcase.vite'))
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body>
|
||||
<div id="tall-bar">
|
||||
<x-navigation-bar tall>
|
||||
<x-navigation-bar-item label="Inbox" icon="inbox" link="#a" no-wire-navigate active />
|
||||
<x-navigation-bar-item label="Sent" icon="send" link="#b" no-wire-navigate />
|
||||
</x-navigation-bar>
|
||||
</div>
|
||||
|
||||
<div id="hide-bar">
|
||||
<x-navigation-bar hide-on-scroll>
|
||||
<x-navigation-bar-item label="Inbox" icon="inbox" link="#a" no-wire-navigate active />
|
||||
<x-navigation-bar-item label="Sent" icon="send" link="#b" no-wire-navigate />
|
||||
</x-navigation-bar>
|
||||
</div>
|
||||
|
||||
<div id="narrow-rail" style="display: flex">
|
||||
<x-navigation-rail mode="collapsed" width="narrow" align="center">
|
||||
<x-navigation-rail-item label="Inbox" icon="inbox" link="#a" no-wire-navigate active />
|
||||
<x-navigation-rail-item label="Starred" icon="star" link="#b" no-wire-navigate />
|
||||
<x-navigation-rail-item label="Sent" icon="send" link="#c" no-wire-navigate />
|
||||
</x-navigation-rail>
|
||||
</div>
|
||||
|
||||
<div id="hiding-rail" style="display: flex">
|
||||
<x-navigation-rail hide-when-collapsed>
|
||||
<x-navigation-rail-item label="Canvas" icon="brush" link="#a" no-wire-navigate active />
|
||||
<x-navigation-rail-item label="Layers" icon="layers" link="#b" no-wire-navigate />
|
||||
</x-navigation-rail>
|
||||
<button type="button" id="open-hiding-rail" x-data x-on:click="$store.rail.show()">Open navigation</button>
|
||||
</div>
|
||||
|
||||
<div style="height: 250vh"></div>
|
||||
|
||||
<x-toast />
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
BLADE));
|
||||
|
||||
return navigationReady(visit('/navigation-extras-probe')->resize(1000, 900));
|
||||
}
|
||||
|
||||
it('draws the tall navigation bar, icon over label at every width', function () {
|
||||
$page = navigationExtrasProbe();
|
||||
|
||||
$page->assertScript("Math.round(document.querySelector('#tall-bar [data-md-navigation-bar-items]').getBoundingClientRect().height) === 80")
|
||||
->assertScript("getComputedStyle(document.querySelector('#tall-bar [data-md-navigation-pill]')).flexDirection === 'column'")
|
||||
->assertNoJavaScriptErrors();
|
||||
});
|
||||
|
||||
it('hides the bar on a scroll down and never while a pinned snackbar is on screen', function () {
|
||||
$page = navigationExtrasProbe();
|
||||
$hidden = "document.querySelector('#hide-bar [data-md-navigation-bar]').hasAttribute('data-md-hidden')";
|
||||
|
||||
$page->script('window.scrollTo(0, 400)');
|
||||
$page->assertScript("{$hidden} === true")
|
||||
->assertNoJavaScriptErrors();
|
||||
|
||||
// Back to the top brings it back, as a fresh scroll-down would; pin a snackbar first.
|
||||
$page->script('window.scrollTo(0, 0)');
|
||||
$page->assertScript("{$hidden} === false");
|
||||
|
||||
$page->script("materialToast('Pinned', { sticky: true })")
|
||||
->assertScript("document.querySelector('[data-md-toast-snackbar]')?.textContent.includes('Pinned')");
|
||||
|
||||
$page->script('window.scrollTo(0, 400)');
|
||||
$page->assertScript("{$hidden} === false")
|
||||
->assertNoJavaScriptErrors();
|
||||
});
|
||||
|
||||
it('draws the narrow collapsed width and centres the destinations', function () {
|
||||
$page = navigationExtrasProbe();
|
||||
|
||||
$page->assertScript("Math.round(document.querySelector('#narrow-rail [data-md-navigation-rail]').getBoundingClientRect().width) === 80")
|
||||
->assertScript("getComputedStyle(document.querySelector('#narrow-rail [data-md-navigation-rail-destinations]')).justifyContent === 'safe center'")
|
||||
->assertNoJavaScriptErrors();
|
||||
});
|
||||
|
||||
it('takes a rail out of the layout entirely when it hides collapsed, and back over the page', function () {
|
||||
$page = navigationExtrasProbe();
|
||||
$rail = "document.querySelector('#hiding-rail [data-md-navigation-rail]')";
|
||||
$railWidth = "Math.round({$rail}.getBoundingClientRect().width)";
|
||||
|
||||
$page->assertScript("{$railWidth} > 200")
|
||||
->click('#hiding-rail [data-md-navigation-rail-menu]')
|
||||
->assertScript("{$railWidth} === 0")
|
||||
->assertScript("! {$rail}.hasAttribute('data-md-open')");
|
||||
|
||||
// The only way back: an application's own menu button calling $store.rail.show().
|
||||
$page->click('#open-hiding-rail')
|
||||
->assertScript("{$rail}.hasAttribute('data-md-open')")
|
||||
->assertScript("getComputedStyle(document.querySelector('#hiding-rail [data-md-navigation-rail-panel]')).display === 'flex'")
|
||||
->assertNoJavaScriptErrors();
|
||||
});
|
||||
|
||||
@@ -10,9 +10,9 @@ it('moves between sections through the rail and the next-section link, keeping t
|
||||
$page = visit('/material')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
|
||||
$page->click('[data-navigation-rail-panel] a[href$="/material/buttons"]')
|
||||
$page->click('[data-md-navigation-rail-panel] a[href$="/material/buttons"]')
|
||||
->assertSee('Variants')
|
||||
->assertScript("document.querySelector('[data-navigation-rail-panel] [aria-current=\"page\"]').getAttribute('href').endsWith('/material/buttons')")
|
||||
->assertScript("document.querySelector('[data-md-navigation-rail-panel] [aria-current=\"page\"]').getAttribute('href').endsWith('/material/buttons')")
|
||||
->assertScript("document.title === 'Buttons · Livewire Material'");
|
||||
|
||||
$page->click('[data-test="next-section"]')
|
||||
|
||||
@@ -19,27 +19,27 @@ it('draws the adaptive rail, the bar, the content region and the snackbar host',
|
||||
$html = (string) $this->blade('<x-scaffold :destinations="$destinations">The page</x-scaffold>', ['destinations' => scaffoldDestinations()]);
|
||||
|
||||
expect($html)
|
||||
->toContain('data-navigation-rail="adaptive"')
|
||||
->toContain('data-navigation-bar')
|
||||
->toContain('data-md-navigation-rail="adaptive"')
|
||||
->toContain('data-md-navigation-bar')
|
||||
->toContain('<main id="content" tabindex="-1" wire:transition.navigate')
|
||||
->toContain('max-expanded:overflow-x-clip')
|
||||
->toContain('The page')
|
||||
->toContain('x-data="materialSnackbar"')
|
||||
->toMatch('/<a\s+href="#content"\s+data-skip-link/')
|
||||
->and(strpos($html, 'data-skip-link'))->toBeLessThan(strpos($html, 'data-navigation-rail='));
|
||||
->and(strpos($html, 'data-skip-link'))->toBeLessThan(strpos($html, 'data-md-navigation-rail='));
|
||||
});
|
||||
|
||||
it('puts every destination in the rail and only those marked for the bar in the bar', function () {
|
||||
$html = (string) $this->blade('<x-scaffold :destinations="$destinations" />', ['destinations' => scaffoldDestinations()]);
|
||||
|
||||
$rail = substr($html, strpos($html, 'data-navigation-rail='), strpos($html, 'data-app-shell-bar') - strpos($html, 'data-navigation-rail='));
|
||||
$rail = substr($html, strpos($html, 'data-md-navigation-rail='), strpos($html, 'data-app-shell-bar') - strpos($html, 'data-md-navigation-rail='));
|
||||
$bar = substr($html, strpos($html, 'data-app-shell-bar'));
|
||||
|
||||
expect(substr_count($rail, '<a data-navigation-rail-item'))->toBe(4)
|
||||
->and(substr_count($bar, '<a data-navigation-bar-item'))->toBe(2)
|
||||
expect(substr_count($rail, '<a data-md-navigation-rail-item'))->toBe(4)
|
||||
->and(substr_count($bar, '<a data-md-navigation-bar-item'))->toBe(2)
|
||||
->and($bar)->toContain('Inbox')->toContain('Sent')->not->toContain('Travel')
|
||||
->and($rail)->toContain('data-navigation-rail-heading>Labels</p>')
|
||||
->and(substr_count($rail, 'data-navigation-rail-section'))->toBe(1)
|
||||
->and($rail)->toContain('data-md-navigation-rail-heading>Labels</p>')
|
||||
->and(substr_count($rail, 'data-md-navigation-rail-section'))->toBe(1)
|
||||
->and($rail)->toMatch('/href="\/receipts"(?![^>]*wire:navigate)/')
|
||||
->and(substr_count($html, 'aria-current="page"'))->toBe(2);
|
||||
});
|
||||
@@ -99,28 +99,28 @@ it('lifts the snackbar above the bar only when there is a bar', function () {
|
||||
|
||||
it('passes M3\'s tall bar through, and the bottom offset grows with it', function () {
|
||||
expect((string) $this->blade('<x-scaffold :destinations="$destinations" tall-bar />', ['destinations' => scaffoldDestinations()]))
|
||||
->toMatch('/data-navigation-bar\s+data-tall\s/')
|
||||
->toMatch('/data-md-navigation-bar\s+data-md-tall\s/')
|
||||
->toContain('max-medium:[--material-bottom-bar:calc(5rem+')
|
||||
->not->toContain('[--material-bottom-bar:calc(4rem+')
|
||||
->and((string) $this->blade('<x-scaffold :destinations="$destinations" />', ['destinations' => scaffoldDestinations()]))
|
||||
->not->toContain('data-tall')
|
||||
->not->toContain('data-md-tall')
|
||||
->toContain('max-medium:[--material-bottom-bar:calc(4rem+');
|
||||
});
|
||||
|
||||
it('lets the bar leave the window while the page scrolls down', function () {
|
||||
expect((string) $this->blade('<x-scaffold :destinations="$destinations" hide-bar-on-scroll />', ['destinations' => scaffoldDestinations()]))
|
||||
->toContain('data-hide-on-scroll')
|
||||
->toContain('data-md-hide-on-scroll')
|
||||
->toContain('x-data="materialNavigationBar"')
|
||||
->and((string) $this->blade('<x-scaffold :destinations="$destinations" />', ['destinations' => scaffoldDestinations()]))
|
||||
->not->toContain('data-hide-on-scroll');
|
||||
->not->toContain('data-md-hide-on-scroll');
|
||||
});
|
||||
|
||||
it('exposes the rail that hides when collapsed', function () {
|
||||
expect((string) $this->blade('<x-scaffold :destinations="$destinations" hide-rail-when-collapsed />', ['destinations' => scaffoldDestinations()]))
|
||||
->toContain('data-hide-when-collapsed')
|
||||
->toContain('data-md-hide-when-collapsed')
|
||||
->toContain("materialNavigationRail('adaptive', true)")
|
||||
->and((string) $this->blade('<x-scaffold :destinations="$destinations" />', ['destinations' => scaffoldDestinations()]))
|
||||
->not->toContain('data-hide-when-collapsed')
|
||||
->not->toContain('data-md-hide-when-collapsed')
|
||||
->toContain("materialNavigationRail('adaptive', false)");
|
||||
});
|
||||
|
||||
@@ -150,7 +150,7 @@ it('places each slot once', function () {
|
||||
expect(substr_count($html, $slot))->toBe(1);
|
||||
}
|
||||
|
||||
expect($html)->toMatch('/data-navigation-rail-header.*BRAND.*FAB.*data-navigation-rail-footer.*FOOTER.*data-app-shell-actions.*ACTIONS.*APP BAR.*<main.*PAGE/s');
|
||||
expect($html)->toMatch('/data-md-navigation-rail-header.*BRAND.*FAB.*data-md-navigation-rail-footer.*FOOTER.*data-app-shell-actions.*ACTIONS.*APP BAR.*<main.*PAGE/s');
|
||||
});
|
||||
|
||||
it('spans the window with a banner, above the rail, and renders none without the slot', function () {
|
||||
@@ -163,7 +163,7 @@ it('spans the window with a banner, above the rail, and renders none without the
|
||||
|
||||
// Bars, then rails, then panes: the banner is outside the row the rail and the page share.
|
||||
expect($html)
|
||||
->toMatch('/data-app-shell-banner.*WINDOW BAR.*data-navigation-rail=.*<main/s')
|
||||
->toMatch('/data-app-shell-banner.*WINDOW BAR.*data-md-navigation-rail=.*<main/s')
|
||||
->and((string) $this->blade('<x-scaffold :destinations="$destinations" />', ['destinations' => scaffoldDestinations()]))
|
||||
->not->toContain('data-app-shell-banner');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user