Hide a disabled fab button on a compact window, as M3 removes a FAB
`<x-button fab>` is an extended FAB below `medium` and a filled button from there, but a disabled one was still drawn as the FAB below `medium`: the compact FAB's rule set no state of its own, so the button stood greyed out and fixed over the content. M3 never disables a FAB: "if its action is unavailable, remove the FAB entirely". A `fab` given `disabled` (a `<button disabled>`, or a link's `aria-disabled="true"`) now renders `data-md-unavailable`, and button.css draws it `display: none` below `medium`, which also takes it out of the accessibility tree and the Tab order; from `medium` it is the disabled filled button it was. The mark comes from the prop, not from `:disabled`, because a `spinner` puts `disabled` on its button while the action runs, and that FAB is busy rather than unavailable: hidden, it would vanish instead of showing its loading indicator. The docblocks and the skill's button table say so. A browser test renders an enabled fab with a slow spinner action, a disabled one and a disabled link on a 393px window: the disabled two are not rendered, the busy one stays on screen with its indicator, and at 600px both disabled ones are the disabled button again; it fails without the change in Chrome, Firefox and Safari (and in Chrome with `:disabled` alone as the condition, on the busy FAB). A feature test reads the mark and the rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cf63f11bfb
commit
6d4487feb3
@@ -509,6 +509,77 @@ it('collapses an extended FAB to a FAB while the page scrolls down, and extends
|
||||
->assertScript("{$fab}.getBoundingClientRect().width > 56");
|
||||
});
|
||||
|
||||
class DisabledFabProbe extends Component
|
||||
{
|
||||
public int $created = 0;
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
usleep(800_000);
|
||||
|
||||
$this->created++;
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return <<<'BLADE'
|
||||
<div>
|
||||
<p>created: <span id="created">{{ $created }}</span></p>
|
||||
<x-button id="enabled-fab" fab icon="add" label="New plan" wire:click="create" spinner />
|
||||
<x-button id="disabled-fab" fab icon="add" label="New route" disabled />
|
||||
<x-button id="disabled-link-fab" fab icon="add" label="New course" link="/courses/new" disabled />
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
}
|
||||
|
||||
it('hides a disabled fab button below 600px, as M3 removes a FAB whose action is unavailable, and keeps the disabled button from 600px', function () {
|
||||
Livewire::component('disabled-fab-probe', DisabledFabProbe::class);
|
||||
|
||||
Route::middleware('web')->get('/disabled-fab-probe', fn () => 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);">
|
||||
<livewire:disabled-fab-probe />
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
BLADE));
|
||||
|
||||
// [rendered, fixed as a FAB]: `display: none` draws nothing and takes the button out of the
|
||||
// accessibility tree and the Tab order with it.
|
||||
$state = fn (string $id): string => "(() => {
|
||||
const button = document.getElementById('{$id}');
|
||||
|
||||
return [getComputedStyle(button).display !== 'none' && button.checkVisibility(), getComputedStyle(button).position === 'fixed'];
|
||||
})()";
|
||||
|
||||
$page = visit('/disabled-fab-probe')->resize(393, 800)->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Livewire !== 'undefined'");
|
||||
|
||||
expect($page->script($state('enabled-fab')))->toBe([true, true])
|
||||
->and($page->script($state('disabled-fab')))->toBe([false, true])
|
||||
->and($page->script($state('disabled-link-fab')))->toBe([false, true]);
|
||||
|
||||
// A spinner disables its button while the action runs; that FAB is busy, not unavailable, and
|
||||
// stays on screen with its loading indicator.
|
||||
$page->click('#enabled-fab')
|
||||
->assertScript("(() => { const button = document.getElementById('enabled-fab'); return button.hasAttribute('data-loading') && button.disabled && button.checkVisibility() && button.querySelector('[data-md-button-spinner]').checkVisibility(); })()")
|
||||
->assertSeeIn('#created', '1');
|
||||
|
||||
$page->resize(600, 800)->assertScript("getComputedStyle(document.getElementById('enabled-fab')).position !== 'fixed'");
|
||||
|
||||
expect($page->script($state('disabled-fab')))->toBe([true, false])
|
||||
->and($page->script($state('disabled-link-fab')))->toBe([true, false])
|
||||
// The disabled filled button it is from medium: on-surface at 10% behind 38% ink.
|
||||
->and($page->script("getComputedStyle(document.getElementById('disabled-fab')).backgroundColor"))->not->toBe($page->script("getComputedStyle(document.getElementById('enabled-fab')).backgroundColor"));
|
||||
});
|
||||
|
||||
it('takes a press on a small connected segment at its 48px edge, past what it draws', function () {
|
||||
// M3: "XS and S connected button groups have a 48dp target area and a 48dp minimum width".
|
||||
showcase()->assertScript(<<<'JS'
|
||||
|
||||
@@ -265,6 +265,15 @@ it('is a FAB on a compact window and a filled button from medium, in one element
|
||||
->and($fab['bottom'])->toBe('calc(var(--material-bottom-bar, 0px) + var(--material-snackbar-height, 0px) + var(--md-sys-measurement-space200))');
|
||||
});
|
||||
|
||||
it('marks a disabled fab button unavailable, which is not drawn below medium, and nothing else', function () {
|
||||
expect(buttonAttributes('<x-button label="New share" icon="add" fab disabled />'))->toMatchArray(['data-md-unavailable' => 'data-md-unavailable', 'disabled' => 'disabled'])
|
||||
->and(buttonAttributes('<x-button label="New share" icon="add" link="/shares/new" fab disabled />'))->toMatchArray(['data-md-unavailable' => 'data-md-unavailable', 'aria-disabled' => 'true'])
|
||||
// A spinner disables the button only while its action runs: busy, not unavailable.
|
||||
->and(buttonAttributes('<x-button label="New share" icon="add" fab wire:click="create" spinner />'))->not->toHaveKey('data-md-unavailable')
|
||||
->and(buttonAttributes('<x-button label="Delete" disabled />'))->not->toHaveKey('data-md-unavailable')
|
||||
->and(buttonCss()->declarations('[data-md-button][data-md-compact-fab][data-md-unavailable]', ['@media (width < 600px)']))->toBe(['display' => 'none']);
|
||||
});
|
||||
|
||||
it('submits a form when asked', function () {
|
||||
$this->blade('<x-button label="Save" type="submit" />')
|
||||
->assertSee('type="submit"', false);
|
||||
|
||||
Reference in New Issue
Block a user