diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index e1d51567..1b58f02b 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -291,7 +291,7 @@ Attributes go to the leading button; the slot is the menu. `variant` (`filled` d ### `` -`` — `size` `sm` 56px (default), `md` 80px, `lg` 96px; with `label` it is an extended FAB. The glyph is filled, as M3 requires of a FAB. `color` `primary`/`secondary`/`tertiary`, drawn in the container, or `variant="filled"`. It does not position itself; wrap it (`
` — M3's 16dp margin, 24dp from `large`). `link`, `external`, `disabled`, `type`. +`` — `size` `sm` 56px (default), `md` 80px, `lg` 96px; with `label` it is an extended FAB. The glyph is filled, as M3 requires of a FAB. `color` `primary`/`secondary`/`tertiary`, drawn in the container, or `variant="filled"`. It does not position itself; wrap it (`
` — M3's 16dp margin, 24dp from `large`). `link`, `external`, `type`. There is no `disabled`: M3 says to remove a FAB whose action is unavailable, so hide it instead. `data-fab` on the root lets a place restyle a nested FAB (a rail flattens it to elevation 0). ### ``, `` diff --git a/resources/views/components/fab.blade.php b/resources/views/components/fab.blade.php index 494b0291..647336f1 100644 --- a/resources/views/components/fab.blade.php +++ b/resources/views/components/fab.blade.php @@ -12,6 +12,14 @@ The glyph is filled, not outlined: M3 says so twice, in the FAB's anatomy and again in its guidelines ("icon should be filled (not outlined) and unambiguous"). + There is no `disabled`: M3 says never to disable a FAB — "if its action is unavailable, + remove the FAB entirely instead" — so hide it (`@if`), or leave it out of the render, rather + than greying it out. A form-submit FAB that must not be pressed twice takes the caller's own + `wire:loading.attr="disabled"`. + + `data-fab` marks the root, so a place a FAB sits in can draw it its own way: a navigation + rail flattens a nested FAB to elevation 0. + Sizes, corners and elevation from FabBaseline/Medium/LargeTokens and ExtendedFab*Tokens (androidx Compose Material 3, Apache-2.0). --}} @@ -24,7 +32,6 @@ 'link' => null, 'external' => false, 'tooltip' => null, - 'disabled' => false, 'type' => 'button', ]) @@ -39,7 +46,7 @@ : ['primary' => 'bg-primary-container text-on-primary-container', 'secondary' => 'bg-secondary-container text-on-secondary-container', 'tertiary' => 'bg-tertiary-container text-on-tertiary-container'][$color]; $dimensions = $extended - ? ['sm' => 'h-14 min-w-14 gap-2 px-4 rounded-corner-lg type-title-md', 'md' => 'h-20 min-w-20 gap-3 px-[26px] rounded-corner-lg-increased type-title-lg', 'lg' => 'h-24 min-w-24 gap-4 px-7 rounded-corner-xl type-headline-sm'][$size] + ? ['sm' => 'h-14 min-w-20 gap-2 px-4 rounded-corner-lg type-title-md', 'md' => 'h-20 min-w-20 gap-4 px-[26px] rounded-corner-lg-increased type-title-lg', 'lg' => 'h-24 min-w-24 gap-5 px-7 rounded-corner-xl type-headline-sm'][$size] : ['sm' => 'size-14 rounded-corner-lg', 'md' => 'size-20 rounded-corner-lg-increased', 'lg' => 'size-24 rounded-corner-xl'][$size]; $iconSize = ['sm' => 'size-6', 'md' => 'size-7', 'lg' => 'size-8'][$size]; @@ -52,7 +59,6 @@ 'shadow-elevation-3 hover:shadow-elevation-4 transition-[box-shadow,background-color,color] duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast', $dimensions, $colours, - 'disabled:cursor-not-allowed disabled:bg-on-surface/10 disabled:text-on-surface/38 disabled:shadow-none', ]) ->merge(array_filter([ 'href' => $isLink ? $link : null, @@ -60,7 +66,8 @@ 'rel' => $isLink && $external ? 'noopener' : null, 'wire:navigate' => $isLink && ! $external && ! $attributes->has('wire:navigate') ? true : null, 'type' => $isLink ? null : $type, - 'disabled' => ! $isLink && $disabled ? true : null, + // The hook a place uses to draw a nested FAB its own way: a rail flattens it to 0dp. + 'data-fab' => true, 'aria-label' => ! $extended && ! $attributes->has('aria-label') ? $tooltip : null, 'style' => $anchor ? "anchor-name: {$anchor}" : null, ], fn ($value): bool => $value !== null)); diff --git a/tests/Feature/Components/FabTest.php b/tests/Feature/Components/FabTest.php index b6cff0c8..6d2e4e11 100644 --- a/tests/Feature/Components/FabTest.php +++ b/tests/Feature/Components/FabTest.php @@ -27,12 +27,32 @@ it('fills the FAB\'s glyph, as M3 asks twice over', function () { it('extends with a label', function () { expect((string) $this->blade('')) - ->toContain('h-20 min-w-20 gap-3 px-[26px] rounded-corner-lg-increased type-title-lg') + ->toContain('h-20 min-w-20 gap-4 px-[26px] rounded-corner-lg-increased type-title-lg') ->toContain('bg-tertiary text-on-tertiary') ->toContain('Upload') ->not->toContain('aria-label'); }); +it('gives the extended FAB M3\'s gaps and its 80px minimum width', function (string $size, string $classes) { + expect((string) $this->blade(""))->toContain($classes); +})->with([ + 'sm' => ['sm', 'h-14 min-w-20 gap-2'], + 'md' => ['md', 'h-20 min-w-20 gap-4'], + 'lg' => ['lg', 'h-24 min-w-24 gap-5'], +]); + +it('marks its root for the places that draw a nested FAB their own way', function () { + expect((string) $this->blade(''))->toContain('data-fab'); +}); + +it('cannot be disabled, because M3 says to remove a FAB instead', function () { + $html = (string) $this->blade(''); + + // `disabled` is no longer a prop, so it falls through as a plain attribute rather than + // painting M3's disabled treatment. + expect($html)->not->toContain('disabled:bg-on-surface/10')->not->toContain('disabled:shadow-none'); +}); + it('opens a FAB menu of end-aligned actions above it', function () { $html = (string) $this->blade(<<<'BLADE'