Draw the navigation rail without Tailwind
Plan step 36 (navigation group, second batch): <x-navigation-rail>'s, <x-navigation-rail-item>'s and <x-navigation-rail-section>'s class lists move into navigation-rail.css, navigation-rail-item.css and navigation-rail-section.css, keyed on data-md-navigation-rail (the mode, data-md-width, data-md-align, data-md-hide-when-collapsed, data-md-divider, data-md-fill, data-md-open), data-md-navigation-rail- item (data-md-active) and data-md-navigation-rail-section. Every Tailwind wrapper class in the view — the menu row's centring padding, the FAB row, the two swapped menu glyphs, the brand's visibility — becomes a hook the stylesheet draws instead; the menu button itself renders the shared md-state-layer/md-focus-ring/md-touch-target classes (N-01's pattern) since it draws its own layer on itself, not a child. `<x-icon>` and `<x-badge>` take size and floating props instead of size/position classes; a small data-md-navigation-icon hook (the shared navigation-item.css) replaces the ad hoc "relative inline-flex" wrapper a floating badge anchors to. `rail-collapsed` (a Tailwind @custom-variant, forbidden in Phase F) is reproduced as plain selectors, branch for branch: the three width- independent conditions (a fixed collapsed mode; a collapsible rail the visitor collapsed and not open; a modal rail not open) merge into one :where() group, provably the same match set as three separate rules since :where(A, B, C) on an element is true exactly when :where(A) or :where(B) or :where(C) is; the four width-gated conditions stay separate media blocks, since CSS cannot merge different `@media` queries. Every rem length becomes px, since these are dp-based M3 tokens, not a text measure (unlike <x-pane>'s rem widths). The FAB overrides for a rail's header — elevation 0 (N-03), morphing into an extended FAB instead of swapping two by display (N-23) — move from unlayered into this file's own material.components, like toolbar.css's FAB override: fab.css's `[data-md-fab]` is one attribute, so a doubled selector here always outranks it without needing to sit outside the layer. navigation.js: the arriving-indicator stylesheet and every code comment follow the new hooks; resources/css/components/navigation.css is deleted (nothing imports it any more) and its line in tailwind.css with it. Behaviour is unchanged except one thing Tailwind's `rail-collapsed:` variant could do that plain CSS in this shape cannot: it is gone for consuming applications too, since the definition lived only in the file this commit removes. The development skill's guidance for it is rewritten to point at navigation-rail.css's own selectors instead of teaching a Tailwind variant that no longer exists. 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
63d421ccb6
commit
25549374ec
@@ -75,3 +75,6 @@
|
||||
@import './components/navigation-item.css';
|
||||
@import './components/navigation-bar.css';
|
||||
@import './components/navigation-bar-item.css';
|
||||
@import './components/navigation-rail.css';
|
||||
@import './components/navigation-rail-item.css';
|
||||
@import './components/navigation-rail-section.css';
|
||||
|
||||
@@ -0,0 +1,645 @@
|
||||
/*
|
||||
* `<x-navigation-rail-item>`: one destination in an `<x-navigation-rail>`, drawn in whichever
|
||||
* shape the rail has (NavigationRailVerticalItemTokens.kt, NavigationRailHorizontalItemTokens.kt,
|
||||
* NavigationRailBaselineItemTokens.kt, NavigationRailColorTokens.kt, androidx Compose Material 3,
|
||||
* Apache-2.0).
|
||||
*
|
||||
* [data-md-navigation-rail-item] data-md-active; the link or button itself
|
||||
* [data-md-navigation-indicator] the 56×32 indicator, collapsed only
|
||||
* [data-md-navigation-icon] the floating badge's anchor (navigation-item.css)
|
||||
* [data-md-navigation-badge-icon] the small/large badge, collapsed only
|
||||
* [data-md-navigation-label]
|
||||
* [data-md-navigation-badge-end] the large badge at the label's end, expanded only
|
||||
*
|
||||
* Expanded, a 56px full-width pill: the icon, the label-large label beside it (up to two lines,
|
||||
* collapsed) and a count at its end. Collapsed, the icon sits in a 56×32 `CornerFull` indicator
|
||||
* over a label-medium label, with the count on the icon instead — badges move from the icon
|
||||
* (collapsed) to the label end (expanded), M3's stated rule.
|
||||
*
|
||||
* The item's own state layer is split in two: while expanded the layer paints the whole item (the
|
||||
* 56px pill *is* the indicator); while collapsed it paints only the 56×32 box. A CSS variant
|
||||
* selector cannot follow a pseudo-element, so `--navigation-item-layer`/`--navigation-indicator-layer`
|
||||
* hand `--navigation-layer` (navigation-item.css) on through variables, and each rule below
|
||||
* outweighs navigation-item.css's shared one with a doubled `[data-md-navigation-rail-item]`
|
||||
* attribute, the same specificity trick `navigation-rail.css` uses for the header's FAB.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
@import './badge.css';
|
||||
@import './navigation-item.css';
|
||||
|
||||
@layer material.components {
|
||||
[data-md-navigation-rail-item] {
|
||||
--navigation-layer: 0;
|
||||
--navigation-item-layer: var(--navigation-layer);
|
||||
--navigation-indicator-layer: 0;
|
||||
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 56px;
|
||||
margin-inline: 20px;
|
||||
padding-inline: 16px;
|
||||
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;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item][data-md-navigation-rail-item]::before {
|
||||
background-color: var(--md-sys-color-on-secondary-container);
|
||||
opacity: var(--navigation-item-layer);
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item][data-md-navigation-rail-item] [data-md-navigation-indicator]::before {
|
||||
background-color: var(--md-sys-color-on-secondary-container);
|
||||
opacity: var(--navigation-indicator-layer);
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item][data-md-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);
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item] [data-md-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);
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item] [data-md-navigation-label] {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item]:focus-visible {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* The large badge at the label's end, expanded only — collapsed, the badge moves onto the
|
||||
icon instead (`[data-md-navigation-badge-icon]`, `display: contents` there). */
|
||||
[data-md-navigation-badge-end] {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-md-navigation-badge-icon] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ---- Collapsed: icon over label, the 56×32 indicator, badge back on the icon. ---- */
|
||||
|
||||
[data-md-navigation-rail-item] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
height: auto;
|
||||
min-height: 64px;
|
||||
margin-inline: 0;
|
||||
padding: 6px 4px;
|
||||
border-radius: 0;
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
height: auto;
|
||||
min-height: 64px;
|
||||
margin-inline: 0;
|
||||
padding: 6px 4px;
|
||||
border-radius: 0;
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
height: auto;
|
||||
min-height: 64px;
|
||||
margin-inline: 0;
|
||||
padding: 6px 4px;
|
||||
border-radius: 0;
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
height: auto;
|
||||
min-height: 64px;
|
||||
margin-inline: 0;
|
||||
padding: 6px 4px;
|
||||
border-radius: 0;
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
height: auto;
|
||||
min-height: 64px;
|
||||
margin-inline: 0;
|
||||
padding: 6px 4px;
|
||||
border-radius: 0;
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
--navigation-item-layer: 0;
|
||||
--navigation-indicator-layer: var(--navigation-layer);
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
--navigation-item-layer: 0;
|
||||
--navigation-indicator-layer: var(--navigation-layer);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
--navigation-item-layer: 0;
|
||||
--navigation-indicator-layer: var(--navigation-layer);
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
--navigation-item-layer: 0;
|
||||
--navigation-indicator-layer: var(--navigation-layer);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
--navigation-item-layer: 0;
|
||||
--navigation-indicator-layer: var(--navigation-layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item] [data-md-navigation-label] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
flex: none;
|
||||
display: -webkit-box;
|
||||
max-width: 100%;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
flex: none;
|
||||
display: -webkit-box;
|
||||
max-width: 100%;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
flex: none;
|
||||
display: -webkit-box;
|
||||
max-width: 100%;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
flex: none;
|
||||
display: -webkit-box;
|
||||
max-width: 100%;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
flex: none;
|
||||
display: -webkit-box;
|
||||
max-width: 100%;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item][data-md-active] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
background-image: none;
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
background-image: none;
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
background-image: none;
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
background-image: none;
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
background-image: none;
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item] [data-md-navigation-indicator] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
width: 56px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
width: 56px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
width: 56px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
width: 56px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
width: 56px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item][data-md-active] [data-md-navigation-indicator] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item]:focus-visible {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-item]:focus-visible [data-md-navigation-indicator] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-badge-end],
|
||||
[data-md-navigation-badge-icon] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-badge-icon] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* `<x-navigation-rail-section>`: a group of `<x-navigation-rail-item>`s under a heading, in an
|
||||
* `<x-navigation-rail>`. M3 draws a section header only while the rail is expanded; collapsed, a
|
||||
* little space sets the group apart instead, and the heading keeps naming the group for a screen
|
||||
* reader either way (`role="group"`/`aria-labelledby`, rendered by the view).
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
[data-md-navigation-rail-section] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-section]:not(:first-child) {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-heading] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 48px;
|
||||
padding-inline: 36px 20px;
|
||||
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;
|
||||
}
|
||||
|
||||
[data-md-navigation-rail-heading] {
|
||||
&:where(
|
||||
[data-md-navigation-rail='collapsed'],
|
||||
[data-md-navigation-rail='collapsed'] *,
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]),
|
||||
[data-md-navigation-rail='modal']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 840px) {
|
||||
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (840px <= width < 1200px) {
|
||||
&:where(
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
&:where(
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
|
||||
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,9 @@
|
||||
*
|
||||
* `$store.rail.collapsed` is the visitor's choice for a collapsible rail, remembered in
|
||||
* localStorage. <x-theme-script> has already applied it before the first paint as
|
||||
* <html data-rail="expanded|collapsed">, which is what the stylesheet keys on (the
|
||||
* `rail-collapsed:` variant); the store starts from that attribute and writes it back.
|
||||
* <html data-rail="expanded|collapsed">, which is what navigation-rail.css keys on (every branch
|
||||
* of "collapsed" reads it, or `data-rail-auto` where no choice has been made); the store starts
|
||||
* from that attribute and writes it back.
|
||||
* `$store.rail.auto` is true while nothing is stored — the value is `rail.default`, not a choice —
|
||||
* and the adaptive rail then takes its window size class's default instead: collapsed in the
|
||||
* expanded class (840–1199), expanded from `large` (1200), as M3 asks. The first `set()` drops it.
|
||||
@@ -25,13 +26,13 @@ import { from, upTo } from './breakpoints.js'
|
||||
* new page's indicator is new markup, so the only way to animate it is a starting style — and
|
||||
* only while a navigation swaps the page in, or every full load would animate it too. The sheet
|
||||
* is adopted as the navigation starts and dropped two frames after it ends; the transition itself
|
||||
* is resources/css/components/navigation.css.
|
||||
* is navigation-item.css.
|
||||
*/
|
||||
const arriving = new CSSStyleSheet()
|
||||
|
||||
arriving.replaceSync(`@starting-style {
|
||||
:is([data-navigation-bar-item], [data-navigation-rail-item])[data-active],
|
||||
:is([data-navigation-bar-item], [data-navigation-rail-item])[data-active] :is([data-navigation-indicator], [data-navigation-pill]) {
|
||||
:is([data-md-navigation-bar-item], [data-md-navigation-rail-item])[data-md-active],
|
||||
:is([data-md-navigation-bar-item], [data-md-navigation-rail-item])[data-md-active] :is([data-md-navigation-indicator], [data-md-navigation-pill]) {
|
||||
background-size: 0% 100%;
|
||||
}
|
||||
}`)
|
||||
@@ -135,7 +136,7 @@ document.addEventListener('alpine:init', () => {
|
||||
|
||||
// From `large` (1200px) it starts expanded rather than collapsed, until someone
|
||||
// chooses otherwise; below that the expanded class starts it collapsed. Both mirror
|
||||
// the `rail-collapsed:` variant in resources/css/components/navigation.css.
|
||||
// navigation-rail.css's own branches for those two bands.
|
||||
this.watch(from('large'), (matches) => (this.roomy = matches))
|
||||
},
|
||||
|
||||
@@ -179,7 +180,7 @@ document.addEventListener('alpine:init', () => {
|
||||
* 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.
|
||||
* are in navigation-rail.css.
|
||||
*/
|
||||
get away() {
|
||||
if (!hideWhenCollapsed || this.cramped || this.standing) {
|
||||
|
||||
@@ -43,32 +43,32 @@
|
||||
'wire:navigate' => $isLink && ! $external && ! $noWireNavigate && ! $attributes->has('wire:navigate') ? true : null,
|
||||
'type' => $isLink ? null : 'button',
|
||||
'aria-current' => $active ? 'page' : null,
|
||||
'data-active' => $active ? true : null,
|
||||
'data-md-active' => $active ? true : null,
|
||||
], fn ($value): bool => $value !== null));
|
||||
@endphp
|
||||
|
||||
<{{ $tag }} data-navigation-rail-item {{ $attributes }}>
|
||||
<span data-navigation-indicator>
|
||||
<span class="relative inline-flex">
|
||||
<{{ $tag }} data-md-navigation-rail-item {{ $attributes }}>
|
||||
<span data-md-navigation-indicator>
|
||||
<span data-md-navigation-icon>
|
||||
@if ($icon)
|
||||
<x-livewire-material::icon :name="$icon" :filled="$active" class="size-6" />
|
||||
<x-livewire-material::icon :name="$icon" :filled="$active" />
|
||||
@endif
|
||||
|
||||
@if ($dot)
|
||||
<x-livewire-material::badge floating />
|
||||
@elseif ($count)
|
||||
<span class="hidden rail-collapsed:contents"><x-livewire-material::badge :value="$badge" max="999" floating /></span>
|
||||
<span data-md-navigation-badge-icon><x-livewire-material::badge :value="$badge" max="999" floating /></span>
|
||||
@endif
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span data-navigation-label>{{ $label ?? $slot }}</span>
|
||||
<span data-md-navigation-label>{{ $label ?? $slot }}</span>
|
||||
|
||||
@if ($count)
|
||||
<span class="flex shrink-0 rail-collapsed:hidden"><x-livewire-material::badge :value="$badge" max="999" /></span>
|
||||
<span data-md-navigation-badge-end><x-livewire-material::badge :value="$badge" max="999" /></span>
|
||||
@endif
|
||||
|
||||
@if ($spoken !== null)
|
||||
<span class="sr-only">, {{ $spoken }}</span>
|
||||
<span class="md-visually-hidden">, {{ $spoken }}</span>
|
||||
@endif
|
||||
</{{ $tag }}>
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
$headingId = 'navigation-section-'.substr(md5((string) $label), 0, 10);
|
||||
@endphp
|
||||
|
||||
<div role="group" aria-labelledby="{{ $headingId }}" data-navigation-rail-section {{ $attributes }}>
|
||||
<p id="{{ $headingId }}" data-navigation-rail-heading>{{ $label }}</p>
|
||||
<div role="group" aria-labelledby="{{ $headingId }}" data-md-navigation-rail-section {{ $attributes }}>
|
||||
<p id="{{ $headingId }}" data-md-navigation-rail-heading>{{ $label }}</p>
|
||||
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
@@ -45,18 +45,18 @@
|
||||
`footer`, pinned to the foot. Header and footer never scroll, so nothing in them is cut off by
|
||||
the scroller's edge.
|
||||
|
||||
Anything else inside can take both shapes with the `rail-collapsed:` variant, which applies
|
||||
while the rail is drawn collapsed for whatever reason:
|
||||
`<span class="rail-collapsed:hidden">…only while expanded…</span>`
|
||||
`<span class="hidden rail-collapsed:inline-flex">…only while collapsed…</span>`.
|
||||
Nothing that shows while collapsed may be wider than 96px.
|
||||
Anything else inside can take both shapes by matching the rail's own state directly, the same
|
||||
selectors resources/css/components/navigation-rail.css uses for every branch of "collapsed" —
|
||||
`data-md-navigation-rail`'s value, `:not([data-md-open])`, and the window band each mode
|
||||
collapses in; that file is the one place the numbers need to stay right, rather than a
|
||||
Tailwind variant repeating them. Nothing that shows while collapsed may be wider than 96px.
|
||||
|
||||
Props: `label` names the landmark ("Main"); `width` is the expanded width (`16rem`, held
|
||||
Props: `label` names the landmark ("Main"); `width` is the expanded width (`256px`, held
|
||||
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
|
||||
narrow rail expands to the default 256px; `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
|
||||
@@ -82,12 +82,12 @@
|
||||
https://github.com/androidx/androidx/tree/androidx-main/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3
|
||||
— surface (surface-container and elevation 2 with a large inner corner when modal), 44px above
|
||||
the header and 40px under it, 4px between collapsed items. The styles are
|
||||
resources/css/components/navigation.css; the behaviour resources/js/navigation.js. --}}
|
||||
resources/css/components/navigation-rail.css; the behaviour resources/js/navigation.js. --}}
|
||||
|
||||
@props([
|
||||
'mode' => 'collapsible',
|
||||
'label' => null,
|
||||
'width' => '16rem',
|
||||
'width' => '256px',
|
||||
'align' => 'top',
|
||||
'hideWhenCollapsed' => false,
|
||||
'menu' => null,
|
||||
@@ -105,29 +105,29 @@
|
||||
$menu ??= $interactive;
|
||||
$collapsedAtFirst = in_array($mode, ['collapsed', 'modal'], true);
|
||||
$narrow = $width === 'narrow';
|
||||
$width = $narrow ? '16rem' : $width;
|
||||
$width = $narrow ? '256px' : $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
|
||||
data-md-navigation-rail="{{ $mode }}"
|
||||
@if ($narrow) data-md-width="narrow" @endif
|
||||
@if ($centred) data-md-align="center" @endif
|
||||
@if ($hideWhenCollapsed) data-md-hide-when-collapsed @endif
|
||||
@if ($divider) data-md-divider @endif
|
||||
@unless ($fill) data-md-fill="false" @endunless
|
||||
@if ($interactive)
|
||||
x-data="materialNavigationRail('{{ $mode }}', {{ $hideWhenCollapsed ? 'true' : 'false' }})"
|
||||
x-bind:data-open="open"
|
||||
x-bind:data-md-open="open"
|
||||
@endif
|
||||
{{ $attributes->merge(['style' => "--navigation-rail-width: {$width}"]) }}
|
||||
>
|
||||
@if ($canOpen)
|
||||
<div data-navigation-rail-scrim aria-hidden="true" x-on:click="$store.rail.hide()"></div>
|
||||
<div data-md-navigation-rail-scrim aria-hidden="true" x-on:click="$store.rail.hide()"></div>
|
||||
@endif
|
||||
|
||||
<nav
|
||||
data-navigation-rail-panel
|
||||
data-md-navigation-rail-panel
|
||||
aria-label="{{ $label ?? __('Main') }}"
|
||||
@if ($canOpen)
|
||||
x-trap.inert.noscroll="open"
|
||||
@@ -135,46 +135,43 @@
|
||||
@endif
|
||||
>
|
||||
@if ($menu || isset($brand) || isset($header))
|
||||
<div data-navigation-rail-header>
|
||||
<div data-md-navigation-rail-header>
|
||||
@if ($menu || isset($brand))
|
||||
{{-- 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])>
|
||||
<div data-md-navigation-rail-menu-row>
|
||||
@if ($menu)
|
||||
<button
|
||||
type="button"
|
||||
data-navigation-rail-menu
|
||||
data-md-navigation-rail-menu
|
||||
aria-label="{{ $collapsedAtFirst ? __('Expand navigation') : __('Collapse navigation') }}"
|
||||
aria-expanded="{{ $collapsedAtFirst ? 'false' : 'true' }}"
|
||||
x-on:click="menu()"
|
||||
x-bind:aria-label="expanded ? @js(__('Collapse navigation')) : @js(__('Expand navigation'))"
|
||||
x-bind:aria-expanded="expanded.toString()"
|
||||
class="state-layer touch-target focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full text-on-surface-variant"
|
||||
class="md-state-layer md-focus-ring md-touch-target"
|
||||
>
|
||||
<span class="contents rail-collapsed:hidden"><x-livewire-material::icon name="menu_open" class="size-6" /></span>
|
||||
<span class="hidden rail-collapsed:contents"><x-livewire-material::icon name="menu" class="size-6" /></span>
|
||||
<span data-md-navigation-rail-menu-collapse-icon><x-livewire-material::icon name="menu_open" /></span>
|
||||
<span data-md-navigation-rail-menu-expand-icon><x-livewire-material::icon name="menu" /></span>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@isset($brand)
|
||||
<div class="min-w-0 flex-1 rail-collapsed:hidden">{{ $brand }}</div>
|
||||
<div data-md-navigation-rail-brand>{{ $brand }}</div>
|
||||
@endisset
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@isset($header)
|
||||
{{-- 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>
|
||||
<div data-md-navigation-rail-fab-row {{ $header->attributes }}>{{ $header }}</div>
|
||||
@endisset
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div data-navigation-rail-destinations>
|
||||
<div data-md-navigation-rail-destinations>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
@isset($footer)
|
||||
<div data-navigation-rail-footer {{ $footer->attributes }}>{{ $footer }}</div>
|
||||
<div data-md-navigation-rail-footer {{ $footer->attributes }}>{{ $footer }}</div>
|
||||
@endisset
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
|
||||
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
||||
|
||||
it('draws each mode, falling back to collapsible', function (string $mode, string $expected) {
|
||||
expect((string) $this->blade('<x-navigation-rail :mode="$mode"><x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" /></x-navigation-rail>', ['mode' => $mode]))
|
||||
->toContain("data-navigation-rail=\"{$expected}\"")
|
||||
->toContain("data-md-navigation-rail=\"{$expected}\"")
|
||||
->toContain('<nav')
|
||||
->toContain('aria-label="Main"');
|
||||
})->with([
|
||||
@@ -19,26 +21,26 @@ it('gives the collapsible, modal and adaptive rails a menu button and the store'
|
||||
|
||||
expect($collapsible)
|
||||
->toContain('x-data="materialNavigationRail(\'collapsible\', false)"')
|
||||
->toContain('data-navigation-rail-menu')
|
||||
->toContain('data-md-navigation-rail-menu')
|
||||
->toContain('aria-label="Collapse navigation"')
|
||||
->toContain('aria-expanded="true"')
|
||||
// The 40px menu button reaches M3's 48px target through the shared utility (N-01).
|
||||
->toContain('touch-target')
|
||||
->not->toContain('data-navigation-rail-scrim')
|
||||
// The 40px menu button reaches M3's 48px target through the shared classes (N-01).
|
||||
->toContain('class="md-state-layer md-focus-ring md-touch-target"')
|
||||
->not->toContain('data-md-navigation-rail-scrim')
|
||||
->not->toContain('x-trap')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="modal" />'))
|
||||
->toContain('aria-label="Expand navigation"')
|
||||
->toContain('data-navigation-rail-scrim')
|
||||
->toContain('data-md-navigation-rail-scrim')
|
||||
->toContain('x-trap.inert.noscroll="open"')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="expanded" />'))
|
||||
->not->toContain('data-navigation-rail-menu')
|
||||
->not->toContain('data-md-navigation-rail-menu')
|
||||
->not->toContain('x-data')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="collapsible" :menu="false" />'))->not->toContain('data-navigation-rail-menu');
|
||||
->and((string) $this->blade('<x-navigation-rail mode="collapsible" :menu="false" />'))->not->toContain('data-md-navigation-rail-menu');
|
||||
});
|
||||
|
||||
it('keeps the header and footer out of the scrolling destinations', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-navigation-rail mode="expanded" width="20rem">
|
||||
<x-navigation-rail mode="expanded" width="320px">
|
||||
<x-slot:brand><span>Brand</span></x-slot:brand>
|
||||
<x-slot:header><button>FAB</button></x-slot:header>
|
||||
<x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" />
|
||||
@@ -47,117 +49,163 @@ it('keeps the header and footer out of the scrolling destinations', function ()
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
->toContain('--navigation-rail-width: 20rem')
|
||||
->toMatch('/data-navigation-rail-header.*Brand.*FAB.*data-navigation-rail-destinations.*Inbox.*data-navigation-rail-footer.*Account/s');
|
||||
->toContain('--navigation-rail-width: 320px')
|
||||
->toMatch('/data-md-navigation-rail-header.*Brand.*FAB.*data-md-navigation-rail-destinations.*Inbox.*data-md-navigation-rail-footer.*Account/s');
|
||||
});
|
||||
|
||||
it('takes M3\'s optional divider and turns the container fill off', function () {
|
||||
expect((string) $this->blade('<x-navigation-rail mode="expanded" divider :fill="false" />'))
|
||||
->toContain('data-divider')
|
||||
->toContain('data-fill="false"')
|
||||
->toContain('data-md-divider')
|
||||
->toContain('data-md-fill="false"')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="expanded" />'))
|
||||
->not->toContain('data-divider')
|
||||
->not->toContain('data-fill');
|
||||
->not->toContain('data-md-divider')
|
||||
->not->toContain('data-md-fill');
|
||||
|
||||
$css = ComponentStylesheet::read('navigation-rail');
|
||||
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'))
|
||||
// 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] {")
|
||||
expect($css->declarations('[data-md-navigation-rail][data-md-divider]:not([data-md-open]) > [data-md-navigation-rail-panel]'))
|
||||
->toBe(['border-inline-end' => '1px solid var(--md-sys-color-outline-variant)'])
|
||||
->and($css->declarations('[data-md-navigation-rail][data-md-fill=\'false\']:not([data-md-open]) > [data-md-navigation-rail-panel]'))
|
||||
->toBe(['background-color' => 'transparent'])
|
||||
// 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\\);/");
|
||||
->and($css->declarations('[data-md-navigation-rail=\'collapsible\']', ['@media (width < 600px)']))
|
||||
->toBe(['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')
|
||||
->toContain('data-md-width="narrow"')
|
||||
// A narrow rail expands to the same 256px; `width` names one or the other, never both.
|
||||
->toContain('--navigation-rail-width: 256px')
|
||||
->toContain('data-md-navigation-rail-menu-row')
|
||||
->toContain('data-md-navigation-rail-fab-row')
|
||||
// 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');
|
||||
->toContain('<span data-md-navigation-label>Inbox</span>')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="collapsed" width="320px" />'))
|
||||
->not->toContain('data-md-width')
|
||||
->toContain('--navigation-rail-width: 320px');
|
||||
|
||||
$css = ComponentStylesheet::read('navigation-rail');
|
||||
|
||||
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] {");
|
||||
expect($css->declarations('[data-md-navigation-rail][data-md-width=\'narrow\']'))
|
||||
->toBe(['--navigation-rail-collapsed-width' => '80px'])
|
||||
->and($css->declarations('[data-md-navigation-rail]'))
|
||||
->toHaveKey('--navigation-rail-collapsed-width', '96px')
|
||||
// Out of the drawing, not out of the page: clipped only while collapsed.
|
||||
->and($css->declarations("[data-md-navigation-rail][data-md-width='narrow'] [data-md-navigation-rail-item] [data-md-navigation-label]:where( [data-md-navigation-rail='collapsed'], [data-md-navigation-rail='collapsed'] *, [data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]), [data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *, [data-md-navigation-rail='modal']:not([data-md-open]), [data-md-navigation-rail='modal']:not([data-md-open]) * )"))
|
||||
->toHaveKey('clip-path', 'inset(50%)');
|
||||
});
|
||||
|
||||
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');
|
||||
->toContain('data-md-align="center"')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="collapsed" />'))->not->toContain('data-md-align')
|
||||
->and((string) $this->blade('<x-navigation-rail mode="collapsed" align="middle" />'))->not->toContain('data-md-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;/");
|
||||
expect(ComponentStylesheet::read('navigation-rail')->declarations("[data-md-navigation-rail][data-md-align='center'] [data-md-navigation-rail-destinations]"))
|
||||
->toBe(['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('data-md-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('data-md-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')
|
||||
->not->toContain('data-md-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');
|
||||
->not->toContain('data-md-hide-when-collapsed')
|
||||
->and((string) $this->blade('<x-navigation-rail />'))->not->toContain('data-md-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;/')
|
||||
$css = ComponentStylesheet::read('navigation-rail');
|
||||
|
||||
expect($css->declarations("[data-md-navigation-rail][data-md-hide-when-collapsed]:where( [data-md-navigation-rail='collapsed'], [data-md-navigation-rail='collapsed'] *, [data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]), [data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *, [data-md-navigation-rail='modal']:not([data-md-open]), [data-md-navigation-rail='modal']:not([data-md-open]) * )"))
|
||||
->toBe(['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);
|
||||
->and($css->declarations("[data-md-navigation-rail='collapsible'][data-md-hide-when-collapsed]", ['@media (width < 600px)']))
|
||||
->toBe(['width' => 'var(--navigation-rail-collapsed-width)'])
|
||||
->and($css->declarations("[data-md-navigation-rail='adaptive'][data-md-hide-when-collapsed]", ['@media (600px <= width < 840px)']))
|
||||
->toBe(['width' => 'var(--navigation-rail-collapsed-width)']);
|
||||
});
|
||||
|
||||
/**
|
||||
* The Tailwind-era `rail-collapsed:` custom variant had seven branches for "this rail is drawn
|
||||
* collapsed"; navigation-rail.css merges the three that carry no media query into one `:where()`
|
||||
* (provably the same match set: an element matches `X:where(A, B, C)` exactly when it matches
|
||||
* `X:where(A)` or `X:where(B)` or `X:where(C)`) and keeps the four media-gated ones separate, since
|
||||
* CSS cannot merge different `@media` queries into one rule. This pins all five physical branches
|
||||
* — and so every one of the seven original conditions — for the rail's own width, the property
|
||||
* every branch sets.
|
||||
*/
|
||||
it('reproduces every branch of the old rail-collapsed variant for the rail\'s own width', function () {
|
||||
$css = ComponentStylesheet::read('navigation-rail');
|
||||
|
||||
// Branches 1-3: a fixed `collapsed` mode; a `collapsible` rail the visitor collapsed and not
|
||||
// open; a `modal` rail not open.
|
||||
expect($css->declarations("[data-md-navigation-rail]:where( [data-md-navigation-rail='collapsed'], [data-md-navigation-rail='collapsed'] *, [data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]), [data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *, [data-md-navigation-rail='modal']:not([data-md-open]), [data-md-navigation-rail='modal']:not([data-md-open]) * )"))
|
||||
->toBe(['width' => 'var(--navigation-rail-collapsed-width)'])
|
||||
// Branch 4: below `medium` (600px), a `collapsible` rail regardless of choice.
|
||||
->and($css->declarations("[data-md-navigation-rail]:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *)", ['@media (width < 600px)']))
|
||||
->toBe(['width' => 'var(--navigation-rail-collapsed-width)'])
|
||||
// Branch 5: below `expanded` (840px), an `adaptive` rail not open.
|
||||
->and($css->declarations("[data-md-navigation-rail]:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *)", ['@media (width < 840px)']))
|
||||
->toBe(['width' => 'var(--navigation-rail-collapsed-width)'])
|
||||
// Branch 6: at `expanded` (840-1199), an `adaptive` rail not open, chosen collapsed or no
|
||||
// choice yet (`data-rail-auto`).
|
||||
->and($css->declarations("[data-md-navigation-rail]:where( :is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]), :is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) * )", ['@media (840px <= width < 1200px)']))
|
||||
->toBe(['width' => 'var(--navigation-rail-collapsed-width)'])
|
||||
// Branch 7: from `large` (1200px), an `adaptive` rail not open, only when chosen collapsed —
|
||||
// M3 starts it expanded there by default.
|
||||
->and($css->declarations("[data-md-navigation-rail]:where( [data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]), [data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) * )", ['@media (width >= 1200px)']))
|
||||
->toBe(['width' => 'var(--navigation-rail-collapsed-width)']);
|
||||
});
|
||||
|
||||
it('flattens a FAB nested in the rail header and morphs its label', function () {
|
||||
// Both rules hang off `data-md-fab` on <x-fab>'s root, and both are unlayered, because what they
|
||||
// beat — the FAB's shadow, its gap, an extended FAB's minimum width — are utilities.
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css'))
|
||||
// A nested FAB rests at elevation 0, not the 3 a standalone one has (N-03).
|
||||
->toContain('[data-navigation-rail-header] [data-md-fab],')
|
||||
->toContain('[data-navigation-rail-header] [data-md-fab]:hover {')
|
||||
$css = ComponentStylesheet::read('navigation-rail');
|
||||
|
||||
// A nested FAB rests at elevation 0, not the 3 a standalone one has (N-03). Now layered, like
|
||||
// toolbar.css's own docked-FAB override: fab.css's `[data-md-fab]` is one attribute, so the
|
||||
// doubled selector here (two) always wins without needing to sit outside the layer.
|
||||
expect($css->declarations('[data-md-navigation-rail-header] [data-md-fab][data-md-fab], [data-md-navigation-rail-header] [data-md-fab][data-md-fab]:hover'))
|
||||
->toBe(['box-shadow' => 'none'])
|
||||
// One FAB whose label springs shut, not two swapped by display (N-23).
|
||||
->toContain('[data-navigation-rail-header] [data-md-fab] > span {')
|
||||
->toMatch('/@variant rail-collapsed \{\s+min-width: 0;\s+aspect-ratio: 1;\s+gap: 0;/');
|
||||
->and($css->declarations('[data-md-navigation-rail-header] [data-md-fab] > span'))
|
||||
->toHaveKey('max-width', '256px');
|
||||
|
||||
expect($css->declarations("[data-md-navigation-rail-header] [data-md-fab] > span:where( [data-md-navigation-rail='collapsed'], [data-md-navigation-rail='collapsed'] *, [data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]), [data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *, [data-md-navigation-rail='modal']:not([data-md-open]), [data-md-navigation-rail='modal']:not([data-md-open]) * )"))
|
||||
->toBe(['max-width' => '0', 'opacity' => '0'])
|
||||
->and($css->declarations("[data-md-navigation-rail-header] [data-md-fab][data-md-extended]:where( [data-md-navigation-rail='collapsed'], [data-md-navigation-rail='collapsed'] *, [data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]), [data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *, [data-md-navigation-rail='modal']:not([data-md-open]), [data-md-navigation-rail='modal']:not([data-md-open]) * )"))
|
||||
->toBe(['min-inline-size' => '0', 'aspect-ratio' => '1', 'gap' => '0']);
|
||||
});
|
||||
|
||||
it('draws a destination in both shapes, with its count on the icon and at the end', function () {
|
||||
$html = (string) $this->blade('<x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" active badge="12" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('<a data-navigation-rail-item')
|
||||
->toContain('<a data-md-navigation-rail-item')
|
||||
->toContain('wire:navigate')
|
||||
->toContain('aria-current="page"')
|
||||
->toContain('data-navigation-indicator')
|
||||
->toContain('<span data-navigation-label>Inbox</span>')
|
||||
->toContain('hidden rail-collapsed:contents')
|
||||
->toContain('rail-collapsed:hidden')
|
||||
->toContain('<span class="sr-only">, 12</span>')
|
||||
->toContain('data-md-navigation-indicator')
|
||||
->toContain('<span data-md-navigation-label>Inbox</span>')
|
||||
->toContain('data-md-navigation-badge-icon')
|
||||
->toContain('data-md-navigation-badge-end')
|
||||
->toContain('<span class="md-visually-hidden">, 12</span>')
|
||||
->and(substr_count($html, '>12</span>'))->toBe(2)
|
||||
->and((string) $this->blade('<x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" no-wire-navigate />'))->not->toContain('wire:navigate')
|
||||
->and((string) $this->blade('<x-navigation-rail-item label="Inbox" icon="inbox" />'))->toContain('<button data-navigation-rail-item');
|
||||
->and((string) $this->blade('<x-navigation-rail-item label="Inbox" icon="inbox" />'))->toContain('<button data-md-navigation-rail-item');
|
||||
});
|
||||
|
||||
it('groups destinations under a heading that names the group', function () {
|
||||
@@ -167,5 +215,16 @@ it('groups destinations under a heading that names the group', function () {
|
||||
|
||||
expect($html)->toContain('role="group"')
|
||||
->and($labelledBy[1] ?? null)->not->toBeNull()
|
||||
->and($html)->toContain("<p id=\"{$labelledBy[1]}\" data-navigation-rail-heading>Labels</p>");
|
||||
->and($html)->toContain("<p id=\"{$labelledBy[1]}\" data-md-navigation-rail-heading>Labels</p>");
|
||||
});
|
||||
|
||||
it('draws the item\'s own state layer split between the item and its indicator', function () {
|
||||
$item = ComponentStylesheet::read('navigation-rail-item');
|
||||
|
||||
expect($item->declarations('[data-md-navigation-rail-item]'))
|
||||
->toHaveKeys(['--navigation-item-layer', '--navigation-indicator-layer'])
|
||||
->and($item->declarations('[data-md-navigation-rail-item][data-md-navigation-rail-item]::before'))
|
||||
->toBe(['background-color' => 'var(--md-sys-color-on-secondary-container)', 'opacity' => 'var(--navigation-item-layer)'])
|
||||
->and($item->declarations('[data-md-navigation-rail-item][data-md-navigation-rail-item] [data-md-navigation-indicator]::before'))
|
||||
->toBe(['background-color' => 'var(--md-sys-color-on-secondary-container)', 'opacity' => 'var(--navigation-indicator-layer)']);
|
||||
});
|
||||
|
||||
@@ -26,6 +26,9 @@ dataset('navigation components', [
|
||||
'toolbar',
|
||||
'tabs',
|
||||
'navigation-bar-item',
|
||||
'navigation-rail',
|
||||
'navigation-rail-item',
|
||||
'navigation-rail-section',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user