Cover six actions/communication behaviours the browser suite missed

Plan step 36's handoff listed six behaviours no browser test exercised
yet:
- FieldsTest: <x-select>'s open list uses the customizable picker
  (appearance: base-select) where the browser supports it, drawn as
  M3's 48px-row menu.
- CommunicationTest: a two-line snackbar's action stays on the
  title's row from 600px, only wrapping under it below that
  (toast.css's [data-md-toast-wrap]).
- ActionsTest: a disabled menu item reached by the keyboard withholds
  its state layer (interaction.css's [aria-disabled="true"] rule).
- DataTest: a paginator's page step shows md-focus-ring's outline on
  keyboard focus, alongside its already-tested 48px target.
- CommunicationTest: a transient rich tooltip stays open 1.5s after
  focus leaves rather than closing at once (rich-tooltip.js's
  LEAVE_GRACE_MS).
- ActionsTest: an xs or sm group segment takes a press at its 48px
  edge, past its drawn size, now that the target comes from the
  shared md-touch-target class rather than a rule of its own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 18:40:43 +02:00
co-authored by Claude Sonnet 5
parent 4bf1ca13a3
commit a29a147f1a
4 changed files with 152 additions and 0 deletions
+76
View File
@@ -171,6 +171,20 @@ it('opens a menu from the keyboard, on the last item with ArrowUp', function ()
->assertScript(focused("textContent.trim().startsWith('Delete')"));
});
it('shows no state layer on a disabled menu item even when keyboard focus reaches it', function () {
$trigger = '#menus button:has-text("Sort")';
$reset = '#menus [role="menuitem"]:has-text("Reset")';
$page = showcase('menus')->click($trigger);
// M3 keeps a disabled item reachable so a person can find out it is there, but
// .md-state-layer withholds the layer itself from [aria-disabled="true"] (interaction.css).
$page->keys(':focus', 'End')
->assertScript(focused("textContent.trim().startsWith('Reset')"))
->assertAttribute($reset, 'aria-disabled', 'true')
->assertScript("getComputedStyle(document.activeElement, '::before').display === 'none'");
});
it('opens a menu the moment the page can be used', function () {
// The guard against a light-dismiss press reopening the menu once measured from the page's
// time origin, and swallowed every click in the first quarter second.
@@ -501,6 +515,68 @@ it('takes a press on a small connected segment at its 48px edge, past what it dr
JS);
});
class GroupSizeProbe extends Component
{
public function render(): string
{
return <<<'BLADE'
<div class="grid gap-6 p-4">
<x-group name="xs-group" size="xs" :options="[['id' => 'a', 'name' => 'A'], ['id' => 'b', 'name' => 'B']]" />
<x-group name="sm-group" size="sm" :options="[['id' => 'a', 'name' => 'A'], ['id' => 'b', 'name' => 'B']]" />
</div>
BLADE;
}
}
function groupSizeProbe()
{
Livewire::component('group-size-probe', GroupSizeProbe::class);
Route::middleware('web')->get('/group-size-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:group-size-probe />
@livewireScripts
</body>
</html>
BLADE));
return visit('/group-size-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'");
}
it('takes a press at an xs or sm group segment\'s 48px edge, past what it draws, from the shared classes', function () {
// M3: "XS and S connected button groups have a 48dp target area and a 48dp minimum width" —
// group.blade.php reaches it with md-touch-target, not a rule of its own.
groupSizeProbe()
->assertScript(<<<'JS'
(() => {
const segment = document.querySelector('input[name="xs-group"][value="a"]').parentElement
const box = segment.getBoundingClientRect()
const x = box.left + box.width / 2
return box.height < 48
&& document.elementFromPoint(x, box.top - 3) === segment
&& document.elementFromPoint(x, box.bottom + 3) === segment
})()
JS)
->assertScript(<<<'JS'
(() => {
const segment = document.querySelector('input[name="sm-group"][value="a"]').parentElement
const box = segment.getBoundingClientRect()
const x = box.left + box.width / 2
return box.height < 48
&& document.elementFromPoint(x, box.top - 3) === segment
&& document.elementFromPoint(x, box.bottom + 3) === segment
})()
JS);
});
it('keeps one choice in a single, required selection group', function () {
$group = '#buttons [data-md-selection="single"]';
$view = "document.querySelector('{$group}').parentElement.querySelector('code').textContent";