Add the M3 Expressive slider
tests / lint (push) Successful in 1m0s
tests / feature (8.4) (push) Successful in 1m5s
tests / feature (8.5) (push) Successful in 1m5s
tests / browser (chrome, chromium) (push) Successful in 3m17s
tests / browser (firefox, firefox) (push) Successful in 4m1s
tests / browser (safari, webkit) (push) Failing after 5m38s
tests / lint (push) Successful in 1m0s
tests / feature (8.4) (push) Successful in 1m5s
tests / feature (8.5) (push) Successful in 1m5s
tests / browser (chrome, chromium) (push) Successful in 3m17s
tests / browser (firefox, firefox) (push) Successful in 4m1s
tests / browser (safari, webkit) (push) Failing after 5m38s
Standard, centered and range sliders on native range inputs, drawn as Compose draws them, in five sizes with ticks, value labels and inset icons. Completes Phase 6. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
co-authored by
Claude Opus 5
parent
0fee2a0952
commit
bf00e4c40a
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Livewire\Component;
|
||||
use Livewire\Livewire;
|
||||
|
||||
/**
|
||||
* A Livewire component bound live to a slider and a range, whose values the server can also set.
|
||||
*/
|
||||
class SliderProbe extends Component
|
||||
{
|
||||
public int $volume = 30;
|
||||
|
||||
/** @var array<int, int|string> */
|
||||
public array $price = [20, 80];
|
||||
|
||||
public function maximise(): void
|
||||
{
|
||||
$this->volume = 90;
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return <<<'BLADE'
|
||||
<div style="width: 420px; padding: 4rem 2rem">
|
||||
<x-slider label="Volume" wire:model.live="volume" />
|
||||
<p>volume: <span id="volume">{{ $volume }}</span></p>
|
||||
<x-slider label="Price" wire:model.live="price" range />
|
||||
<p>price: <span id="price">{{ implode(',', $price) }}</span></p>
|
||||
<button type="button" wire:click="maximise">Maximise</button>
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
}
|
||||
|
||||
function sliderProbe()
|
||||
{
|
||||
Livewire::component('slider-probe', SliderProbe::class);
|
||||
|
||||
Route::middleware('web')->get('/slider-probe', fn () => Blade::render(<<<'BLADE'
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<x-theme-script />
|
||||
@vite(config('livewire-material.showcase.vite'))
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="bg-surface">
|
||||
<livewire:slider-probe />
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
BLADE));
|
||||
|
||||
return visit('/slider-probe')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
}
|
||||
|
||||
function sliderShowcase()
|
||||
{
|
||||
return visit('/material')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
}
|
||||
|
||||
/**
|
||||
* A script run against the first slider labelled `$label`, scrolled into view. In scope: `root`
|
||||
* (the element with x-data), `inputs`, `width` (the track's), `left(selector)` (a drawn part's
|
||||
* position), `segment(name)` (a track segment's {from, to}, or null while hidden), `near()`,
|
||||
* `pause(ms)` and `pointer(type, fraction)`, which sends a mouse pointer event at that fraction of
|
||||
* the track. Pest retries a failing assertion: scripts that change something go through
|
||||
* `script()`, and the assertions after them only read.
|
||||
*/
|
||||
function onSlider(string $label, string $body): string
|
||||
{
|
||||
return <<<JS
|
||||
(async () => {
|
||||
const root = [...document.querySelectorAll('[data-slider]')]
|
||||
.find((slider) => slider.parentElement.querySelector(':scope > label, :scope > span')?.textContent.trim() === '{$label}')
|
||||
root.scrollIntoView({ block: 'center' })
|
||||
const inputs = [...root.querySelectorAll('input[type="range"]')]
|
||||
const width = root.clientWidth - 4
|
||||
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
const near = (a, b, tolerance = 1) => Math.abs(a - b) <= tolerance
|
||||
const left = (selector) => Number.parseFloat(root.querySelector(selector).style.left)
|
||||
const segment = (name) => {
|
||||
const element = root.querySelector(`[data-segment="\${name}"]`)
|
||||
const from = Number.parseFloat(element.style.left)
|
||||
return element.hidden ? null : { from, to: from + Number.parseFloat(element.style.width) }
|
||||
}
|
||||
const pointer = (type, fraction) => {
|
||||
const box = root.getBoundingClientRect()
|
||||
const target = type === 'pointerdown' ? root : window
|
||||
target.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true, clientX: box.left + 2 + width * fraction, clientY: box.top + box.height / 2, button: 0, pointerId: 1, pointerType: 'mouse' }))
|
||||
}
|
||||
{$body}
|
||||
})()
|
||||
JS;
|
||||
}
|
||||
|
||||
it('moves with the keyboard, and the drawing follows', function () {
|
||||
$page = sliderShowcase()->assertNoJavaScriptErrors();
|
||||
|
||||
$page->script(onSlider('Volume', 'inputs[0].focus()'));
|
||||
|
||||
$page->keys(':focus', ['ArrowRight', 'ArrowRight'])
|
||||
->assertScript(onSlider('Volume', <<<'JS'
|
||||
return inputs[0].value === '42'
|
||||
&& near(left('[data-handle="start"]'), width * 0.42)
|
||||
&& near(segment('active').to, width * 0.42 - 8)
|
||||
&& near(segment('end').from, width * 0.42 + 8)
|
||||
&& root.querySelector('[data-handle="start"]').hasAttribute('data-focused')
|
||||
&& root.querySelector('[data-value-label]').textContent === '42'
|
||||
JS));
|
||||
|
||||
$page->keys(':focus', 'PageUp')
|
||||
->assertScript(onSlider('Volume', "return inputs[0].value === '52' && near(left('[data-handle=\"start\"]'), width * 0.52)"));
|
||||
|
||||
$page->keys(':focus', 'End')
|
||||
->assertScript(onSlider('Volume', "return inputs[0].value === '100' && segment('end') === null && ! root.querySelector('[data-stop=\"end\"]').checkVisibility()"));
|
||||
|
||||
$page->keys(':focus', 'Home')
|
||||
->assertScript(onSlider('Volume', "return inputs[0].value === '0' && segment('active') === null && near(segment('end').from, 8)"));
|
||||
});
|
||||
|
||||
it('follows a pointer drag with a narrowed handle and its value label', function () {
|
||||
$page = sliderShowcase();
|
||||
|
||||
$page->script(onSlider('Volume', "pointer('pointerdown', 0.4); pointer('pointermove', 0.75)"));
|
||||
|
||||
$page->assertScript(onSlider('Volume', <<<'JS'
|
||||
await pause(450)
|
||||
const thumb = root.querySelector('[data-handle="start"]')
|
||||
const label = thumb.querySelector('[data-value-label]')
|
||||
return inputs[0].value === '75'
|
||||
&& label.textContent === '75'
|
||||
&& getComputedStyle(label).opacity === '1'
|
||||
&& thumb.hasAttribute('data-pressed')
|
||||
&& thumb.firstElementChild.offsetWidth === 2
|
||||
&& near(left('[data-handle="start"]'), width * 0.75)
|
||||
&& document.activeElement === inputs[0]
|
||||
JS));
|
||||
|
||||
$page->script(onSlider('Volume', "window.__changed = 0; inputs[0].addEventListener('change', () => window.__changed++); pointer('pointerup', 0.75)"));
|
||||
|
||||
$page->assertScript(onSlider('Volume', <<<'JS'
|
||||
await pause(450)
|
||||
const thumb = root.querySelector('[data-handle="start"]')
|
||||
return inputs[0].value === '75'
|
||||
&& window.__changed === 1
|
||||
&& ! thumb.hasAttribute('data-pressed')
|
||||
&& ! thumb.hasAttribute('data-focused')
|
||||
&& thumb.firstElementChild.offsetWidth === 4
|
||||
&& getComputedStyle(thumb.querySelector('[data-value-label]')).opacity === '0'
|
||||
JS));
|
||||
});
|
||||
|
||||
it('keeps a range\'s handles from crossing, by pointer and by keyboard', function () {
|
||||
$page = sliderShowcase();
|
||||
|
||||
// The start handle, from 20 dragged past the end at 60, stops on it.
|
||||
$page->script(onSlider('Price', "pointer('pointerdown', 0.2); pointer('pointermove', 0.9); pointer('pointerup', 0.9)"));
|
||||
|
||||
$page->assertScript(onSlider('Price', <<<'JS'
|
||||
return inputs[0].value === '60' && inputs[1].value === '60'
|
||||
&& near(left('[data-handle="start"]'), left('[data-handle="end"]'))
|
||||
&& segment('active') === null
|
||||
JS));
|
||||
|
||||
$page->script(onSlider('Price', 'inputs[1].focus()'));
|
||||
|
||||
$page->keys(':focus', 'Home')
|
||||
->assertScript(onSlider('Price', "return inputs[0].value === '60' && inputs[1].value === '60'"));
|
||||
|
||||
// Bound with x-model to an array: the model never holds crossed ends either.
|
||||
$page->script(onSlider('Bound price', 'inputs[0].focus()'));
|
||||
|
||||
$page->keys(':focus', 'End')
|
||||
->assertScript("document.querySelector('[data-bound=\"price\"]').textContent === '[300,300]'");
|
||||
});
|
||||
|
||||
it('fills a centred slider from the middle', function () {
|
||||
$page = sliderShowcase();
|
||||
|
||||
$page->assertScript(onSlider('Balance', <<<'JS'
|
||||
const handle = left('[data-handle="start"]')
|
||||
return inputs[0].value === '15'
|
||||
&& near(handle, width * 0.65)
|
||||
&& near(segment('start').to, width / 2 - 6)
|
||||
&& near(segment('active').from, width / 2)
|
||||
&& near(segment('active').to, handle - 8)
|
||||
&& near(segment('end').from, handle + 8)
|
||||
JS));
|
||||
|
||||
$page->script(onSlider('Balance', 'inputs[0].focus()'));
|
||||
|
||||
$page->keys(':focus', 'Home')
|
||||
->assertScript(onSlider('Balance', <<<'JS'
|
||||
return inputs[0].value === '-50'
|
||||
&& segment('start') === null
|
||||
&& near(segment('active').from, 8)
|
||||
&& near(segment('active').to, width / 2)
|
||||
&& near(segment('end').from, width / 2 + 6)
|
||||
JS));
|
||||
});
|
||||
|
||||
it('follows a value x-model sets from outside', function () {
|
||||
$page = sliderShowcase();
|
||||
|
||||
$page->script("[...document.querySelectorAll('#sliders button')].find((button) => button.textContent.trim() === 'Set 80').click()");
|
||||
|
||||
$page->assertScript(onSlider('Bound volume', "return inputs[0].value === '80' && near(left('[data-handle=\"start\"]'), width * 0.8)"));
|
||||
});
|
||||
|
||||
it('sends live values to Livewire, is not moved back by a render mid-drag, and moves when the server sets a value', function () {
|
||||
$page = sliderProbe();
|
||||
|
||||
$page->script(onSlider('Volume', 'inputs[0].focus()'));
|
||||
|
||||
$page->keys(':focus', ['ArrowRight', 'ArrowRight', 'ArrowRight'])
|
||||
->assertSeeIn('#volume', '33');
|
||||
|
||||
// A drag that outruns a round trip: while the request for 50 is on its way, the handle moves
|
||||
// on to 70, and the render for 50 must not pull it back.
|
||||
$page->script(onSlider('Volume', <<<'JS'
|
||||
window.__renders = 0
|
||||
let moved = false
|
||||
window.Livewire.hook('commit', ({ succeed }) => {
|
||||
if (! moved) {
|
||||
moved = true
|
||||
queueMicrotask(() => pointer('pointermove', 0.7))
|
||||
}
|
||||
succeed(() => window.__renders++)
|
||||
})
|
||||
pointer('pointerdown', 0.5)
|
||||
JS));
|
||||
|
||||
// Both round trips back: the handle is where it was dragged, and so is the property.
|
||||
$page->assertScript(onSlider('Volume', <<<'JS'
|
||||
return window.__renders >= 2 && inputs[0].value === '70' && near(left('[data-handle="start"]'), width * 0.7)
|
||||
JS))
|
||||
->assertSeeIn('#volume', '70');
|
||||
|
||||
$page->script(onSlider('Volume', "pointer('pointerup', 0.7)"));
|
||||
|
||||
$page->click('button:has-text("Maximise")')
|
||||
->assertSeeIn('#volume', '90')
|
||||
->assertScript(onSlider('Volume', "return inputs[0].value === '90' && near(left('[data-handle=\"start\"]'), width * 0.9) && root.querySelector('[data-value-label]').textContent === '90'"));
|
||||
|
||||
$page->script(onSlider('Price', 'inputs[1].focus()'));
|
||||
|
||||
$page->keys(':focus', 'ArrowLeft')
|
||||
->assertSeeIn('#price', '20,79');
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\MessageBag;
|
||||
use Illuminate\Support\ViewErrorBag;
|
||||
use Livewire\Component;
|
||||
use Livewire\Livewire;
|
||||
|
||||
it('is a labelled native range input under a drawing only the script touches', function () {
|
||||
$html = (string) $this->blade('<x-slider label="Volume" name="volume" value="40" :min="0" :max="200" :step="5" id="volume" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('<label for="volume" id="volume-label"')
|
||||
->toContain('type="range"')
|
||||
->toContain('id="volume"')
|
||||
->toContain('name="volume"')
|
||||
->toContain('min="0"')
|
||||
->toContain('max="200"')
|
||||
->toContain('step="5"')
|
||||
->toContain('value="40"')
|
||||
->toContain('x-data="materialSlider"')
|
||||
->toContain('x-on:pointerdown="press($event)"')
|
||||
->toContain('data-slider-drawing wire:ignore aria-hidden="true"')
|
||||
->toContain('noscript:appearance-auto')
|
||||
->not->toContain('role="group"');
|
||||
});
|
||||
|
||||
it('draws the first frame on the server: the track split around the handle, and the stop', function () {
|
||||
$html = (string) $this->blade('<x-slider value="40" />');
|
||||
|
||||
expect($html)
|
||||
->toMatch('/data-segment="active"\s+class="[^"]*bg-primary[^"]*"\s+style="left: calc\(0% \+ 0px\); width: calc\(40% - 8px\); border-radius: 8px 2px 2px 8px"/')
|
||||
->toMatch('/data-segment="end"\s+class="[^"]*bg-secondary-container[^"]*"\s+style="left: calc\(40% \+ 8px\); width: calc\(60% - 8px\); border-radius: 2px 8px 8px 2px"/')
|
||||
->toMatch('/data-segment="start"\s+hidden/')
|
||||
->toContain('style="left: calc(100% - 8px)"')
|
||||
->toContain('<span data-handle="start" class="group/thumb absolute inset-y-0 w-0" style="left: calc(40% + 0px)">')
|
||||
->toMatch('/data-value-label\s+class="[^"]*opacity-0[^"]*"\s*>40<\/span>/');
|
||||
});
|
||||
|
||||
it('snaps and clamps the value to the step grid', function (string $template, string $value) {
|
||||
expect((string) $this->blade($template))->toContain("value=\"{$value}\"");
|
||||
})->with([
|
||||
'between steps' => ['<x-slider value="23" :step="5" />', '25'],
|
||||
'below the minimum' => ['<x-slider :value="-20" />', '0'],
|
||||
'past the last step' => ['<x-slider value="10" :max="10" :step="3" />', '9'],
|
||||
'a fraction' => ['<x-slider value="0.34" :max="1" step="0.1" />', '0.3'],
|
||||
'continuous' => ['<x-slider value="12.345" step="any" />', '12.345'],
|
||||
'centred without a value' => ['<x-slider :min="-50" :max="50" centered />', '0'],
|
||||
]);
|
||||
|
||||
it('binds wire:model and x-model to the input as numbers, and names it after the property', function () {
|
||||
expect((string) $this->blade('<x-slider wire:model.live.debounce.250ms="volume" />'))
|
||||
->toContain('wire:model.number.live.debounce.250ms="volume"')
|
||||
->toContain('name="volume"');
|
||||
|
||||
expect((string) $this->blade('<x-slider x-model="volume" name="volume" x-on:change="saved = true" />'))
|
||||
->toContain('x-model.number="volume"')
|
||||
->toContain('x-on:change="saved = true"');
|
||||
|
||||
expect((string) $this->blade('<x-slider x-model.number="volume" />'))->toContain('x-model.number="volume"');
|
||||
});
|
||||
|
||||
it('gives a range two inputs bound to the ends of an array, in a named group', function () {
|
||||
$html = (string) $this->blade('<x-slider label="Price" wire:model="price" range :value="[80, 20]" id="price" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('role="group"')
|
||||
->toContain('aria-labelledby="price-label"')
|
||||
->toContain('wire:model.number="price.0"')
|
||||
->toContain('wire:model.number="price.1"')
|
||||
->toContain('aria-label="Range start"')
|
||||
->toContain('aria-label="Range end"')
|
||||
->toContain('id="price-end"')
|
||||
->toContain('data-handle="end"')
|
||||
->not->toContain('<label for=');
|
||||
|
||||
expect(substr_count($html, 'name="price[]"'))->toBe(2)
|
||||
// Out of order, the ends are sorted, as Compose sorts them.
|
||||
->and(strpos($html, 'value="20"'))->toBeLessThan(strpos($html, 'value="80"'));
|
||||
|
||||
expect((string) $this->blade('<x-slider x-model="price" range />'))
|
||||
->toContain('x-model.number="price[0]"')
|
||||
->toContain('x-model.number="price[1]"');
|
||||
});
|
||||
|
||||
it('draws the value of the bound Livewire property', function () {
|
||||
$component = new class extends Component
|
||||
{
|
||||
public int $volume = 70;
|
||||
|
||||
/** @var array<int, int> */
|
||||
public array $price = [100, 400];
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return <<<'BLADE'
|
||||
<div>
|
||||
<x-slider wire:model.live="volume" />
|
||||
<x-slider wire:model="price" range :max="500" />
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
};
|
||||
|
||||
Livewire::test($component)
|
||||
->assertSeeHtml('value="70"')
|
||||
->assertSeeHtml('style="left: calc(70% + 0px)"')
|
||||
->assertSeeHtml('value="100"')
|
||||
->assertSeeHtml('value="400"');
|
||||
});
|
||||
|
||||
it('replaces the hint with the validation message and marks the input invalid', function () {
|
||||
$errors = (new ViewErrorBag)->put('default', new MessageBag(['volume' => ['Too loud.'], 'price.1' => ['Too expensive.']]));
|
||||
|
||||
$this->withViewErrors([]);
|
||||
view()->share('errors', $errors);
|
||||
|
||||
expect((string) $this->blade('<x-slider name="volume" hint="Quietly" id="volume" />'))
|
||||
->toContain('Too loud.')
|
||||
->toContain('text-error')
|
||||
->toContain('aria-invalid="true"')
|
||||
->toContain('aria-describedby="volume-hint"')
|
||||
->not->toContain('Quietly');
|
||||
|
||||
expect((string) $this->blade('<x-slider name="price" range hint="Per night" />'))
|
||||
->toContain('Too expensive.')
|
||||
->not->toContain('Per night');
|
||||
|
||||
expect((string) $this->blade('<x-slider name="balance" hint="Centre is even" />'))
|
||||
->toContain('Centre is even')
|
||||
->not->toContain('aria-invalid');
|
||||
});
|
||||
|
||||
it('sizes the track and the handle by Expressive\'s tokens', function (string $size, string $track, string $handle, string $body) {
|
||||
expect((string) $this->blade('<x-slider :size="$size" />', ['size' => $size]))
|
||||
->toContain("data-size=\"{$size}\"")
|
||||
->toContain("-translate-y-1/2 {$track}\"")
|
||||
->toContain($handle)
|
||||
->toMatch("/group\\/slider [^\"]* {$body} /");
|
||||
})->with([
|
||||
'xs' => ['xs', 'h-4', 'h-11 group-data-focused/thumb:h-9.5', 'h-12'],
|
||||
'sm' => ['sm', 'h-6', 'h-11 group-data-focused/thumb:h-9.5', 'h-12'],
|
||||
'md' => ['md', 'h-10', 'h-11 group-data-focused/thumb:h-9.5', 'h-12'],
|
||||
'lg' => ['lg', 'h-14', 'h-17 group-data-focused/thumb:h-15.5', 'h-17'],
|
||||
'xl' => ['xl', 'h-24', 'h-27 group-data-focused/thumb:h-25.5', 'h-27'],
|
||||
]);
|
||||
|
||||
it('insets an icon in the track from md, but not on a small, range or centred slider', function () {
|
||||
expect((string) $this->blade('<x-slider size="md" icon="volume_up" value="60" />'))
|
||||
->toContain('data-track-icon')
|
||||
->toMatch('/data-track-icon\s+data-active/')
|
||||
->toContain('text-primary data-active:text-secondary-container');
|
||||
|
||||
expect((string) $this->blade('<x-slider size="xl" icon="volume_up" value="2" />'))
|
||||
->toMatch('/data-track-icon\s+class/')
|
||||
->toContain('size-8');
|
||||
|
||||
foreach (['<x-slider size="sm" icon="volume_up" />', '<x-slider size="lg" icon="volume_up" range />', '<x-slider size="lg" icon="volume_up" centered />'] as $template) {
|
||||
expect((string) $this->blade($template))->not->toContain('data-track-icon');
|
||||
}
|
||||
});
|
||||
|
||||
it('fills a centred slider from the middle', function () {
|
||||
expect((string) $this->blade('<x-slider :min="-50" :max="50" value="25" centered />'))
|
||||
->toContain('data-centered')
|
||||
->toMatch('/data-segment="start"\s+class="[^"]*"\s+style="left: calc\(0% \+ 0px\); width: calc\(50% - 6px\)/')
|
||||
->toMatch('/data-segment="active"\s+class="[^"]*"\s+style="left: calc\(50% \+ 0px\); width: calc\(25% - 8px\)/')
|
||||
->toMatch('/data-segment="end"\s+class="[^"]*"\s+style="left: calc\(75% \+ 8px\)/');
|
||||
});
|
||||
|
||||
it('marks every step when asked, and no more than 200 of them', function () {
|
||||
$html = (string) $this->blade('<x-slider ticks :max="10" value="5" />');
|
||||
|
||||
expect(substr_count($html, 'data-tick="'))->toBe(11)
|
||||
->and($html)->toContain('data-tick="0.5"')
|
||||
->and($html)->toContain('bg-primary data-active:bg-secondary-container');
|
||||
|
||||
expect((string) $this->blade('<x-slider ticks :max="1000" />'))->not->toContain('data-tick=')
|
||||
->and((string) $this->blade('<x-slider ticks step="any" />'))->not->toContain('data-tick=')
|
||||
->and((string) $this->blade('<x-slider :max="10" />'))->not->toContain('data-tick=');
|
||||
});
|
||||
|
||||
it('shows the value label on drag, always, or never', function () {
|
||||
expect((string) $this->blade('<x-slider />'))->toContain('opacity-0 scale-75 group-data-pressed/thumb:opacity-100');
|
||||
|
||||
expect((string) $this->blade('<x-slider value-label="always" value="30" />'))
|
||||
->toContain('data-value-label')
|
||||
->toContain('mt-9')
|
||||
->not->toContain('opacity-0 scale-75');
|
||||
|
||||
expect((string) $this->blade('<x-slider value-label="never" />'))->not->toContain('data-value-label');
|
||||
});
|
||||
|
||||
it('draws in the colour and its container, and greys out when disabled', function (string $color, string $active, string $inactive) {
|
||||
expect((string) $this->blade('<x-slider :color="$color" value="50" disabled />', ['color' => $color]))
|
||||
->toContain("absolute inset-y-0 {$active} group-has-disabled/slider:bg-on-surface/38")
|
||||
->toContain("absolute inset-y-0 {$inactive} group-has-disabled/slider:bg-on-surface/12")
|
||||
->toMatch('/<input[^>]*\sdisabled[\s>]/');
|
||||
})->with([
|
||||
'primary' => ['primary', 'bg-primary', 'bg-secondary-container'],
|
||||
'tertiary' => ['tertiary', 'bg-tertiary', 'bg-tertiary-container'],
|
||||
'error' => ['error', 'bg-error', 'bg-error-container'],
|
||||
'unknown' => ['pink', 'bg-primary', 'bg-secondary-container'],
|
||||
]);
|
||||
Reference in New Issue
Block a user