Merge branch 'worktree-agent-aed4618fbd0b6aeac'
This commit is contained in:
@@ -786,7 +786,7 @@ A visitor who has pressed the menu button keeps that choice in both standard ban
|
||||
```
|
||||
|
||||
- `destinations`: `title`, `icon`, `url`; optional `active` (default: the URL is the page's, also during a Livewire update request), `badge` (`true` for a dot, or a count), `badgeLabel` (what a screen reader hears for the badge: "3 unread"), `section` (a heading in the rail, shown only while it is expanded; consecutive destinations with the same section are grouped), `bar` (default `true`; `false` keeps it out of the bottom bar — M3 wants three to five there), `navigate` (`false` for a full page load instead of `wire:navigate`).
|
||||
- Slots, each rendered once: `banner` (a bar across the whole window, above the rail and the page), `brand` (beside the rail's menu button, expanded only), `rail-header` (a FAB), `rail-footer` (pinned to the foot of the rail), `actions` (a row of icon buttons at the very foot, stacked when collapsed), `top` (the page's own bar, above the page and beside the rail), and the page. `label` names the landmarks ("Main"); `rail-width` is the expanded width (`16rem`).
|
||||
- Slots, each rendered once: `banner` (a bar across the whole window, above the rail and the page), `brand` (beside the rail's menu button, expanded only), `rail-header` (a FAB), `rail-footer` (pinned to the foot of the rail), `actions` (a row of icon buttons at the very foot, stacked when collapsed), `top` (the page's own bar, above the page and beside the rail), and the page. `label` names the landmarks ("Main"); `rail-width` is the expanded width (`16rem`); `tall-bar` picks M3's 80px navigation bar over the 64px one; `hide-bar-on-scroll` lets the bar leave the window while the page scrolls down; `hide-rail-when-collapsed` takes the rail out of the layout from `expanded` when its menu button collapses it, instead of narrowing it to 96px — the only way back is `$store.rail.show()`, so the app bar then needs a menu button at every width.
|
||||
- `banner` or `top`: M3's scaffold is bars, then rails, then panes. An application-wide bar — one search, one account menu, the same on every page — goes in `banner` and the rail starts under it; a bar that titles the page goes in `top`, beside the rail. Never both. A banner that pins itself to the top of the window says how tall it is (`style="--material-banner: 4rem"` on `<x-app-shell>`), so the rail sticks under it instead of behind it.
|
||||
- The rail is one element at every width: what is in it is also what a phone sees in the modal rail. On a compact window nothing opens it but `$store.rail.show()`, so a page whose destinations are not all in the bar needs a menu button in its app bar (hidden from `medium`).
|
||||
- `--material-margin` is M3's window margin (16px compact, 24px from `medium`) and the content region already carries it, so a page inside the shell writes no gutters of its own; something that must reach the window's edges opts out with `-mx-(--material-margin)`.
|
||||
@@ -807,7 +807,7 @@ M3 Expressive's flexible navigation bar, for three to five destinations. It does
|
||||
</div>
|
||||
```
|
||||
|
||||
64px in surface-container with the bottom safe area under it. Narrower than 600px the icon sits in a 56×32 indicator over the label; from 600px (the bar's own width) icon and label share a 40px pill and the items gather in the middle. `<x-navigation-bar>`: `label` ("Main"). `<x-navigation-bar-item>`: `label` / slot, `icon`, `link` (with `wire:navigate` unless `external` or `no-wire-navigate`; without a link it is a button), `active` (`aria-current="page"`, filled icon, secondary-container indicator), `badge` (`true` for a dot, a number for a count, 999+ at most), `badge-label` (what a screen reader hears instead of ", 3").
|
||||
64px in surface-container with the bottom safe area under it. Narrower than 600px the icon sits in a 56×32 indicator over the label; from 600px (the bar's own width) icon and label share a 40px pill and the items gather in the middle. `<x-navigation-bar>`: `label` ("Main"), `tall` (M3's 80px container, which keeps the icon over the label at every width — `<x-app-shell tall-bar>` picks it, and the bottom offset grows with it), `hide-on-scroll` (M3's scrolling behaviour: the bar slides out on a scroll down and springs back on a scroll up, never before the first screenful and never while a snackbar, bottom sheet or drawer is on screen; focus reaching it brings it back. `<x-app-shell hide-bar-on-scroll>` picks it, and `--material-bottom-bar` goes down and comes back with the bar, so a `fab` button and the snackbar keep their distance from it). `<x-navigation-bar-item>`: `label` / slot, `icon`, `link` (with `wire:navigate` unless `external` or `no-wire-navigate`; without a link it is a button), `active` (`aria-current="page"`, filled icon, secondary-container indicator), `badge` (`true` for a dot, a number for a count, 999+ at most), `badge-label` (what a screen reader hears instead of ", 3").
|
||||
|
||||
### `<x-navigation-rail>`, `<x-navigation-rail-item>`, `<x-navigation-rail-section>`
|
||||
|
||||
@@ -834,7 +834,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-app-shell>`'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), `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.
|
||||
- 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.
|
||||
- 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.
|
||||
@@ -855,7 +855,7 @@ A collapsing bar needs the window to scroll: no ancestor with `overflow-hidden`/
|
||||
|
||||
### `<x-toolbar>`
|
||||
|
||||
M3 Expressive toolbar, `role="toolbar"` (arrow keys move between controls). `variant`: `floating` (default pill at elevation 3; `vibrant`, `vertical`) or `docked` (full-width surface-container bar). `place`: `bottom` or `end` to fix it over the page; `fab` slot sets a FAB beside a floating toolbar; `label` names it.
|
||||
M3 Expressive toolbar, `role="toolbar"` (arrow keys move between controls). `variant`: `floating` (default pill at elevation 3; `vibrant`, `vertical`) or `docked` (full-width surface-container bar). `place`: `bottom` or `end` to fix it over the page; `fab` slot takes an `<x-fab>` — beside a floating toolbar, or at the end of a docked one, where the controls gather at the start and the FAB rests flat on the bar (M3's elevation 0 for a nested FAB) and the arrow keys reach it; `label` names it.
|
||||
|
||||
```blade
|
||||
<x-toolbar label="Selection" place="bottom" vibrant>
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
* NavigationRailHorizontalItemTokens.kt, NavigationRailColorTokens.kt, and the layout in
|
||||
* ShortNavigationBar.kt, WideNavigationRail.kt and NavigationItem.kt.
|
||||
*
|
||||
* [data-navigation-bar] surface-container, 64px, the bottom safe area under it
|
||||
* [data-navigation-bar-items] equal widths; centred from a 600px-wide bar
|
||||
* [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
|
||||
@@ -42,16 +43,19 @@
|
||||
* 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 last block, which restyles a FAB the
|
||||
* application put in the rail: that one has to beat a utility, and a layer never does.
|
||||
* 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-app-shell> 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'], [data-rail='collapsed'] [data-navigation-rail='collapsible'] *) {
|
||||
&:where([data-rail='collapsed'] [data-navigation-rail='collapsible']:not([data-open]), [data-rail='collapsed'] [data-navigation-rail='collapsible']:not([data-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
|
||||
@@ -62,7 +66,7 @@
|
||||
/* 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'], [data-navigation-rail='collapsible'] *) {
|
||||
&:where([data-navigation-rail='collapsible']:not([data-open]), [data-navigation-rail='collapsible']:not([data-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
@@ -78,14 +82,14 @@
|
||||
`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'], :is([data-rail='collapsed'], [data-rail-auto]) [data-navigation-rail='adaptive'] *) {
|
||||
&: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'], [data-rail='collapsed'] [data-navigation-rail='adaptive'] *) {
|
||||
&:where([data-rail='collapsed'] [data-navigation-rail='adaptive']:not([data-open]), [data-rail='collapsed'] [data-navigation-rail='adaptive']:not([data-open]) *) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
@@ -108,6 +112,26 @@
|
||||
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;
|
||||
@@ -149,22 +173,24 @@
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
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-items] {
|
||||
[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-items]:has(> :last-child:nth-child(1)) { --navigation-bar-count: 1; }
|
||||
[data-navigation-bar-items]:has(> :last-child:nth-child(2)) { --navigation-bar-count: 2; }
|
||||
[data-navigation-bar-items]:has(> :last-child:nth-child(3)) { --navigation-bar-count: 3; }
|
||||
[data-navigation-bar-items]:has(> :last-child:nth-child(4)) { --navigation-bar-count: 4; }
|
||||
[data-navigation-bar-items]:has(> :last-child:nth-child(5)) { --navigation-bar-count: 5; }
|
||||
[data-navigation-bar-items]:has(> :last-child:nth-child(6)) { --navigation-bar-count: 6; }
|
||||
[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-item] {
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item] {
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
@@ -172,7 +198,7 @@
|
||||
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-item] [data-navigation-pill] {
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item] [data-navigation-pill] {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
flex-direction: row;
|
||||
@@ -180,15 +206,15 @@
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
[data-navigation-bar-item] [data-navigation-label] {
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item] [data-navigation-label] {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
[data-navigation-bar-item][data-active] {
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item][data-active] {
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-navigation-bar-item][data-active] [data-navigation-pill] {
|
||||
[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));
|
||||
}
|
||||
}
|
||||
@@ -197,25 +223,61 @@
|
||||
|
||||
[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: 6rem;
|
||||
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: 6rem;
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
}
|
||||
|
||||
@media (width < 52.5rem) {
|
||||
[data-navigation-rail='adaptive'] {
|
||||
width: 6rem;
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,10 +288,10 @@
|
||||
|
||||
/* "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 the collapsed 96px. `collapsed` and `expanded` mean what they say and are
|
||||
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: 6rem;
|
||||
width: var(--navigation-rail-collapsed-width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +395,72 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
@@ -395,6 +523,16 @@
|
||||
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;
|
||||
@@ -513,13 +651,17 @@
|
||||
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-item][data-active] [data-navigation-indicator] {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
@container (width >= 37.5rem) {
|
||||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item][data-active] [data-navigation-indicator] {
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,14 +721,16 @@
|
||||
background-color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
@container (width < 37.5rem) {
|
||||
[data-navigation-bar-item] [data-navigation-pill]::before {
|
||||
display: none;
|
||||
}
|
||||
[data-navigation-bar-item] [data-navigation-pill]::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@container (width >= 37.5rem) {
|
||||
[data-navigation-bar-item] [data-navigation-indicator]::before {
|
||||
[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;
|
||||
}
|
||||
}
|
||||
@@ -615,15 +759,17 @@
|
||||
}
|
||||
|
||||
/* M3's focus indicator, 3px of secondary 2px out, around the same shape. */
|
||||
@container (width < 37.5rem) {
|
||||
[data-navigation-bar-item]:focus-visible [data-navigation-indicator] {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
[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-item]:focus-visible [data-navigation-pill] {
|
||||
[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;
|
||||
}
|
||||
@@ -646,6 +792,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 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-app-shell> 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
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* Apache-2.0).
|
||||
*
|
||||
* docked a 64px bar across the bottom of the screen in surface-container, square, its
|
||||
* controls spread out 4 to 32px apart between 16px ends
|
||||
* controls spread out 4 to 32px apart between 16px ends; with a FAB, the controls
|
||||
* at the start and the FAB flat at the end
|
||||
* floating a 64px pill in surface-container (or primary-container, `vibrant`) at elevation 3,
|
||||
* 8px at its ends and 4px between controls; `vertical` stands it on end
|
||||
*
|
||||
@@ -71,6 +72,24 @@
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
/* A docked toolbar with a FAB: the controls at the start, the FAB at the end, resting on the bar
|
||||
at elevation 0 — M3 puts a FAB nested in another component there, and Compose's
|
||||
FloatingActionButtonDefaults.bottomAppBarFabElevation() is 0 in every state. `<x-fab>` draws its
|
||||
shadow with utilities, which is one more reason this file is unlayered. */
|
||||
[data-toolbar][data-variant="docked"]:has(> [data-toolbar-fab]) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
[data-toolbar-fab] {
|
||||
display: flex;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
[data-toolbar-fab] [data-fab],
|
||||
[data-toolbar-fab] [data-fab]:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* A floating toolbar and its FAB, side by side (or stacked, vertical). */
|
||||
[data-toolbar-group] {
|
||||
display: inline-flex;
|
||||
|
||||
+127
-16
@@ -13,10 +13,12 @@
|
||||
* button opens it over a scrim (`show()`), and Escape, the scrim or leaving the page closes it
|
||||
* (`hide()`). It is never remembered.
|
||||
*
|
||||
* `materialNavigationRail` is one rail's view of the store for its `mode` — see
|
||||
* resources/views/components/navigation-rail.blade.php.
|
||||
* `materialNavigationRail` is one rail's view of the store for its `mode` and whether it hides
|
||||
* when collapsed — see resources/views/components/navigation-rail.blade.php.
|
||||
*
|
||||
* `materialNavigationBar` is `<x-navigation-bar hide-on-scroll>` — see the same file's sibling.
|
||||
*/
|
||||
import { from } from './breakpoints.js'
|
||||
import { from, upTo } from './breakpoints.js'
|
||||
|
||||
/*
|
||||
* The active indicator grows out of its centre when a page arrives through wire:navigate. The
|
||||
@@ -94,14 +96,29 @@ document.addEventListener('alpine:init', () => {
|
||||
// A destination chosen in the modal rail leaves the page; the next one starts with it shut.
|
||||
document.addEventListener('livewire:navigating', () => window.Alpine.store('rail').hide())
|
||||
|
||||
window.Alpine.data('materialNavigationRail', (mode) => ({
|
||||
window.Alpine.data('materialNavigationRail', (mode, hideWhenCollapsed = false) => ({
|
||||
wide: mode === 'adaptive' ? from('expanded').matches : false,
|
||||
roomy: mode === 'adaptive' ? from('large').matches : false,
|
||||
tight: mode === 'collapsible' ? upTo('medium').matches : false,
|
||||
queries: [],
|
||||
listeners: [],
|
||||
|
||||
init() {
|
||||
if (mode !== 'adaptive') {
|
||||
// Below `medium` a collapsible rail is held at its collapsed width whatever the
|
||||
// visitor chose, so `hide-when-collapsed` does not reach it there.
|
||||
// A drawer left open as the window narrows past it is shut, as the adaptive
|
||||
// rail's is below, or it would spring open again the next time the rail is away.
|
||||
if (mode === 'collapsible' && hideWhenCollapsed) {
|
||||
this.watch(upTo('medium'), (matches) => {
|
||||
this.tight = matches
|
||||
|
||||
if (matches) {
|
||||
this.$store.rail.hide()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -134,17 +151,9 @@ document.addEventListener('alpine:init', () => {
|
||||
this.queries.forEach((query, index) => query.removeEventListener('change', this.listeners[index]))
|
||||
},
|
||||
|
||||
/** Whether this rail expands over a scrim rather than in the layout. */
|
||||
get modal() {
|
||||
return mode === 'modal' || (mode === 'adaptive' && !this.wide)
|
||||
},
|
||||
|
||||
get open() {
|
||||
return this.modal && this.$store.rail.open
|
||||
},
|
||||
|
||||
get expanded() {
|
||||
if (this.open || mode === 'expanded') {
|
||||
/** What this rail's mode and the visitor's choice make of it, before anything opens it. */
|
||||
get standing() {
|
||||
if (mode === 'expanded') {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -161,9 +170,49 @@ document.addEventListener('alpine:init', () => {
|
||||
return mode === 'collapsible'
|
||||
},
|
||||
|
||||
/** Whether the mode or the window leaves no room for an expanded rail in the layout. */
|
||||
get cramped() {
|
||||
return mode === 'modal' || (mode === 'adaptive' && !this.wide)
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the rail has left the layout altogether — `hide-when-collapsed`, once the
|
||||
* visitor collapses it. Not in the two bands where it is the window size class and not the
|
||||
* visitor that collapses a rail: M3's "collapsed rail may not hide". The same two numbers
|
||||
* are in resources/css/components/navigation.css.
|
||||
*/
|
||||
get away() {
|
||||
if (!hideWhenCollapsed || this.cramped || this.standing) {
|
||||
return false
|
||||
}
|
||||
|
||||
return mode === 'adaptive' ? this.wide : !this.tight
|
||||
},
|
||||
|
||||
/** Whether this rail expands over a scrim rather than in the layout. */
|
||||
get modal() {
|
||||
// A rail that is away has nothing left in the layout to expand, so the menu button
|
||||
// that brings it back — the app bar's — opens it over the page.
|
||||
return this.cramped || this.away
|
||||
},
|
||||
|
||||
get open() {
|
||||
return this.modal && this.$store.rail.open
|
||||
},
|
||||
|
||||
get expanded() {
|
||||
return this.open || this.standing
|
||||
},
|
||||
|
||||
/** The rail's own menu button: open or close the modal, or collapse and expand in place. */
|
||||
menu() {
|
||||
if (this.modal) {
|
||||
if (this.open && this.away) {
|
||||
// This rail is only over the page because it hid itself, so the button docks it
|
||||
// back into the layout — the same "expand" it means on a rail that is standing
|
||||
// there. Expanding drops `away`, which closes the drawer behind it.
|
||||
this.$store.rail.expand()
|
||||
this.$store.rail.hide()
|
||||
} else if (this.modal) {
|
||||
this.$store.rail.open ? this.$store.rail.hide() : this.$store.rail.show()
|
||||
} else {
|
||||
// `set`, not `toggle`: with nothing stored the store's `collapsed` is only
|
||||
@@ -172,4 +221,66 @@ document.addEventListener('alpine:init', () => {
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
window.Alpine.data('materialNavigationBar', () => ({
|
||||
away: false,
|
||||
last: 0,
|
||||
frame: null,
|
||||
// Set in init(), so a second bar in the same page scope cannot take the first one's.
|
||||
schedule: null,
|
||||
|
||||
init() {
|
||||
this.last = Math.max(window.scrollY, 0)
|
||||
this.measure = this.measure.bind(this)
|
||||
this.schedule = () => {
|
||||
this.frame ??= requestAnimationFrame(this.measure)
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', this.schedule, { passive: true })
|
||||
},
|
||||
|
||||
destroy() {
|
||||
window.removeEventListener('scroll', this.schedule)
|
||||
cancelAnimationFrame(this.frame)
|
||||
},
|
||||
|
||||
/** Anything that reaches the bar — the keyboard, a screen reader's focus — brings it back. */
|
||||
show() {
|
||||
this.away = false
|
||||
},
|
||||
|
||||
measure() {
|
||||
this.frame = null
|
||||
|
||||
const at = Math.max(window.scrollY, 0)
|
||||
const by = at - this.last
|
||||
|
||||
// Smaller than a finger's jitter, or the rubber band at either end of the page: not a
|
||||
// direction yet, and the bar should not flicker while one is being decided.
|
||||
if (Math.abs(by) < 8) {
|
||||
return
|
||||
}
|
||||
|
||||
this.last = at
|
||||
|
||||
// A bar that is not on screen at this width (the shell hides it from `medium`) has no
|
||||
// scroll behaviour to have; one with something anchored to its edge keeps still, so
|
||||
// the snackbar or sheet resting on it does not slide with it.
|
||||
if (this.$root.getClientRects().length === 0 || anchored()) {
|
||||
this.away = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Never before the first screenful: the bar has to be passed before it can be left.
|
||||
this.away = by > 0 && at > this.$root.offsetHeight
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
/**
|
||||
* Whether something on screen is anchored to the bar's edge and would be dragged along with it: a
|
||||
* snackbar (`<x-toast>`'s, which reads --material-bottom-bar), or a bottom sheet or drawer over the
|
||||
* page. M3 lets those cover the bar; it is the bar leaving from under them that looks broken.
|
||||
*/
|
||||
const anchored = () => [...document.querySelectorAll('[data-toast], [role="dialog"]')].some((over) => over.getClientRects().length > 0)
|
||||
|
||||
@@ -54,7 +54,13 @@
|
||||
sign out), `top` (the page's own bar, above the page and *beside* the rail) and the page
|
||||
itself. The rail is one element at every width, so what is in it is also in the modal rail a
|
||||
phone opens. `label` names both navigation landmarks ("Main"); `rail-width` is the expanded
|
||||
rail's width.
|
||||
rail's width; `tall-bar` picks M3's 80px navigation bar over the 64px one, and the bottom
|
||||
offset every pinned thing reads follows it; `hide-bar-on-scroll` lets the bar leave the window
|
||||
while the page scrolls down, and `--material-bottom-bar` goes down and comes back with it;
|
||||
`hide-rail-when-collapsed` is M3's immersive configuration — from `expanded` the rail leaves
|
||||
the layout when the menu button collapses it, rather than narrowing to 96px, so the page has
|
||||
the whole window. The only way back is `$store.rail.show()`, so put a menu button in the app
|
||||
bar at every width, not just below `medium`.
|
||||
|
||||
`banner` or `top` is a decision about what the bar belongs to: an application-wide bar — one
|
||||
search, one account menu, the same on every page — spans the window and the rail starts under
|
||||
@@ -82,6 +88,9 @@
|
||||
'destinations' => [],
|
||||
'label' => null,
|
||||
'railWidth' => '16rem',
|
||||
'tallBar' => false,
|
||||
'hideBarOnScroll' => false,
|
||||
'hideRailWhenCollapsed' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
@@ -113,7 +122,8 @@
|
||||
data-app-shell
|
||||
@class([
|
||||
'flex min-h-dvh flex-col bg-surface text-on-surface [--material-margin:1rem] medium:[--material-margin:1.5rem]',
|
||||
'max-medium:[--material-bottom-bar:calc(4rem+var(--material-safe-bottom,env(safe-area-inset-bottom))+var(--material-bottom-extra,0px))]' => $barItems->isNotEmpty(),
|
||||
'max-medium:[--material-bottom-bar:calc(4rem+var(--material-safe-bottom,env(safe-area-inset-bottom))+var(--material-bottom-extra,0px))]' => $barItems->isNotEmpty() && ! $tallBar,
|
||||
'max-medium:[--material-bottom-bar:calc(5rem+var(--material-safe-bottom,env(safe-area-inset-bottom))+var(--material-bottom-extra,0px))]' => $barItems->isNotEmpty() && $tallBar,
|
||||
])
|
||||
>
|
||||
<a
|
||||
@@ -127,7 +137,7 @@
|
||||
@endisset
|
||||
|
||||
<div class="flex-1 medium:flex">
|
||||
<x-livewire-material::navigation-rail mode="adaptive" :label="$label" :width="$railWidth">
|
||||
<x-livewire-material::navigation-rail mode="adaptive" :label="$label" :width="$railWidth" :hide-when-collapsed="$hideRailWhenCollapsed">
|
||||
@isset($brand)
|
||||
<x-slot:brand>{{ $brand }}</x-slot:brand>
|
||||
@endisset
|
||||
@@ -174,7 +184,7 @@
|
||||
|
||||
@if ($barItems->isNotEmpty())
|
||||
<div data-app-shell-bar class="fixed inset-x-0 bottom-0 z-30 medium:hidden">
|
||||
<x-livewire-material::navigation-bar :label="$label">
|
||||
<x-livewire-material::navigation-bar :label="$label" :tall="$tallBar" :hide-on-scroll="$hideBarOnScroll">
|
||||
@foreach ($barItems as $item)
|
||||
<x-livewire-material::navigation-bar-item :label="$item['title']" :icon="$item['icon']" :link="$item['url']" :active="$item['active']" :badge="$item['badge']" :badge-label="$item['badgeLabel']" :no-wire-navigate="! $item['navigate']" />
|
||||
@endforeach
|
||||
|
||||
@@ -16,11 +16,24 @@
|
||||
Centered arrangement gives three to six items. Both follow the bar's own width (a container
|
||||
query), so a bar in a narrow column keeps the compact items.
|
||||
|
||||
`tall` is M3's other container: 80px (NavigationBarTokens.TallContainerHeight) with the
|
||||
vertical item layout at every width — icon over label, never side by side — for a bar whose
|
||||
labels need the room. The short bar stays the default.
|
||||
|
||||
`hide-on-scroll` is M3's scrolling behaviour: the bar slides out of the window on a scroll
|
||||
down and springs back on a scroll up, never before the first screenful has gone by, and never
|
||||
while a snackbar, a bottom sheet or a drawer is on screen — those are anchored to the bar's
|
||||
edge and would slide with it. Focus reaching the bar brings it back, which is as close as the
|
||||
web gets to M3's "never hide it while a screen reader is active". Inside `<x-app-shell>`
|
||||
`--material-bottom-bar` follows the bar down and up, so a `fab` button, the snackbar and the
|
||||
page's bottom padding keep their distance from it rather than from where it was.
|
||||
|
||||
It does not position itself: wrap it in the element that pins it (`fixed inset-x-0 bottom-0`)
|
||||
and hides it where a rail takes over. `<x-app-shell>` does both, and lifts the snackbar and a
|
||||
`fab` button above it through `--material-bottom-bar`.
|
||||
|
||||
`label` names the landmark ("Main" by default).
|
||||
`label` names the landmark ("Main" by default); `tall` picks the 80px container;
|
||||
`hide-on-scroll` lets it leave the window while the page scrolls down.
|
||||
|
||||
Values from androidx Compose Material 3 (Apache-2.0), androidx-main
|
||||
27cf9a7d5788aa0f5f2d8b6699ce279560daf326: NavigationBarTokens.kt,
|
||||
@@ -31,9 +44,22 @@
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
'tall' => false,
|
||||
'hideOnScroll' => false,
|
||||
])
|
||||
|
||||
<nav aria-label="{{ $label ?? __('Main') }}" data-navigation-bar {{ $attributes }}>
|
||||
<nav
|
||||
aria-label="{{ $label ?? __('Main') }}"
|
||||
data-navigation-bar
|
||||
@if ($tall) data-tall @endif
|
||||
@if ($hideOnScroll)
|
||||
data-hide-on-scroll
|
||||
x-data="materialNavigationBar"
|
||||
x-bind:data-hidden="away ? '' : null"
|
||||
x-on:focusin="show()"
|
||||
@endif
|
||||
{{ $attributes }}
|
||||
>
|
||||
<div data-navigation-bar-items>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,21 @@
|
||||
Nothing that shows while collapsed may be wider than 96px.
|
||||
|
||||
Props: `label` names the landmark ("Main"); `width` is the expanded width (`16rem`, held
|
||||
between M3's 220 and 360dp); `menu` shows the menu button (by default for `collapsible`,
|
||||
between M3's 220 and 360dp) — or the word `narrow`, M3's other *collapsed* width
|
||||
(NavigationRailCollapsedTokens.NarrowContainerWidth, 80px against the default 96), where the
|
||||
items are their icons alone because no label fits under a 56px indicator at that width; the
|
||||
labels stay in the accessibility tree, since they are what name the destinations, and a
|
||||
narrow rail expands to the default 16rem; `align` is `top` (the default) or `center`, which
|
||||
puts the destinations at the rail's vertical centre — M3 prefers that on a tablet, for reach —
|
||||
while the menu button, the brand and the FAB stay at the top and the footer at the foot, as M3
|
||||
asks; more destinations than fit go back to the top rather than out of reach above the
|
||||
scroller; `hide-when-collapsed` is M3's other expanded behaviour, for a `collapsible` or
|
||||
`adaptive` rail: collapsing it takes it out of the layout altogether instead of narrowing it
|
||||
to 96px, and it comes back expanded over a scrim when something calls `$store.rail.show()` —
|
||||
a menu button in the app bar, which is the only way back, so put one there. Its own menu
|
||||
button then docks it into the layout again. Not below `medium` for a collapsible rail, nor at
|
||||
`medium` for an adaptive one: there it is the window and not the visitor that collapses a
|
||||
rail, and M3's collapsed rail may never hide; `menu` shows the menu button (by default for `collapsible`,
|
||||
`modal` and `adaptive`); `divider` draws M3's optional vertical divider on the edge the page
|
||||
is on — which is also what M3 asks for when a page scrolls underneath a fixed rail; `fill`
|
||||
(`false`) drops the container colour for a transparent rail over the page's own background,
|
||||
@@ -74,6 +88,8 @@
|
||||
'mode' => 'collapsible',
|
||||
'label' => null,
|
||||
'width' => '16rem',
|
||||
'align' => 'top',
|
||||
'hideWhenCollapsed' => false,
|
||||
'menu' => null,
|
||||
'divider' => false,
|
||||
'fill' => true,
|
||||
@@ -82,17 +98,26 @@
|
||||
@php
|
||||
$mode = in_array($mode, ['collapsed', 'expanded', 'collapsible', 'modal', 'adaptive'], true) ? $mode : 'collapsible';
|
||||
$interactive = in_array($mode, ['collapsible', 'modal', 'adaptive'], true);
|
||||
$canOpen = in_array($mode, ['modal', 'adaptive'], true);
|
||||
// Only a rail that has a collapsed *and* an expanded state of its own can hide instead of
|
||||
// narrowing; a `modal` one is already over the page, and the two fixed modes mean what they say.
|
||||
$hideWhenCollapsed = $hideWhenCollapsed && in_array($mode, ['collapsible', 'adaptive'], true);
|
||||
$canOpen = in_array($mode, ['modal', 'adaptive'], true) || $hideWhenCollapsed;
|
||||
$menu ??= $interactive;
|
||||
$collapsedAtFirst = in_array($mode, ['collapsed', 'modal'], true);
|
||||
$narrow = $width === 'narrow';
|
||||
$width = $narrow ? '16rem' : $width;
|
||||
$centred = $align === 'center';
|
||||
@endphp
|
||||
|
||||
<div
|
||||
data-navigation-rail="{{ $mode }}"
|
||||
@if ($narrow) data-width="narrow" @endif
|
||||
@if ($centred) data-align="center" @endif
|
||||
@if ($hideWhenCollapsed) data-hide-when-collapsed @endif
|
||||
@if ($divider) data-divider @endif
|
||||
@unless ($fill) data-fill="false" @endunless
|
||||
@if ($interactive)
|
||||
x-data="materialNavigationRail('{{ $mode }}')"
|
||||
x-data="materialNavigationRail('{{ $mode }}', {{ $hideWhenCollapsed ? 'true' : 'false' }})"
|
||||
x-bind:data-open="open"
|
||||
@endif
|
||||
{{ $attributes->merge(['style' => "--navigation-rail-width: {$width}"]) }}
|
||||
@@ -112,7 +137,9 @@
|
||||
@if ($menu || isset($brand) || isset($header))
|
||||
<div data-navigation-rail-header>
|
||||
@if ($menu || isset($brand))
|
||||
<div @class(['flex w-full min-w-0 items-center gap-3 pe-5', 'ps-7' => $menu, 'ps-5' => ! $menu])>
|
||||
{{-- The menu button is centred on the collapsed rail: 28px either side of a
|
||||
40px button in 96, 20px in a narrow 80. --}}
|
||||
<div @class(['flex w-full min-w-0 items-center gap-3 pe-5', 'ps-7' => $menu, 'rail-collapsed:ps-5' => $menu && $narrow, 'ps-5' => ! $menu])>
|
||||
@if ($menu)
|
||||
<button
|
||||
type="button"
|
||||
@@ -136,7 +163,8 @@
|
||||
@endif
|
||||
|
||||
@isset($header)
|
||||
<div {{ $header->attributes->class(['flex w-full flex-col items-start gap-2 px-5']) }}>{{ $header }}</div>
|
||||
{{-- And so is a 56px FAB: 20px either side in 96, 12px in a narrow 80. --}}
|
||||
<div {{ $header->attributes->class(['flex w-full flex-col items-start gap-2 px-5', 'rail-collapsed:px-3' => $narrow]) }}>{{ $header }}</div>
|
||||
@endisset
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -4,8 +4,12 @@
|
||||
`vertical` stands it on end) or `docked` (a full-width bar in surface-container, for the bottom
|
||||
of a screen). `place` puts it over the page: `bottom` (centred above the bottom edge; a docked
|
||||
toolbar spans it) or `end` (centred against the end edge, for a vertical one); without it the
|
||||
toolbar sits where it is written. A `fab` slot sets an `<x-fab>` beside a floating toolbar.
|
||||
`label` names it for screen readers.
|
||||
toolbar sits where it is written. A `fab` slot sets an `<x-fab>` beside a floating toolbar,
|
||||
and at the end of a docked one — M3's "With FAB" configuration for both. On a docked toolbar the
|
||||
controls then gather at the start and the FAB rests on the bar, flat: M3 gives a FAB nested in
|
||||
another component elevation 0 (Compose's bottomAppBarFabElevation is 0 at every state), where
|
||||
one beside a floating toolbar keeps its own. It is one of the toolbar's controls then, so the
|
||||
arrow keys reach it. `label` names it for screen readers.
|
||||
|
||||
A docked toolbar and a navigation bar occupy the same region of the screen and M3 says never to
|
||||
show both at once: the bar belongs on a primary page, the toolbar on a secondary or contextual
|
||||
@@ -29,6 +33,7 @@
|
||||
$vertical = $vertical && $variant === 'floating';
|
||||
$place = in_array($place, ['bottom', 'end'], true) ? $place : null;
|
||||
$grouped = isset($fab) && $variant === 'floating';
|
||||
$docksFab = isset($fab) && $variant === 'docked';
|
||||
@endphp
|
||||
|
||||
@if ($grouped)
|
||||
@@ -48,6 +53,10 @@
|
||||
{{ $attributes }}
|
||||
>
|
||||
{{ $slot }}
|
||||
|
||||
@if ($docksFab)
|
||||
<div data-toolbar-fab>{{ $fab }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($grouped)
|
||||
|
||||
@@ -73,6 +73,16 @@
|
||||
<x-button icon="delete" tooltip="Delete" />
|
||||
</x-toolbar>
|
||||
</div>
|
||||
|
||||
<div class="w-full overflow-hidden rounded-corner-lg border border-outline-variant">
|
||||
<x-toolbar variant="docked" label="Editing">
|
||||
<x-button icon="check_box" tooltip="Select" />
|
||||
<x-button icon="brush" tooltip="Draw" />
|
||||
<x-button icon="mic" tooltip="Record" />
|
||||
<x-button icon="image" tooltip="Add image" />
|
||||
<x-slot:fab><x-fab icon="add" tooltip="New note" color="secondary" /></x-slot:fab>
|
||||
</x-toolbar>
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'Primary tabs' => <<<'BLADE'
|
||||
|
||||
@@ -29,9 +29,36 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-[25rem] space-y-2">
|
||||
<p class="type-label-lg text-on-surface-variant">Hide on scroll: scroll the page down, then up</p>
|
||||
<div class="flex h-56 flex-col overflow-hidden rounded-corner-lg border border-outline-variant bg-surface">
|
||||
<div class="flex-1 p-4 type-body-md text-on-surface-variant">The bar follows the window's scrolling, not this box's: scroll the showcase down and it leaves, scroll up and it springs back. It stays while a snackbar or a bottom sheet is on screen.</div>
|
||||
<x-navigation-bar hide-on-scroll>
|
||||
<x-navigation-bar-item label="Shares" icon="folder_shared" link="#navigation" no-wire-navigate active />
|
||||
<x-navigation-bar-item label="Upload" icon="upload" link="#navigation" no-wire-navigate />
|
||||
<x-navigation-bar-item label="Inbox" icon="inbox" link="#navigation" no-wire-navigate badge="3" />
|
||||
</x-navigation-bar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-[50rem] space-y-2">
|
||||
<p class="type-label-lg text-on-surface-variant">Tall (80px): icon over label whatever the width</p>
|
||||
<div class="overflow-x-auto rounded-corner-lg border border-outline-variant">
|
||||
<div class="flex h-56 min-w-[37.5rem] flex-col bg-surface">
|
||||
<div class="flex-1 p-4 type-body-md text-on-surface-variant">The page</div>
|
||||
<x-navigation-bar tall>
|
||||
<x-navigation-bar-item label="Shares" icon="folder_shared" link="#navigation" no-wire-navigate active />
|
||||
<x-navigation-bar-item label="Upload" icon="upload" link="#navigation" no-wire-navigate />
|
||||
<x-navigation-bar-item label="Inbox" icon="inbox" link="#navigation" no-wire-navigate badge="3" />
|
||||
<x-navigation-bar-item label="Account" icon="account_circle" link="#navigation" no-wire-navigate />
|
||||
</x-navigation-bar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'Collapsed and expanded rails' => <<<'BLADE'
|
||||
'Collapsed, narrow (centred) and expanded rails' => <<<'BLADE'
|
||||
<div class="flex w-full flex-wrap items-start gap-6">
|
||||
<div class="flex h-[36rem] overflow-hidden rounded-corner-lg border border-outline-variant">
|
||||
<x-navigation-rail mode="collapsed" label="Collapsed example" divider>
|
||||
@@ -53,6 +80,22 @@
|
||||
</x-navigation-rail>
|
||||
</div>
|
||||
|
||||
<div class="flex h-[36rem] overflow-hidden rounded-corner-lg border border-outline-variant">
|
||||
<x-navigation-rail mode="collapsed" width="narrow" align="center" label="Narrow example" divider>
|
||||
<x-slot:header>
|
||||
<x-fab label="Compose" icon="edit" />
|
||||
</x-slot:header>
|
||||
|
||||
<x-navigation-rail-item label="Inbox" icon="inbox" link="#navigation" no-wire-navigate active badge="12" />
|
||||
<x-navigation-rail-item label="Starred" icon="star" link="#navigation" no-wire-navigate />
|
||||
<x-navigation-rail-item label="Sent" icon="send" link="#navigation" no-wire-navigate />
|
||||
|
||||
<x-slot:footer>
|
||||
<x-navigation-rail-item label="Settings" icon="settings" link="#navigation" no-wire-navigate />
|
||||
</x-slot:footer>
|
||||
</x-navigation-rail>
|
||||
</div>
|
||||
|
||||
<div class="flex h-[36rem] overflow-hidden rounded-corner-lg border border-outline-variant">
|
||||
<x-navigation-rail mode="expanded" label="Expanded example">
|
||||
<x-slot:header>
|
||||
@@ -95,6 +138,24 @@
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'Rail that hides when collapsed' => <<<'BLADE'
|
||||
<div class="flex h-[30rem] w-full overflow-hidden rounded-corner-lg border border-outline-variant">
|
||||
<x-navigation-rail label="Immersive example" hide-when-collapsed>
|
||||
<x-slot:brand>
|
||||
<span class="block truncate type-title-lg">Studio</span>
|
||||
</x-slot:brand>
|
||||
|
||||
<x-navigation-rail-item label="Canvas" icon="brush" link="#navigation" no-wire-navigate active />
|
||||
<x-navigation-rail-item label="Layers" icon="layers" link="#navigation" no-wire-navigate />
|
||||
<x-navigation-rail-item label="Exports" icon="download" link="#navigation" no-wire-navigate />
|
||||
</x-navigation-rail>
|
||||
|
||||
<div class="min-w-0 flex-1 space-y-4 bg-surface-container-low p-6 type-body-md text-on-surface-variant">
|
||||
<p>M3's other expanded behaviour: collapsing this rail takes it out of the layout instead of narrowing it to 96px, so an immersive page gets the whole window. The only way back is a menu button of the application's own — its own button then docks it again.</p>
|
||||
<x-button icon="menu" label="Open navigation" x-on:click="$store.rail.show()" />
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
];
|
||||
@endphp
|
||||
|
||||
|
||||
@@ -69,6 +69,27 @@ it('draws floating and docked toolbars', function () {
|
||||
->not->toContain('data-toolbar-group');
|
||||
});
|
||||
|
||||
it('sets a FAB at the end of a docked toolbar, resting flat on it', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-toolbar variant="docked" label="Editing">
|
||||
<button>Draw</button>
|
||||
<x-slot:fab><button>+</button></x-slot:fab>
|
||||
</x-toolbar>
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
// Inside the toolbar, after its controls, where the arrow keys reach it too.
|
||||
->toMatch('/role="toolbar".*<button>Draw<\/button>\s*<div data-toolbar-fab><button>\+<\/button><\/div>\s*<\/div>/s')
|
||||
->not->toContain('data-toolbar-group')
|
||||
->and((string) $this->blade('<x-toolbar variant="docked"><button>Draw</button></x-toolbar>'))
|
||||
->not->toContain('data-toolbar-fab');
|
||||
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/toolbar.css'))
|
||||
->toMatch('/\[data-toolbar-fab\] \{\s+display: flex;\s+margin-inline-start: auto;/')
|
||||
// A nested FAB rests at elevation 0.
|
||||
->toMatch('/\[data-toolbar-fab\] \[data-fab\],\s+\[data-toolbar-fab\] \[data-fab\]:hover \{\s+box-shadow: none;/');
|
||||
});
|
||||
|
||||
it('centres a headline on a three-column row and curves the search container', function () {
|
||||
$css = file_get_contents(__DIR__.'/../../../resources/css/components/app-bar.css');
|
||||
|
||||
|
||||
@@ -97,6 +97,33 @@ it('lifts the snackbar above the bar only when there is a bar', function () {
|
||||
->not->toContain('--material-bottom-bar:');
|
||||
});
|
||||
|
||||
it('passes M3\'s tall bar through, and the bottom offset grows with it', function () {
|
||||
expect((string) $this->blade('<x-app-shell :destinations="$destinations" tall-bar />', ['destinations' => shellDestinations()]))
|
||||
->toMatch('/data-navigation-bar\s+data-tall\s/')
|
||||
->toContain('max-medium:[--material-bottom-bar:calc(5rem+')
|
||||
->not->toContain('[--material-bottom-bar:calc(4rem+')
|
||||
->and((string) $this->blade('<x-app-shell :destinations="$destinations" />', ['destinations' => shellDestinations()]))
|
||||
->not->toContain('data-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-app-shell :destinations="$destinations" hide-bar-on-scroll />', ['destinations' => shellDestinations()]))
|
||||
->toContain('data-hide-on-scroll')
|
||||
->toContain('x-data="materialNavigationBar"')
|
||||
->and((string) $this->blade('<x-app-shell :destinations="$destinations" />', ['destinations' => shellDestinations()]))
|
||||
->not->toContain('data-hide-on-scroll');
|
||||
});
|
||||
|
||||
it('exposes the rail that hides when collapsed', function () {
|
||||
expect((string) $this->blade('<x-app-shell :destinations="$destinations" hide-rail-when-collapsed />', ['destinations' => shellDestinations()]))
|
||||
->toContain('data-hide-when-collapsed')
|
||||
->toContain("materialNavigationRail('adaptive', true)")
|
||||
->and((string) $this->blade('<x-app-shell :destinations="$destinations" />', ['destinations' => shellDestinations()]))
|
||||
->not->toContain('data-hide-when-collapsed')
|
||||
->toContain("materialNavigationRail('adaptive', false)");
|
||||
});
|
||||
|
||||
it('reads the safe area and anything docked on the bar through variables an application can set', function () {
|
||||
$html = (string) $this->blade('<x-app-shell :destinations="$destinations" />', ['destinations' => shellDestinations()]);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use Illuminate\Support\Str;
|
||||
|
||||
it('draws a navigation landmark around its items', function () {
|
||||
expect((string) $this->blade('<x-navigation-bar><x-navigation-bar-item label="Inbox" icon="inbox" link="/inbox" /></x-navigation-bar>'))
|
||||
->toContain('<nav aria-label="Main" data-navigation-bar')
|
||||
->toMatch('/<nav\s+aria-label="Main"\s+data-navigation-bar/')
|
||||
->toContain('data-navigation-bar-items')
|
||||
->and((string) $this->blade('<x-navigation-bar label="Sections" />'))->toContain('aria-label="Sections"');
|
||||
});
|
||||
@@ -60,3 +60,46 @@ it('keeps the bar item on the label and state-layer colours M3 tokens', function
|
||||
// The indicator and the pill wash in on-secondary-container, as the rail's do (N-19).
|
||||
->toContain('[data-navigation-bar-item] :is([data-navigation-indicator], [data-navigation-pill])::before {');
|
||||
});
|
||||
|
||||
it('takes M3\'s tall container, which keeps the vertical item layout at every width', function () {
|
||||
expect((string) $this->blade('<x-navigation-bar tall><x-navigation-bar-item label="Inbox" icon="inbox" link="/inbox" /></x-navigation-bar>'))
|
||||
->toMatch('/data-navigation-bar\s+data-tall\s/')
|
||||
->and((string) $this->blade('<x-navigation-bar />'))->not->toContain('data-tall');
|
||||
|
||||
$css = file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css');
|
||||
|
||||
expect($css)
|
||||
// NavigationBarTokens.TallContainerHeight, 80dp, against the short bar's 64.
|
||||
->toMatch('/\[data-navigation-bar\]\[data-tall\] \[data-navigation-bar-items\] \{\s+min-height: 5rem;/')
|
||||
// The vertical layout is the base — a compact bar and a tall one at any width share it —
|
||||
// and only the short bar's horizontal layout is behind the 600px container query, so no
|
||||
// rule inside one reaches an item without saying `:not([data-tall])` first.
|
||||
->and(preg_match_all('/@container \(width [<>]=? 37\.5rem\) \{\n(.*?)\n \}/s', $css, $blocks) > 0)->toBeTrue()
|
||||
->and(implode("\n", $blocks[1]))->not->toMatch('/^ \[data-navigation-bar-item/m');
|
||||
});
|
||||
|
||||
it('hides on a scroll down and springs back on a scroll up', function () {
|
||||
expect((string) $this->blade('<x-navigation-bar hide-on-scroll />'))
|
||||
->toContain('data-hide-on-scroll')
|
||||
->toContain('x-data="materialNavigationBar"')
|
||||
->toContain('x-bind:data-hidden="away ? \'\' : null"')
|
||||
// Focus reaching the bar brings it back — the web's nearest answer to M3's "never hide it
|
||||
// while a screen reader is active".
|
||||
->toContain('x-on:focusin="show()"')
|
||||
->and((string) $this->blade('<x-navigation-bar />'))
|
||||
->not->toContain('data-hide-on-scroll')
|
||||
->not->toContain('materialNavigationBar');
|
||||
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'))
|
||||
// The spatial spring, which reduced motion zeroes along with every other duration token.
|
||||
->toMatch('/\[data-navigation-bar\]\[data-hide-on-scroll\] \{\s+transition: translate var\(--md-sys-motion-spatial-default-duration\) var\(--md-sys-motion-spatial-default\);/')
|
||||
->toMatch('/\[data-navigation-bar\]\[data-hide-on-scroll\]\[data-hidden\] \{\s+translate: 0 100%;/')
|
||||
// Unlayered, or the utility <x-app-shell> publishes the offset with would win.
|
||||
->toContain('[data-app-shell]:has([data-navigation-bar][data-hide-on-scroll][data-hidden]) {')
|
||||
->and(Str::of(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'))->after('[data-app-shell]:has(')->toString())
|
||||
->not->toContain('@layer');
|
||||
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/js/navigation.js'))
|
||||
// Never out from under a snackbar, a bottom sheet or a drawer resting on its edge.
|
||||
->toContain("document.querySelectorAll('[data-toast], [role=\"dialog\"]')");
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ it('gives the collapsible, modal and adaptive rails a menu button and the store'
|
||||
$collapsible = (string) $this->blade('<x-navigation-rail />');
|
||||
|
||||
expect($collapsible)
|
||||
->toContain('x-data="materialNavigationRail(\'collapsible\')"')
|
||||
->toContain('x-data="materialNavigationRail(\'collapsible\', false)"')
|
||||
->toContain('data-navigation-rail-menu')
|
||||
->toContain('aria-label="Collapse navigation"')
|
||||
->toContain('aria-expanded="true"')
|
||||
@@ -63,8 +63,72 @@ it('takes M3\'s optional divider and turns the container fill off', function ()
|
||||
// Neither reaches a rail open over a scrim, which is a surface over the page (N-22).
|
||||
->toContain('[data-navigation-rail][data-divider]:not([data-open]) > [data-navigation-rail-panel] {')
|
||||
->toContain("[data-navigation-rail][data-fill='false']:not([data-open]) > [data-navigation-rail-panel] {")
|
||||
// A collapsible rail is held to 96px where M3 asks for a navigation bar instead (N-24).
|
||||
->toMatch("/\\[data-navigation-rail='collapsible'\\] \\{\\s+width: 6rem;/");
|
||||
// A collapsible rail is held to its collapsed width where M3 asks for a bar instead (N-24).
|
||||
->toMatch("/\\[data-navigation-rail='collapsible'\\] \\{\\s+width: var\\(--navigation-rail-collapsed-width\\);/");
|
||||
});
|
||||
|
||||
it('takes M3\'s narrow collapsed width, icons alone but still named', function () {
|
||||
$narrow = (string) $this->blade('<x-navigation-rail width="narrow"><x-slot:header><button>FAB</button></x-slot:header><x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" /></x-navigation-rail>');
|
||||
|
||||
expect($narrow)
|
||||
->toContain('data-width="narrow"')
|
||||
// A narrow rail expands to the same 16rem; `width` names one or the other, never both.
|
||||
->toContain('--navigation-rail-width: 16rem')
|
||||
// 40px menu button and 56px FAB centred in 80px rather than in 96.
|
||||
->toContain('rail-collapsed:ps-5')
|
||||
->toContain('rail-collapsed:px-3')
|
||||
// The label is still rendered: it is what names the destination.
|
||||
->toContain('<span data-navigation-label>Inbox</span>')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="collapsed" width="20rem" />'))
|
||||
->not->toContain('data-width')
|
||||
->toContain('--navigation-rail-width: 20rem');
|
||||
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'))
|
||||
// NavigationRailCollapsedTokens.NarrowContainerWidth, 80dp against the default 96dp.
|
||||
->toMatch("/\\[data-navigation-rail\\]\\[data-width='narrow'\\] \\{\\s+--navigation-rail-collapsed-width: 5rem;/")
|
||||
->toMatch('/--navigation-rail-collapsed-width: 6rem;/')
|
||||
// Out of the drawing, not out of the page.
|
||||
->toContain("[data-navigation-rail][data-width='narrow'] [data-navigation-rail-item] [data-navigation-label] {");
|
||||
});
|
||||
|
||||
it('centres the destinations on request, leaving the header and footer at their ends', function () {
|
||||
expect((string) $this->blade('<x-navigation-rail mode="collapsed" align="center" />'))
|
||||
->toContain('data-align="center"')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="collapsed" />'))->not->toContain('data-align')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="collapsed" align="middle" />'))->not->toContain('data-align');
|
||||
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'))
|
||||
// Only the destinations move, and `safe` gives the top back when there are too many.
|
||||
->toMatch("/\\[data-navigation-rail\\]\\[data-align='center'\\] \\[data-navigation-rail-destinations\\] \\{\\s+justify-content: safe center;/");
|
||||
});
|
||||
|
||||
it('hides a collapsible or adaptive rail entirely when told to, and only those', function () {
|
||||
$hiding = (string) $this->blade('<x-navigation-rail hide-when-collapsed />');
|
||||
|
||||
expect($hiding)
|
||||
->toContain('data-hide-when-collapsed')
|
||||
->toContain("x-data=\"materialNavigationRail('collapsible', true)\"")
|
||||
// It needs the scrim and the focus trap a modal rail has: it comes back over the page.
|
||||
->toContain('data-navigation-rail-scrim')
|
||||
->toContain('x-trap.inert.noscroll="open"')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="adaptive" hide-when-collapsed />'))
|
||||
->toContain("x-data=\"materialNavigationRail('adaptive', true)\"")
|
||||
// A modal rail is already over the page; the two fixed modes mean what they say.
|
||||
->and((string) $this->blade('<x-navigation-rail mode="modal" hide-when-collapsed />'))
|
||||
->not->toContain('data-hide-when-collapsed')
|
||||
->toContain("materialNavigationRail('modal', false)")
|
||||
->and((string) $this->blade('<x-navigation-rail mode="expanded" hide-when-collapsed />'))
|
||||
->not->toContain('data-hide-when-collapsed')
|
||||
->and((string) $this->blade('<x-navigation-rail />'))->not->toContain('data-hide-when-collapsed');
|
||||
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'))
|
||||
->toMatch('/\[data-navigation-rail\]\[data-hide-when-collapsed\] \{\s+@variant rail-collapsed \{\s+width: 0;/')
|
||||
// The two bands where the window, not the visitor, collapses a rail keep theirs.
|
||||
->toMatch("/@media \\(width < 37\\.5rem\\) \\{\\s+\\[data-navigation-rail='collapsible'\\]\\[data-hide-when-collapsed\\]/")
|
||||
->toMatch("/@media \\(37\\.5rem <= width < 52\\.5rem\\) \\{\\s+\\[data-navigation-rail='adaptive'\\]\\[data-hide-when-collapsed\\]/")
|
||||
// Every collapsed branch stops while the rail is open, or what comes back would be narrow.
|
||||
->and(substr_count(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'), ':not([data-open])'))
|
||||
->toBeGreaterThanOrEqual(12);
|
||||
});
|
||||
|
||||
it('flattens a FAB nested in the rail header and morphs its label', function () {
|
||||
|
||||
Reference in New Issue
Block a user