diff --git a/UPGRADE.md b/UPGRADE.md index 2b080512..1cf581b8 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,15 @@ # Upgrading +## From 2.1.0 to 2.1.1 + +- **`$store.rail.toggle()`** flips what the first rail on the page draws, as its menu button does. + With nothing stored, ``'s rail is drawn collapsed from 840 to 1199px whatever + `rail.default` says, and `toggle()` collapsed it again, so the first press of an application's + shortcut changed nothing; it now expands the rail. Below 840px, where there is no room to expand + it in the layout, `toggle()` opens and closes the modal rail (`show()`, `hide()`) instead of + changing a choice nothing draws there. A shortcut that pressed the rail's menu button to get this + can call `$store.rail.toggle()` again. + ## From 2.0.0 to 2.1.0 - **Browsers:** Firefox 151 or later (was 147), for container style queries on a custom property; diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index a7b52cb6..ddda3735 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -1046,7 +1046,7 @@ M3 Expressive's navigation rail: collapsed (96px, icon over label) or expanded ( - 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 `` if it must not show on a phone. - ``: the same props as ``. ``: a group with a heading that shows only while the rail is expanded; it names the group for screen readers either way. -- `$store.rail`: `collapsed`, `toggle()`, `collapse()`, `expand()` (the remembered choice; `auto` is true while nothing is stored, so an adaptive rail takes its window size class's default instead, and the first choice clears it), `open`, `show()`, `hide()` (the modal rail; closed on every `wire:navigate`). `config/livewire-material.php` → `rail.default` (`expanded` or `collapsed`) and `rail.storage_key` (`material-rail`). +- `$store.rail`: `collapsed`, `collapse()`, `expand()` (the remembered choice; `auto` is true while nothing is stored, so an adaptive rail takes its window size class's default instead, and the first choice clears it), `open`, `show()`, `hide()` (the modal rail; closed on every `wire:navigate`), and `toggle()` — for a keyboard shortcut — which flips what the first rail on the page draws, as its menu button does: the choice where the rail stands in the layout (from `expanded` for ``'s, where a collapsed rail with nothing stored expands), the modal where the window leaves it no room. `config/livewire-material.php` → `rail.default` (`expanded` or `collapsed`) and `rail.storage_key` (`material-rail`). ### `` diff --git a/resources/js/navigation.js b/resources/js/navigation.js index 5e7eaa4f..5e5def66 100644 --- a/resources/js/navigation.js +++ b/resources/js/navigation.js @@ -9,6 +9,7 @@ * `$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. + * So `collapsed` is not always what is drawn, and `toggle()` asks the rail on the page instead. * * `$store.rail.open` is the modal rail: on a window too narrow for an expanded rail, a menu * button opens it over a scrim (`show()`), and Escape, the scrim or leaving the page closes it @@ -22,6 +23,9 @@ */ import { from, upTo } from './breakpoints.js' +/** The root of every interactive rail on the page once its view is initialised: what `toggle()` asks. */ +const rails = new Set() + /* * The active indicator grows out of its centre when a page arrives through wire:navigate. The * new page's indicator is new markup, so the only way to animate it is a starting style — and @@ -61,8 +65,27 @@ document.addEventListener('alpine:init', () => { auto: root.hasAttribute('data-rail-auto'), open: false, + /** + * Flips what the first rail on the page draws: shut when it is open over the page, open + * over a scrim where the mode or the window leaves it no room to expand (`show()`, + * remembering nothing), and otherwise the choice, from what is drawn — a rail that hides + * when collapsed comes back into the layout. Not `set(!collapsed)`: with nothing stored, + * an adaptive rail in the expanded class is drawn collapsed whatever `rail.default` says. + * With no rail on the page, the choice itself. + */ toggle() { - this.set(!this.collapsed) + const root = [...document.querySelectorAll('[data-md-navigation-rail]')].find((element) => rails.has(element)) + const rail = root && window.Alpine.$data(root) + + if (!rail) { + this.set(!this.collapsed) + } else if (rail.open) { + this.hide() + } else if (rail.cramped) { + this.show() + } else { + this.set(rail.expanded) + } }, collapse() { @@ -120,6 +143,8 @@ document.addEventListener('alpine:init', () => { wasOpen: false, init() { + rails.add(this.$root) + 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. @@ -164,6 +189,7 @@ document.addEventListener('alpine:init', () => { }, destroy() { + rails.delete(this.$root) this.queries.forEach((query, index) => query.removeEventListener('change', this.listeners[index])) }, @@ -272,8 +298,8 @@ document.addEventListener('alpine:init', () => { } 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 - // `rail.default`, so the button has to flip what is actually drawn. + // `set`, not `toggle`: the button flips what this rail draws, and `toggle()` asks + // the first rail on the page. this.$store.rail.set(this.expanded) } }, diff --git a/tests/Browser/NavigationTest.php b/tests/Browser/NavigationTest.php index 77928277..46ead1df 100644 --- a/tests/Browser/NavigationTest.php +++ b/tests/Browser/NavigationTest.php @@ -117,6 +117,72 @@ it('starts the rail collapsed across the expanded class and expands it in the la navigationReady($page->refresh())->assertScript(railWidth(256)); }); +const TOGGLE_RAIL = "window.eval(\"Alpine.store('rail').toggle()\")"; + +it('flips the standing rail from $store.rail.toggle() as drawn, whatever is stored', function () { + // The expanded class with nothing stored: `data-rail` says `expanded` (`rail.default`), and + // the rail is drawn collapsed all the same — so an application's own shortcut expands it on + // the first press, as the rail's menu button does, rather than collapsing a collapsed rail. + $page = shellPage(960) + ->assertScript(railWidth(96)) + ->assertScript("document.documentElement.getAttribute('data-rail') === 'expanded'") + ->assertScript("document.documentElement.hasAttribute('data-rail-auto')"); + + $page->script(TOGGLE_RAIL); + + $page->assertScript(railWidth(256)) + ->assertScript('! '.RAIL.".hasAttribute('data-md-open')") + ->assertScript("localStorage.getItem('material-rail') === 'expanded'") + ->assertAttribute('[data-md-navigation-rail-menu]', 'aria-expanded', 'true'); + + $page->script(TOGGLE_RAIL); + + $page->assertScript(railWidth(96)) + ->assertScript("localStorage.getItem('material-rail') === 'collapsed'") + ->assertNoJavaScriptErrors(); + + // Large: expanded to begin with, so the first press collapses it. + $page = shellPage(1512)->assertScript(railWidth(256)); + + $page->script(TOGGLE_RAIL); + + $page->assertScript(railWidth(96)) + ->assertScript("localStorage.getItem('material-rail') === 'collapsed'") + ->assertNoJavaScriptErrors(); +}); + +it('opens and closes the modal rail from $store.rail.toggle() below expanded, storing nothing', function () { + // Medium: collapsed in the layout, and expanding it means the scrim, as its menu button does. + $page = shellPage(768)->assertScript(railWidth(96)); + + $page->script(TOGGLE_RAIL); + + $page->assertScript(RAIL.".hasAttribute('data-md-open')") + ->assertScript('Math.round('.RAIL_PANEL.'.getBoundingClientRect().width) === 256') + ->assertScript(railWidth(96)); + + $page->script(TOGGLE_RAIL); + + $page->assertScript('! '.RAIL.".hasAttribute('data-md-open')") + ->assertScript("localStorage.getItem('material-rail') === null") + ->assertNoJavaScriptErrors(); + + // Compact: nothing is drawn until the modal rail slides in, as `$store.rail.show()` brings it. + $page = shellPage(393, 852)->assertScript('getComputedStyle('.RAIL_PANEL.").display === 'none'"); + + $page->script(TOGGLE_RAIL); + + $page->assertScript(RAIL.".hasAttribute('data-md-open')") + ->assertScript('getComputedStyle('.RAIL_PANEL.").display === 'flex'"); + + $page->script(TOGGLE_RAIL); + + $page->assertScript('! '.RAIL.".hasAttribute('data-md-open')") + ->assertScript('getComputedStyle('.RAIL_PANEL.").display === 'none'") + ->assertScript("localStorage.getItem('material-rail') === null") + ->assertNoJavaScriptErrors(); +}); + it('morphs the rail header FAB between collapsed (icon only) and extended (with its label)', function () { $fab = "document.querySelector('[data-md-navigation-rail-fab-row] [data-md-fab]')"; $label = "document.querySelector('[data-md-navigation-rail-fab-row] [data-md-fab] > span')"; @@ -399,6 +465,28 @@ it('takes a rail out of the layout entirely when it hides collapsed, and back ov ->assertNoJavaScriptErrors(); }); +it('docks a rail that hid itself back into the layout from $store.rail.toggle(), not over the page', function () { + $page = navigationExtrasProbe(); + $rail = "document.querySelector('#hiding-rail [data-md-navigation-rail]')"; + $railWidth = "Math.round({$rail}.getBoundingClientRect().width)"; + + $page->click('#hiding-rail [data-md-navigation-rail-menu]') + ->assertScript("{$railWidth} === 0"); + + // The first interactive rail on the page is this one: the narrow rail above it is fixed. + $page->script(TOGGLE_RAIL); + + $page->assertScript("{$railWidth} === 256") + ->assertScript("! {$rail}.hasAttribute('data-md-open')") + ->assertScript("localStorage.getItem('material-rail') === 'expanded'"); + + $page->script(TOGGLE_RAIL); + + $page->assertScript("{$railWidth} === 0") + ->assertScript("! {$rail}.hasAttribute('data-md-open')") + ->assertNoJavaScriptErrors(); +}); + it('takes an adaptive rail out of the layout across the expanded class when it hides collapsed, standing again from large', function () { $page = navigationExtrasProbe(); $rail = "document.querySelector('#adaptive-hiding-rail [data-md-navigation-rail]')";