{{-- 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))->class([
'md-state-layer',
'md-focus-ring',
'md-touch-target' => in_array($size, ['xs', 'sm'], true),
]);
@endphp
<{{ $tag }} {{ $attributes }}>
@if ($spinnerTarget)
@endif
@if ($icon)
@endif
@unless ($iconOnly)
{{ $label ?? $slot }}
@endunless
@if ($iconRight)
@endif
@if ($tip !== null)
@endif
{{ $tag }}>