Add M3 text fields, selects and selection controls
tests / feature (8.4) (push) Successful in 1m5s
tests / feature (8.5) (push) Successful in 1m4s
tests / lint (push) Successful in 59s
tests / browser (chrome, chromium) (push) Successful in 2m54s
tests / browser (firefox, firefox) (push) Successful in 3m3s
tests / browser (safari, webkit) (push) Successful in 4m13s
tests / feature (8.4) (push) Successful in 1m5s
tests / feature (8.5) (push) Successful in 1m4s
tests / lint (push) Successful in 59s
tests / browser (chrome, chromium) (push) Successful in 2m54s
tests / browser (firefox, firefox) (push) Successful in 3m3s
tests / browser (safari, webkit) (push) Successful in 4m13s
Outlined and filled text fields (input, password, auto-growing textarea, native select with the customizable select menu, file), checkbox with an indeterminate state, radio buttons, the M3 switch and form, ported from ReStride and extended. The first half of 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
ad724662ca
commit
7f92f1cb29
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Livewire\Component;
|
||||
use Livewire\Livewire;
|
||||
|
||||
class FieldProbe extends Component
|
||||
{
|
||||
public string $name = '';
|
||||
|
||||
public string $hours = '24';
|
||||
|
||||
/** @var list<string> */
|
||||
public array $files = [];
|
||||
|
||||
public string $audience = 'anyone';
|
||||
|
||||
public bool $public = false;
|
||||
|
||||
public string $message = '';
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate(['name' => 'required']);
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return <<<'BLADE'
|
||||
<div class="grid max-w-md gap-4 p-4">
|
||||
<p>name: <span id="name">{{ $name }}</span></p>
|
||||
<p>hours: <span id="hours">{{ $hours }}</span></p>
|
||||
<p>files: <span id="files">{{ implode(',', $files) }}</span></p>
|
||||
<p>audience: <span id="audience">{{ $audience }}</span></p>
|
||||
<p>public: <span id="public-state">{{ var_export($public, true) }}</span></p>
|
||||
|
||||
<x-input id="name-field" label="Share name" wire:model.live="name" clearable copyable />
|
||||
<x-password id="password-field" label="Password" />
|
||||
<x-select id="hours-field" label="Expires" wire:model.live="hours" :options="[['id' => '1', 'name' => '1 hour'], ['id' => '24', 'name' => '1 day']]" />
|
||||
|
||||
<x-checkbox id="all" label="All files" :indeterminate="count($files) > 0 && count($files) < 3" />
|
||||
@foreach (['a', 'b', 'c'] as $file)
|
||||
<x-checkbox id="file-{{ $file }}" :label="'File '.$file" value="{{ $file }}" wire:model.live="files" wire:key="file-{{ $file }}" />
|
||||
@endforeach
|
||||
|
||||
<x-radio label="Audience" wire:model.live="audience" :options="[['id' => 'anyone', 'name' => 'Anyone'], ['id' => 'team', 'name' => 'Team']]" />
|
||||
<x-toggle id="public" label="Public" wire:model.live="public" />
|
||||
<x-textarea id="message" label="Message" rows="2" max-rows="4" wire:model="message" />
|
||||
|
||||
<x-button label="Save" wire:click="save" />
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
}
|
||||
|
||||
function fieldProbe()
|
||||
{
|
||||
Livewire::component('field-probe', FieldProbe::class);
|
||||
|
||||
Route::middleware('web')->get('/field-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:field-probe />
|
||||
<x-toast />
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
BLADE));
|
||||
|
||||
return visit('/field-probe')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
}
|
||||
|
||||
it('floats the label while the field has focus or holds a value', function () {
|
||||
$label = "getComputedStyle(document.querySelector('label[for=\"name-field\"]')).fontSize";
|
||||
|
||||
$page = fieldProbe()->assertScript("{$label} === '16px'");
|
||||
|
||||
$page->click('#name-field')->assertScript("{$label} === '12px'");
|
||||
|
||||
$page->type('#name-field', 'Holiday')
|
||||
->assertSeeIn('#name', 'Holiday');
|
||||
|
||||
$page->click('#password-field')->assertScript("{$label} === '12px'");
|
||||
});
|
||||
|
||||
it('clears a field and tells Livewire', function () {
|
||||
fieldProbe()
|
||||
->type('#name-field', 'Holiday')
|
||||
->assertSeeIn('#name', 'Holiday')
|
||||
->click('[data-field-clear]')
|
||||
->assertScript("document.querySelector('#name-field').value === ''")
|
||||
->assertScript("document.querySelector('#name').textContent === ''")
|
||||
->assertScript("document.activeElement.id === 'name-field'");
|
||||
});
|
||||
|
||||
it('copies a field\'s value and says so', function () {
|
||||
$page = fieldProbe()->type('#name-field', 'Holiday');
|
||||
|
||||
$page->script("window.eval(\"Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: async (text) => { window.copied = text } } })\")");
|
||||
|
||||
$page->click('[data-field-copy]')
|
||||
->assertScript("window.copied === 'Holiday'")
|
||||
->assertSee('Copied to the clipboard');
|
||||
});
|
||||
|
||||
it('shows and hides a password', function () {
|
||||
fieldProbe()
|
||||
->type('#password-field', 'secret')
|
||||
->click('[data-field-reveal]')
|
||||
->assertScript("document.querySelector('#password-field').type === 'text'")
|
||||
->assertAttribute('[data-field-reveal]', 'aria-pressed', 'true')
|
||||
->click('[data-field-reveal]')
|
||||
->assertScript("document.querySelector('#password-field').type === 'password'");
|
||||
});
|
||||
|
||||
it('shows the server\'s error in place of the hint', function () {
|
||||
fieldProbe()
|
||||
->click('button:has-text("Save")')
|
||||
->assertSee('The name field is required.')
|
||||
->assertAttribute('#name-field', 'aria-invalid', 'true')
|
||||
->assertScript("document.querySelector('#name-field').closest('.field').hasAttribute('data-invalid')");
|
||||
});
|
||||
|
||||
it('binds a select to Livewire', function () {
|
||||
fieldProbe()
|
||||
->select('#hours-field', '1')
|
||||
->assertSeeIn('#hours', '1');
|
||||
});
|
||||
|
||||
it('keeps a partly ticked checkbox in step with the server', function () {
|
||||
$all = "document.querySelector('#all')";
|
||||
|
||||
$page = fieldProbe()->assertScript("! {$all}.indeterminate");
|
||||
|
||||
$page->click('label[for="file-a"]')
|
||||
->assertSeeIn('#files', 'a')
|
||||
->assertScript("{$all}.hasAttribute('data-indeterminate') && {$all}.indeterminate");
|
||||
|
||||
$page->click('label[for="file-b"]')
|
||||
->click('label[for="file-c"]')
|
||||
->assertSeeIn('#files', 'a,b,c')
|
||||
->assertScript("! {$all}.hasAttribute('data-indeterminate') && ! {$all}.indeterminate");
|
||||
});
|
||||
|
||||
it('moves between radio buttons with the arrow keys', function () {
|
||||
fieldProbe()
|
||||
->keys('input[type="radio"][value="anyone"]', 'ArrowDown')
|
||||
->assertSeeIn('#audience', 'team')
|
||||
->assertScript("document.querySelector('input[type=\"radio\"][value=\"team\"]').checked");
|
||||
});
|
||||
|
||||
it('turns a switch on from the keyboard and from its label', function () {
|
||||
$page = fieldProbe()
|
||||
->keys('#public', 'Space')
|
||||
->assertSeeIn('#public-state', 'true');
|
||||
|
||||
$page->click('label[for="public"]')
|
||||
->assertSeeIn('#public-state', 'false')
|
||||
->assertAttribute('#public', 'role', 'switch');
|
||||
});
|
||||
|
||||
it('grows a textarea with its text up to its maximum', function () {
|
||||
$height = "document.querySelector('#message').getBoundingClientRect().height";
|
||||
|
||||
$page = fieldProbe();
|
||||
|
||||
$rows = $page->script("({$height})");
|
||||
|
||||
$page->type('#message', "one\ntwo\nthree")
|
||||
->assertScript("({$height}) > {$rows}");
|
||||
|
||||
$three = $page->script("({$height})");
|
||||
|
||||
$page->type('#message', "one\ntwo\nthree\nfour\nfive\nsix\nseven")
|
||||
->assertScript("Math.abs(({$height}) - ({$three} + 24)) < 2")
|
||||
->assertScript("document.querySelector('#message').scrollHeight > document.querySelector('#message').clientHeight");
|
||||
});
|
||||
Reference in New Issue
Block a user