Plan step 36. <x-button> renders data-md-button with its variant, colour, size, shape, icon-button width, selected state and compact FAB as data-md-* attributes, and passes the glyph's size to <x-icon>. button.css draws Button*Tokens' geometry per size, the colour roles through custom properties that the disabled treatment replaces, the state layer, focus ring and 48px target, and the compact FAB below 600px. Its corner rules carry no specificity (:where), so a group's stylesheet reshapes the buttons inside it with any selector; the split button no longer passes an empty corners prop, which is gone. data-icon-button becomes data-md-icon-button, also in groups.css, toolbar.css and theme-toggle's own button (navigation group), and the AppBarTest assertions follow the new hooks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
170 lines
8.1 KiB
PHP
170 lines
8.1 KiB
PHP
{{-- A Material 3 Expressive button — label button, icon button or toggle — in one component.
|
|
|
|
`variant` is M3's style: `filled`, `tonal`, `outlined`, `elevated` or `text` (the default).
|
|
`color` is the role it is drawn in: `primary` (the default), `secondary`, `tertiary`, `error`,
|
|
`success`, `warning`, `info`; `tone` is the same prop under ReStride's name. The shorthands
|
|
`primary`, `danger` and `caution` mean filled primary, filled error and filled warning.
|
|
Unknown values fall back to the defaults rather than rendering nothing.
|
|
|
|
`size` is Expressive's scale — `xs` 32px, `sm` 40px (the default), `md` 56px, `lg` 96px,
|
|
`xl` 136px — each with its own padding, type style and icon size. `shape` is `round` (the
|
|
default) or `square`; either one squares off towards a smaller corner while pressed, on the
|
|
fast spatial spring, while the colours cross on the fast effects spring beside it (a colour
|
|
must never overshoot).
|
|
|
|
With an `icon` and no label it is an icon button: `width` is `narrow`, `default` or `wide`,
|
|
`variant="text"` is M3's standard icon button, and the tooltip or label names it for screen
|
|
readers. Its glyph is filled, as M3 asks of a default icon button. `selected` makes it a
|
|
toggle: `true` or `false` sets `aria-pressed` (not on a `link`, which is no toggle — give it
|
|
`aria-current` instead) and M3's selected colours, and a selected round button turns square
|
|
(a selected square icon button turns round); a toggle reads unselected outlined and selected
|
|
filled, which is the rest of the same rule. Text buttons are not toggles in M3; a selected
|
|
one takes the tonal container.
|
|
|
|
Values from androidx Compose Material 3's tokens (Button*Tokens, *IconButtonTokens,
|
|
Apache-2.0); the text button's label is primary, as Compose draws it, not the token's
|
|
on-surface-variant, which its own source marks as wrong.
|
|
|
|
Behaviour kept from maryUI: `link` renders an anchor with `wire:navigate` (unless `external`
|
|
or `no-wire-navigate`); `disabled` works on a link too, as `aria-disabled`; `spinner` shows
|
|
the loading indicator while the button's own `wire:click` runs (or the action named by a
|
|
string), drawn in the button's own ink — M3 asks an indicator embedded in another component
|
|
to take that component's label colour, so it stays visible on a filled or tonal container;
|
|
`responsive` hides the label below `expanded` (840px, where M3 lets buttons reposition);
|
|
`tooltip`, `tooltip-left`, `tooltip-right`
|
|
and `tooltip-bottom` attach a plain tooltip. `fab` is a page's create action: an extended FAB
|
|
pinned in the thumb zone on a compact window (below `medium`, 600px), a filled button from
|
|
there — one element either way. It lifts clear of a bottom bar and of a snackbar on screen
|
|
(`--material-bottom-bar`, `--material-snackbar-height`), because M3 puts a snackbar above a
|
|
FAB and never over one.
|
|
|
|
The view renders the props as `data-md-*` attributes and resources/css/components/button.css
|
|
draws them, in the package's components layer: a caller's class or unlayered CSS outranks
|
|
every rule there, `hidden` included. --}}
|
|
|
|
@props([
|
|
'label' => null,
|
|
'icon' => null,
|
|
'iconRight' => null,
|
|
'link' => null,
|
|
'external' => false,
|
|
'noWireNavigate' => false,
|
|
'variant' => null,
|
|
'color' => null,
|
|
'tone' => null,
|
|
'primary' => false,
|
|
'danger' => false,
|
|
'caution' => false,
|
|
'size' => 'sm',
|
|
'shape' => 'round',
|
|
'width' => 'default',
|
|
'selected' => null,
|
|
'spinner' => null,
|
|
'responsive' => false,
|
|
'tooltip' => null,
|
|
'tooltipLeft' => null,
|
|
'tooltipRight' => null,
|
|
'tooltipBottom' => null,
|
|
'fab' => false,
|
|
'disabled' => false,
|
|
'type' => 'button',
|
|
])
|
|
|
|
@php
|
|
$variant = match (true) {
|
|
in_array($variant, ['filled', 'tonal', 'outlined', 'text', 'elevated'], true) => $variant,
|
|
$primary || $danger || $caution || $fab => 'filled',
|
|
default => 'text',
|
|
};
|
|
|
|
$color = match (true) {
|
|
$danger => 'error',
|
|
$caution => 'warning',
|
|
in_array($color ?? $tone, ['primary', 'secondary', 'tertiary', 'error', 'success', 'warning', 'info'], true) => $color ?? $tone,
|
|
default => 'primary',
|
|
};
|
|
|
|
$size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm';
|
|
$shape = $shape === 'square' ? 'square' : 'round';
|
|
$width = in_array($width, ['narrow', 'default', 'wide'], true) ? $width : 'default';
|
|
$iconOnly = filled($icon) && blank($label) && $slot->isEmpty();
|
|
$isLink = filled($link);
|
|
$inert = $disabled && $isLink;
|
|
|
|
$tip = $tooltip ?? $tooltipLeft ?? $tooltipRight ?? $tooltipBottom;
|
|
$tipSide = match (true) {
|
|
$tooltipLeft !== null => 'left',
|
|
$tooltipRight !== null => 'right',
|
|
$tooltipBottom !== null => 'bottom',
|
|
default => 'top',
|
|
};
|
|
$anchor = $tip !== null ? '--material-button-'.\Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10)) : null;
|
|
|
|
$spinnerTarget = match (true) {
|
|
$spinner === true, $spinner === 1, $spinner === '1' => $attributes->whereStartsWith('wire:click')->first(),
|
|
is_string($spinner) && $spinner !== '' => $spinner,
|
|
default => null,
|
|
};
|
|
|
|
// The glyph's size in px (button.css draws the loading indicator at the same size). M3 draws a
|
|
// symbol from its 20px cut below 24px, which the icon component picks from the size.
|
|
$iconSize = $iconOnly
|
|
? ['xs' => 20, 'sm' => 24, 'md' => 24, 'lg' => 32, 'xl' => 40][$size]
|
|
: ['xs' => 20, 'sm' => 20, 'md' => 24, 'lg' => 32, 'xl' => 40][$size];
|
|
|
|
$tag = $isLink ? 'a' : 'button';
|
|
|
|
$attributes = $attributes->merge(array_filter([
|
|
'data-md-button' => true,
|
|
'data-md-variant' => $variant,
|
|
'data-md-color' => $color,
|
|
'data-md-size' => $size,
|
|
'data-md-shape' => $shape,
|
|
'data-md-icon-button' => $iconOnly ? true : null,
|
|
'data-md-width' => $iconOnly ? $width : null,
|
|
'data-md-selected' => $selected === null ? null : ($selected ? 'true' : 'false'),
|
|
'data-md-compact-fab' => $fab ? true : null,
|
|
'href' => $isLink ? $link : null,
|
|
'target' => $isLink && $external ? '_blank' : null,
|
|
'rel' => $isLink && $external ? 'noopener' : null,
|
|
'wire:navigate' => $isLink && ! $external && ! $noWireNavigate && ! $attributes->has('wire:navigate') ? true : null,
|
|
'aria-disabled' => $inert ? 'true' : null,
|
|
'tabindex' => $inert ? '-1' : null,
|
|
'type' => $isLink ? null : $type,
|
|
'disabled' => ! $isLink && $disabled ? true : null,
|
|
'aria-label' => $iconOnly && ! $attributes->has('aria-label') ? ($label ?? $tip) : null,
|
|
// A link is not a toggle: ARIA defines aria-pressed for buttons only. A selected link keeps
|
|
// the selected look; `aria-current` is the caller's to set (`:aria-current="'page'"`).
|
|
'aria-pressed' => $selected === null || $isLink ? null : ($selected ? 'true' : 'false'),
|
|
'wire:loading.attr' => $spinnerTarget ? 'disabled' : null,
|
|
'wire:target' => $spinnerTarget,
|
|
'style' => $anchor ? "anchor-name: {$anchor}" : null,
|
|
], fn ($value): bool => $value !== null));
|
|
@endphp
|
|
|
|
<{{ $tag }} {{ $attributes }}>
|
|
@if ($spinnerTarget)
|
|
<span wire:loading.flex wire:target="{{ $spinnerTarget }}" data-md-button-spinner>
|
|
<x-livewire-material::loading :label="false" />
|
|
</span>
|
|
@endif
|
|
|
|
@if ($icon)
|
|
<span data-md-button-icon @if ($spinnerTarget) wire:loading.remove wire:target="{{ $spinnerTarget }}" @endif>
|
|
<x-livewire-material::icon :name="$icon" :size="$iconSize" :filled="$selected === true || ($iconOnly && $selected === null)" />
|
|
</span>
|
|
@endif
|
|
|
|
@unless ($iconOnly)
|
|
<span data-md-button-label @if ($responsive) data-md-responsive @endif>{{ $label ?? $slot }}</span>
|
|
@endunless
|
|
|
|
@if ($iconRight)
|
|
<x-livewire-material::icon :name="$iconRight" :size="$iconSize" />
|
|
@endif
|
|
|
|
@if ($tip !== null)
|
|
<x-livewire-material::tooltip :text="$tip" :side="$tipSide" :anchor="$anchor" />
|
|
@endif
|
|
</{{ $tag }}>
|