Render a tab panel's id and its hidden state on the server
Every panel was visible until Alpine booted — four role="tabpanel" regions at once for a screen reader, and aria-controls pointing at ids that did not exist yet. The panel now carries its id, its aria-labelledby and, unless it is the chosen one, display:none from the server. A slot renders before the component around it, so the panel reads <x-tabs>'s attributes off the component stack and repeats its id and initial-tab lines; both files say so. Plan: docs/plans/material-3-alignment.md, step 14 (navigation N-05). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
a99fbd3794
commit
94e2a900d2
@@ -21,10 +21,27 @@ it('renders the tablist on the server with the first enabled tab chosen', functi
|
||||
->toContain('data-tab-indicator')
|
||||
->toContain('--tabs-indicator: share-indicator')
|
||||
->toContain('x-modelable="selected"')
|
||||
->toContain('role="tabpanel"')
|
||||
// The panel's id is the server's, so aria-controls never dangles (N-05).
|
||||
->toMatch('/role="tabpanel"\s+id="share-people-panel"\s+aria-labelledby="share-people"/')
|
||||
->toContain('>3</span>');
|
||||
});
|
||||
|
||||
it('renders every panel but the chosen one hidden, before Alpine boots', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-tabs :tabs="[['name' => 'files', 'label' => 'Files'], ['name' => 'people', 'label' => 'People']]" selected="people">
|
||||
<x-tab name="files">Three files.</x-tab>
|
||||
<x-tab name="people">Anna and Ben.</x-tab>
|
||||
</x-tabs>
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
->toMatch('/id="[^"]+-files-panel"[^>]*style="display: none"/')
|
||||
->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"/');
|
||||
});
|
||||
|
||||
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"')
|
||||
@@ -42,14 +59,17 @@ it('entangles the chosen tab with Livewire and renders the one the property name
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return '<div><x-tabs wire:model.live="tab" :tabs="[[\'name\' => \'files\', \'label\' => \'Files\'], [\'name\' => \'people\', \'label\' => \'People\']]" /></div>';
|
||||
return '<div><x-tabs wire:model.live="tab" :tabs="[[\'name\' => \'files\', \'label\' => \'Files\'], [\'name\' => \'people\', \'label\' => \'People\']]"><x-tab name="files">Three files.</x-tab><x-tab name="people">Anna and Ben.</x-tab></x-tabs></div>';
|
||||
}
|
||||
});
|
||||
|
||||
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-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('draws section navigation as secondary tabs and a picker', function () {
|
||||
|
||||
Reference in New Issue
Block a user