Take disabled off the FAB, and fix its extended metrics
M3 is explicit twice over — never disable a FAB, remove it instead — and the prop painted the full disabled treatment with nothing warning against it. It is gone; hiding the FAB is the documented answer. The extended FAB's icon-to-label gap goes to M3's 16dp at md and 20dp at lg, the small one takes the baseline's 80dp minimum width, and the root gains a `data-fab` hook for a place that draws a nested FAB its own way. Plan step 18, actions.md ACT-08, ACT-16, ACT-31. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
9828c9258a
commit
deb7ff8ab5
@@ -291,7 +291,7 @@ Attributes go to the leading button; the slot is the menu. `variant` (`filled` d
|
|||||||
|
|
||||||
### `<x-fab>`
|
### `<x-fab>`
|
||||||
|
|
||||||
`<x-fab icon="add" tooltip="New share" />` — `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 (`<div class="fixed end-4 bottom-4 large:end-6 large:bottom-6">` — M3's 16dp margin, 24dp from `large`). `link`, `external`, `disabled`, `type`.
|
`<x-fab icon="add" tooltip="New share" />` — `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 (`<div class="fixed end-4 bottom-4 large:end-6 large:bottom-6">` — 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).
|
||||||
|
|
||||||
### `<x-fab-menu>`, `<x-fab-menu-item>`
|
### `<x-fab-menu>`, `<x-fab-menu-item>`
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,14 @@
|
|||||||
The glyph is filled, not outlined: M3 says so twice, in the FAB's anatomy and again in its
|
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").
|
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
|
Sizes, corners and elevation from FabBaseline/Medium/LargeTokens and ExtendedFab*Tokens
|
||||||
(androidx Compose Material 3, Apache-2.0). --}}
|
(androidx Compose Material 3, Apache-2.0). --}}
|
||||||
|
|
||||||
@@ -24,7 +32,6 @@
|
|||||||
'link' => null,
|
'link' => null,
|
||||||
'external' => false,
|
'external' => false,
|
||||||
'tooltip' => null,
|
'tooltip' => null,
|
||||||
'disabled' => false,
|
|
||||||
'type' => 'button',
|
'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];
|
: ['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
|
$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];
|
: ['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];
|
$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',
|
'shadow-elevation-3 hover:shadow-elevation-4 transition-[box-shadow,background-color,color] duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast',
|
||||||
$dimensions,
|
$dimensions,
|
||||||
$colours,
|
$colours,
|
||||||
'disabled:cursor-not-allowed disabled:bg-on-surface/10 disabled:text-on-surface/38 disabled:shadow-none',
|
|
||||||
])
|
])
|
||||||
->merge(array_filter([
|
->merge(array_filter([
|
||||||
'href' => $isLink ? $link : null,
|
'href' => $isLink ? $link : null,
|
||||||
@@ -60,7 +66,8 @@
|
|||||||
'rel' => $isLink && $external ? 'noopener' : null,
|
'rel' => $isLink && $external ? 'noopener' : null,
|
||||||
'wire:navigate' => $isLink && ! $external && ! $attributes->has('wire:navigate') ? true : null,
|
'wire:navigate' => $isLink && ! $external && ! $attributes->has('wire:navigate') ? true : null,
|
||||||
'type' => $isLink ? null : $type,
|
'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,
|
'aria-label' => ! $extended && ! $attributes->has('aria-label') ? $tooltip : null,
|
||||||
'style' => $anchor ? "anchor-name: {$anchor}" : null,
|
'style' => $anchor ? "anchor-name: {$anchor}" : null,
|
||||||
], fn ($value): bool => $value !== null));
|
], fn ($value): bool => $value !== null));
|
||||||
|
|||||||
@@ -27,12 +27,32 @@ it('fills the FAB\'s glyph, as M3 asks twice over', function () {
|
|||||||
|
|
||||||
it('extends with a label', function () {
|
it('extends with a label', function () {
|
||||||
expect((string) $this->blade('<x-fab icon="upload" label="Upload" size="md" color="tertiary" variant="filled" />'))
|
expect((string) $this->blade('<x-fab icon="upload" label="Upload" size="md" color="tertiary" variant="filled" />'))
|
||||||
->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('bg-tertiary text-on-tertiary')
|
||||||
->toContain('<span>Upload</span>')
|
->toContain('<span>Upload</span>')
|
||||||
->not->toContain('aria-label');
|
->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("<x-fab icon=\"add\" label=\"New\" size=\"{$size}\" />"))->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('<x-fab icon="add" aria-label="New" />'))->toContain('data-fab');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot be disabled, because M3 says to remove a FAB instead', function () {
|
||||||
|
$html = (string) $this->blade('<x-fab icon="add" aria-label="New" disabled />');
|
||||||
|
|
||||||
|
// `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 () {
|
it('opens a FAB menu of end-aligned actions above it', function () {
|
||||||
$html = (string) $this->blade(<<<'BLADE'
|
$html = (string) $this->blade(<<<'BLADE'
|
||||||
<x-fab-menu label="New" color="secondary">
|
<x-fab-menu label="New" color="secondary">
|
||||||
|
|||||||
Reference in New Issue
Block a user