collapse.css animated `<details>`' `::details-content` from `block-size: 0` to `auto`, which needs `interpolate-size: allow-keywords` (Chrome 129 and nothing else) and a `content-visibility` held through the close with `allow-discrete` (not Firefox), so in Firefox and Safari the section snapped open and shut. No stylesheet can do it there. resources/js/collapse.js animates the `<details>` itself, the same way in every engine: a Web Animation of its `block-size` from the height it is drawn at to the height it is going to, on the fast spatial spring's tokens, with `overflow: clip` and a `min-block-size` that keeps the summary whole through the spring's overshoot, all inside the keyframes, so nothing is left in `style`. Content below moves with it. Opening sets `open` at once and grows the height; closing keeps `open` while the height goes, marks `data-md-collapse-closing` so the chevron turns back at the start, and drops `open` (and fires `toggle`) when the height has. A press mid-way turns it round from the height it has reached. The CSS animation is gone, so Chrome takes the same path and never animates twice. Routes: a press on the summary (pointer, Enter, Space) is taken over; the Alpine and Livewire bindings call `materialCollapse()` from the view's `x-effect` (its first call, as Alpine starts, sets the state at once); a `name` group's open member closes on the same spring, its `name` lifted for the close so the browser's exclusivity does not shut it first and put back once closed. Anything else that sets `open` (find-in-page, an application's own script) stays instant, as the browser draws it, and is followed. Under reduced motion every route is the browser's own. Browser tests in Chrome, Firefox and Safari catch the height part-way open and shut (and what is under it moving) from a press and from the Alpine binding, the chevron turning at the start of a close, the clip while it moves, Enter on the summary, a reversal mid-way, a name group, and reduced motion. The four motion tests fail on main in Firefox and Safari. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
78 lines
3.5 KiB
PHP
78 lines
3.5 KiB
PHP
<?php
|
|
|
|
use Livewire\Component;
|
|
use Livewire\Livewire;
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
|
|
|
it('discloses on the native details element, kept open through a morph', function () {
|
|
$html = (string) $this->blade('<x-collapse title="How long do links last?" icon="schedule" open variant="filled">Until the expiry.</x-collapse>');
|
|
|
|
expect($html)
|
|
->toContain('<details')
|
|
->toContain('wire:ignore.self')
|
|
->toContain(' open ')
|
|
->toContain('data-md-collapse')
|
|
->toContain('data-md-variant="filled"')
|
|
->toContain('data-md-collapse-icon')
|
|
->toContain('data-md-collapse-chevron')
|
|
->toContain('How long do links last?')
|
|
->toContain('Until the expiry.')
|
|
->toContain('class="md-state-layer md-focus-ring"');
|
|
});
|
|
|
|
it('has no Alpine on it without a binding', function () {
|
|
$html = (string) $this->blade('<x-collapse title="Advanced" open>Body</x-collapse>');
|
|
|
|
expect($html)
|
|
->toMatch('/<details\s+wire:ignore.self\s+data-md-collapse\s+data-md-variant="plain"\s+open\s+/')
|
|
->not->toContain('x-data')
|
|
->not->toContain('x-modelable')
|
|
->not->toContain('x-on:toggle')
|
|
->and((string) $this->blade('<x-collapse title="Advanced">Body</x-collapse>'))
|
|
->toMatch('/<details\s+wire:ignore.self\s+data-md-collapse\s+data-md-variant="plain"\s+/');
|
|
});
|
|
|
|
it('binds its open state through x-model, drawing the open prop until Alpine starts', function () {
|
|
$html = (string) $this->blade('<x-collapse title="Advanced" x-model="advanced" open>Body</x-collapse>');
|
|
|
|
expect($html)
|
|
->toMatch('/x-data="{ collapseOpen:\s*true\s*}"/')
|
|
->toContain('x-modelable="collapseOpen"')
|
|
->toContain('x-model="advanced"')
|
|
->toContain('x-effect="materialCollapse($el, collapseOpen)"')
|
|
->toContain('x-on:toggle="collapseOpen = $el.open"')
|
|
->toMatch('/\sopen\s/');
|
|
});
|
|
|
|
it('entangles its open state with a Livewire property and renders open as the property is', function (bool $fineTuning) {
|
|
Livewire::component('collapse-probe', new class extends Component
|
|
{
|
|
public bool $fineTuning = false;
|
|
|
|
public function render(): string
|
|
{
|
|
return '<div><x-collapse title="Fine-tuning" wire:model.live="fineTuning" :open="! $fineTuning">Body</x-collapse></div>';
|
|
}
|
|
});
|
|
|
|
$html = Livewire::test('collapse-probe', ['fineTuning' => $fineTuning])->html();
|
|
|
|
expect($html)
|
|
->toContain(".entangle('fineTuning').live")
|
|
->not->toContain('x-modelable')
|
|
->not->toContain('wire:model');
|
|
|
|
preg_match('/<details[^>]*>/', $html, $tag);
|
|
|
|
expect(preg_match('/\sopen\s/', $tag[0]) === 1)->toBe($fineTuning);
|
|
})->with(['open' => true, 'closed' => false]);
|
|
|
|
it('draws a filled collapse\'s padding and an open one\'s chevron on its own parts, never a nested collapse\'s', function () {
|
|
$css = ComponentStylesheet::read('collapse');
|
|
|
|
expect($css->declarations("[data-md-collapse][data-md-variant='filled'] > [data-md-collapse-summary]"))->toBe(['padding-inline' => 'var(--md-sys-measurement-space200)'])
|
|
->and($css->declarations("[data-md-collapse][data-md-variant='filled'] > [data-md-collapse-body]"))->toBe(['padding-inline' => 'var(--md-sys-measurement-space200)'])
|
|
->and($css->declarations('[data-md-collapse][open]:not([data-md-collapse-closing]) > [data-md-collapse-summary] > [data-md-collapse-chevron]'))->toBe(['rotate' => '180deg'])
|
|
->and($css->declarations('[data-md-collapse-summary]'))->toHaveKey('gap', '12px');
|
|
});
|