Keep a focused field's focus edge under the pointer
The hover rule's `:not(:has([data-md-field-control]:disabled))` weighs as much as its argument, so hover weighed (0,5,0) against focus's (0,2,0), and the error's hover (0,6,0) against the error's focus (0,4,0): a focused field with the pointer on it drew a 2px on-surface edge instead of primary, a field in error on-error-container instead of error, in both variants. A customizable select with its menu open lost the same way, since focus is then on an option in the top layer and only select.css's `:open` rule (0,3,1) draws the focus edge. M3 layers focus over hover. The disabled condition is now weightless inside `:where()`, still keeping hover off a disabled field, and the focus rule gains the field ancestor: focus (0,3,0) outweighs hover (0,2,0), the error's focus (0,4,0) the error's hover (0,3,0), and the open select both. An open select in error drew primary, not error, and now draws error. A browser test hovers and clicks an outlined, a filled and an invalid field and opens a select and a select in error, comparing each edge with its role; it fails without the change (the error select without select.css's rule). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0641071a06
commit
029c78d301
@@ -2,6 +2,8 @@
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\MessageBag;
|
||||
use Illuminate\Support\ViewErrorBag;
|
||||
use Livewire\Component;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@@ -246,6 +248,87 @@ it('lights an enabled field\'s outline on hover, and never a disabled one\'s', f
|
||||
->assertScript("/rgba?\\(.*,\\s*0?\\.\\d+\\)|color\\(srgb .* \\/ 0?\\.\\d+\\)/.test('{$disabled}')");
|
||||
});
|
||||
|
||||
/**
|
||||
* An outlined, a filled and an invalid field and a select, beside the three roles their edges take,
|
||||
* with nothing that moves them after the first paint.
|
||||
*/
|
||||
function fieldStateProbe(): mixed
|
||||
{
|
||||
Route::middleware('web')->get('/field-state-probe', function () {
|
||||
view()->share('errors', (new ViewErrorBag)->put('default', new MessageBag(['broken' => ['Say what it is.'], 'broken-choice' => ['Choose one.']])));
|
||||
|
||||
return Blade::render(<<<'BLADE'
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<x-theme-script />
|
||||
@vite(config('livewire-material.showcase.vite'))
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body style="background-color: var(--md-sys-color-surface);">
|
||||
<div style="display: grid; max-width: 448px; gap: var(--md-sys-measurement-space300); padding: var(--md-sys-measurement-space200);">
|
||||
<p id="primary" style="color: var(--md-sys-color-primary);">primary</p>
|
||||
<p id="on-surface" style="color: var(--md-sys-color-on-surface);">on-surface</p>
|
||||
<p id="error" style="color: var(--md-sys-color-error);">error</p>
|
||||
<x-input id="outlined-field" label="Outlined" />
|
||||
<x-input id="filled-field" label="Filled" variant="filled" />
|
||||
<x-input id="invalid-field" label="Invalid" name="broken" />
|
||||
<x-select id="select-field" label="Expires" :options="[['id' => '1', 'name' => '1 hour'], ['id' => '24', 'name' => '1 day']]" />
|
||||
<x-select id="invalid-select-field" label="Invalid expiry" name="broken-choice" :options="[['id' => '1', 'name' => '1 hour'], ['id' => '24', 'name' => '1 day']]" />
|
||||
</div>
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
BLADE);
|
||||
});
|
||||
|
||||
return visit('/field-state-probe')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'");
|
||||
}
|
||||
|
||||
/** A field's edge, "width colour", once its colour transition has run: `Top` outlined, `Bottom` filled. */
|
||||
function settledEdge(string $id, string $side = 'Top'): string
|
||||
{
|
||||
return "(async () => { const outline = document.querySelector('[data-md-field]:has(#{$id}) [data-md-field-outline]'); await Promise.allSettled(outline.getAnimations().map((animation) => animation.finished)); const style = getComputedStyle(outline); return style.border{$side}Width + ' ' + style.border{$side}Color })()";
|
||||
}
|
||||
|
||||
it('keeps a focused field\'s focus edge under the pointer, as M3 layers focus over hover', function () {
|
||||
$page = fieldStateProbe();
|
||||
$role = fn (string $id): string => $page->script("getComputedStyle(document.getElementById('{$id}')).color");
|
||||
|
||||
// Hovered alone, the edge still answers the pointer.
|
||||
$page->hover('[data-md-field]:has(#outlined-field) [data-md-field-box]');
|
||||
|
||||
expect($page->script(settledEdge('outlined-field')))->toBe('1px '.$role('on-surface'));
|
||||
|
||||
// A click focuses the field and leaves the pointer over it.
|
||||
$page->click('#outlined-field');
|
||||
|
||||
expect($page->script(settledEdge('outlined-field')))->toBe('2px '.$role('primary'));
|
||||
|
||||
$page->click('#filled-field');
|
||||
|
||||
expect($page->script(settledEdge('filled-field', 'Bottom')))->toBe('2px '.$role('primary'));
|
||||
|
||||
$page->click('#invalid-field');
|
||||
|
||||
expect($page->script(settledEdge('invalid-field')))->toBe('2px '.$role('error'));
|
||||
|
||||
// A customizable select reads as focused while its menu is open, though focus is then on an
|
||||
// option in the top layer: in primary under the pointer, and in error when the field is.
|
||||
if ($page->script("CSS.supports('appearance', 'base-select')") === true) {
|
||||
$page->click('#select-field')->assertScript("document.getElementById('select-field').matches(':open')");
|
||||
|
||||
expect($page->script(settledEdge('select-field')))->toBe('2px '.$role('primary'));
|
||||
|
||||
$page->keys('#select-field', 'Escape')->assertScript("! document.getElementById('select-field').matches(':open')");
|
||||
|
||||
$page->click('#invalid-select-field')->assertScript("document.getElementById('invalid-select-field').matches(':open')");
|
||||
|
||||
expect($page->script(settledEdge('invalid-select-field')))->toBe('2px '.$role('error'));
|
||||
}
|
||||
});
|
||||
|
||||
it('counts the characters as they are typed, marks a count past the maximum and says it once typing stops', function () {
|
||||
$counter = "document.querySelector('[data-md-field]:has(#bio-field) [data-md-field-counter]')";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user