Draw the tabs without Tailwind
Plan step 36 (navigation group, third and last stream): <x-tabs>'s and <x-tab>'s class lists move into resources/css/components/tabs.css, one commit for both since they share it. Root data-md-tabs; the bar data-md-tabs-bar with data-md-variant/-stacked/-scrollable, holding data-md-tab buttons (data-md-tab-content, the new data-md-tab-label wrapping the label and badge, data-md-tab-indicator); the panel data-md-tab-panel. The server-rendered hidden panel (N-05), the 52dp scrollable offset (N-10), the indicator's 2dp inset (N-18), the ring moved 2px out (N-20) and aria-current sharing the active colour (N-21) already matched the audit; only hooks, units and layer changed. Sizes (48/64px tab height, 90px minimum width) stay px; spacing goes through the measurement tokens where the value matches one (the icon-label and label-badge gaps, the panel's top padding). Imports icon.css and badge.css for what the view renders. Hooks renamed: data-tabs-bar, data-tab, data-tab-content, data-tab-indicator, data-scrollable, data-stacked and the bar's data-variant to data-md-*, updated in tabs.js (dataset.tab -> dataset.mdTab), section-nav.blade.php (which reuses the tab bar's hooks under its own, still-Tailwind, markup), tests/Feature/Components/TabsTest.php and tests/Browser/BarsTest.php. Added the owed browser test: a tab panel never flashes before Alpine boots, read from an inline script that runs while the page is still parsing, well before Alpine's own script (docs/plans/material-3-browser-tests.md). This closes plan step 36: every navigation-group component is now data-md-* and Tailwind-free; tailwind.css's Navigation block keeps only navigation.css (the bar and rail, out of this batch's scope). 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
9c134b2dc7
commit
1f9384efdf
@@ -2,6 +2,7 @@
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\Livewire;
|
||||
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
||||
|
||||
it('renders the tablist on the server with the first enabled tab chosen', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
@@ -12,13 +13,13 @@ it('renders the tablist on the server with the first enabled tab chosen', functi
|
||||
|
||||
expect($html)
|
||||
->toContain('role="tablist"')
|
||||
->toContain('data-variant="primary"')
|
||||
->toContain('data-md-variant="primary"')
|
||||
->toContain('id="share-people"')
|
||||
->toContain('aria-controls="share-people-panel"')
|
||||
->toMatch('/data-tab="people"\s+aria-controls="share-people-panel"\s+aria-selected="true"\s+tabindex="0"/')
|
||||
->toMatch('/data-tab="files"\s+aria-controls="share-files-panel"\s+aria-selected="false"\s+tabindex="-1"/')
|
||||
->toMatch('/data-md-tab="people"\s+aria-controls="share-people-panel"\s+aria-selected="true"\s+tabindex="0"/')
|
||||
->toMatch('/data-md-tab="files"\s+aria-controls="share-files-panel"\s+aria-selected="false"\s+tabindex="-1"/')
|
||||
->toContain('disabled')
|
||||
->toContain('data-tab-indicator')
|
||||
->toContain('data-md-tab-indicator')
|
||||
->toContain('--tabs-indicator: share-indicator')
|
||||
->toContain('x-modelable="selected"')
|
||||
// The panel's id is the server's, so aria-controls never dangles (N-05).
|
||||
@@ -39,17 +40,18 @@ it('renders every panel but the chosen one hidden, before Alpine boots', functio
|
||||
->toMatch('/id="[^"]+-people-panel"(?![^>]*style="display: none")/')
|
||||
// The tab button and its panel agree on the id the slot worked out for itself.
|
||||
->toMatch('/aria-controls="(tabs-[0-9a-f]{8})-people-panel"/')
|
||||
->toMatch('/id="(tabs-[0-9a-f]{8})-people-panel"/');
|
||||
->toMatch('/id="(tabs-[0-9a-f]{8})-people-panel"/')
|
||||
->toContain('data-md-tab-panel');
|
||||
});
|
||||
|
||||
it('takes a variant, stacks icons and scrolls on request', function () {
|
||||
expect((string) $this->blade('<x-tabs variant="secondary" stacked scrollable :tabs="[[\'name\' => \'a\', \'label\' => \'A\']]" />'))
|
||||
->toContain('data-variant="secondary"')
|
||||
->not->toContain('data-stacked')
|
||||
->toContain('data-scrollable')
|
||||
->toContain('data-md-variant="secondary"')
|
||||
->not->toContain('data-md-stacked')
|
||||
->toContain('data-md-scrollable')
|
||||
->and((string) $this->blade('<x-tabs stacked selected="b" :tabs="[[\'name\' => \'a\', \'label\' => \'A\'], [\'name\' => \'b\', \'label\' => \'B\']]" />'))
|
||||
->toContain('data-stacked')
|
||||
->toMatch('/data-tab="b"\s+aria-controls="[^"]+"\s+aria-selected="true"/');
|
||||
->toContain('data-md-stacked')
|
||||
->toMatch('/data-md-tab="b"\s+aria-controls="[^"]+"\s+aria-selected="true"/');
|
||||
});
|
||||
|
||||
it('entangles the chosen tab with Livewire and renders the one the property names', function () {
|
||||
@@ -66,26 +68,36 @@ it('entangles the chosen tab with Livewire and renders the one the property name
|
||||
expect(Livewire::test('tabs-probe')->html())
|
||||
->toContain(".entangle('tab').live")
|
||||
->not->toContain('x-modelable')
|
||||
->toMatch('/data-tab="people"\s+aria-controls="[^"]+"\s+aria-selected="true"/')
|
||||
->toMatch('/data-md-tab="people"\s+aria-controls="[^"]+"\s+aria-selected="true"/')
|
||||
// The panel the property names is the one drawn, before Alpine entangles anything (N-05).
|
||||
->toMatch('/id="[^"]+-files-panel"[^>]*style="display: none"/')
|
||||
->toMatch('/id="[^"]+-people-panel"(?![^>]*style="display: none")/');
|
||||
});
|
||||
|
||||
it('keeps the tab bar on M3\'s offsets, indicator inset and focus ring', function () {
|
||||
$css = file_get_contents(__DIR__.'/../../../resources/css/components/tabs.css');
|
||||
$css = ComponentStylesheet::read('tabs');
|
||||
|
||||
expect($css)
|
||||
expect($css->declarations('[data-md-tabs-bar][data-md-scrollable]'))
|
||||
// 52dp before the first of a scrollable set (N-10).
|
||||
->toContain("[data-tabs-bar][data-scrollable] {\n padding-inline-start: 3.25rem;")
|
||||
->toHaveKey('padding-inline-start', '52px')
|
||||
// A primary indicator is inset 2dp each side; a secondary one spans the tab (N-18).
|
||||
->toMatch('/\[data-tab-indicator\] \{\s+position: absolute;\s+inset-inline: 2px;/')
|
||||
->toMatch('/\[data-variant="secondary"\] \[data-tab-indicator\] \{\s+inset-inline: 0;/')
|
||||
->and($css->declarations('[data-md-tab-indicator]'))
|
||||
->toHaveKey('inset-inline', '2px')
|
||||
->and($css->declarations('[data-md-tabs-bar][data-md-variant=\'secondary\'] [data-md-tab-indicator]'))
|
||||
->toHaveKey('inset-inline', '0')
|
||||
// The ring is 2px outside, as everywhere else in the package (N-20).
|
||||
->toContain('outline-offset: 2px;')
|
||||
->not->toContain('outline-offset: -3px;')
|
||||
->and($css->declarations('[data-md-tab]:focus-visible'))
|
||||
->toBe(['color' => 'var(--md-sys-color-on-surface)', 'outline' => '3px solid var(--md-sys-color-secondary)', 'outline-offset' => '2px'])
|
||||
// A link marked as the page is the chosen tab too (N-21).
|
||||
->toContain('[data-tab]:is([aria-selected="true"], [aria-current="page"]) {');
|
||||
->and($css->declarations('[data-md-tab]:is([aria-selected=\'true\'], [aria-current=\'page\'])'))
|
||||
->toBe(['color' => 'var(--md-sys-color-primary)']);
|
||||
});
|
||||
|
||||
it('hides every panel until Alpine takes over, and stands the panel\'s content clear of the bar', function () {
|
||||
$css = ComponentStylesheet::read('tabs');
|
||||
|
||||
expect($css->declarations('[data-md-tab-panel]'))
|
||||
->toBe(['padding-top' => 'var(--md-sys-measurement-space300)', 'outline' => 'none']);
|
||||
});
|
||||
|
||||
it('draws section navigation as secondary tabs and a picker', function () {
|
||||
@@ -99,10 +111,10 @@ it('draws section navigation as secondary tabs and a picker', function () {
|
||||
expect($html)
|
||||
->toContain('aria-label="Settings"')
|
||||
->toContain('data-section-picker')
|
||||
->toContain('data-variant="secondary"')
|
||||
->toContain('data-md-variant="secondary"')
|
||||
->toContain('max-medium:hidden')
|
||||
->toMatch('/href="\/settings\/security"\s+data-tab\s+aria-current="page"\s+wire:navigate/')
|
||||
->not->toMatch('/href="\/settings\/profile"\s+data-tab\s+aria-current/')
|
||||
->toMatch('/href="\/settings\/security"\s+data-md-tab\s+aria-current="page"\s+wire:navigate/')
|
||||
->not->toMatch('/href="\/settings\/profile"\s+data-md-tab\s+aria-current/')
|
||||
// The picker is a menu of places: the current one is the page, not a checked choice,
|
||||
// and a section's badge shows there as well as on its tab.
|
||||
->not->toContain('role="menuitemcheckbox"')
|
||||
@@ -117,13 +129,13 @@ it('marks the section whose url is the request\'s, and scrolls many sections', f
|
||||
|
||||
expect((string) $this->blade('<x-section-nav :items="$items" no-wire-navigate />', ['items' => $items]))
|
||||
// Five or more sections are M3's scrollable tabs, not a grid of wrapped rows (N-16).
|
||||
->toMatch('/<ul data-tabs-bar data-variant="secondary"\s+data-scrollable\s*>/')
|
||||
->toMatch('/<ul data-md-tabs-bar data-md-variant="secondary"\s+data-md-scrollable\s*>/')
|
||||
->not->toContain('grid-cols-')
|
||||
->toMatch('/data-tab\s+aria-current="page"\s*>\s*<span data-tab-content>\s*<span class="truncate">S3/')
|
||||
->toMatch('/data-md-tab\s+aria-current="page"\s*>\s*<span data-md-tab-content>\s*<span class="truncate">S3/')
|
||||
->not->toContain('wire:navigate')
|
||||
// Four still share the row.
|
||||
->and((string) $this->blade('<x-section-nav :items="$items" no-wire-navigate />', ['items' => array_slice($items, 0, 4)]))
|
||||
->not->toContain('data-scrollable');
|
||||
->not->toContain('data-md-scrollable');
|
||||
});
|
||||
|
||||
it('keeps the page\'s section current while a Livewire component on it updates', function () {
|
||||
@@ -142,7 +154,7 @@ it('keeps the page\'s section current while a Livewire component on it updates',
|
||||
}
|
||||
});
|
||||
|
||||
$current = '/href="[^"]*\/livewire-unit-test-endpoint\/[^"]*"\s+data-tab\s+aria-current="page"/';
|
||||
$current = '/href="[^"]*\/livewire-unit-test-endpoint\/[^"]*"\s+data-md-tab\s+aria-current="page"/';
|
||||
|
||||
$probe = Livewire::test('section-nav-probe');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user