Draw the button without Tailwind
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
This commit is contained in:
co-authored by
Claude Opus 5
parent
7bdd3ef61c
commit
4ad257034e
@@ -1,70 +1,137 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* The classes on the rendered button element.
|
||||
*/
|
||||
function buttonClasses(string $blade): string
|
||||
{
|
||||
preg_match('/<(?:button|a)\b[^>]*\bclass="([^"]*)"/', (string) test()->blade($blade), $matches);
|
||||
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
||||
|
||||
return $matches[1] ?? '';
|
||||
/**
|
||||
* The attributes of the rendered button or link element, by name.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
function buttonAttributes(string $blade): array
|
||||
{
|
||||
preg_match('/<(?:button|a)\b([^>]*)>/', (string) test()->blade($blade), $tag);
|
||||
preg_match_all('/([\w:.-]+)(?:="([^"]*)")?/', $tag[1] ?? '', $pairs, PREG_SET_ORDER);
|
||||
|
||||
return collect($pairs)->mapWithKeys(fn (array $pair): array => [$pair[1] => $pair[2] ?? ''])->all();
|
||||
}
|
||||
|
||||
it('is a text button in the primary colour by default', function () {
|
||||
expect(buttonClasses('<x-button label="Cancel" />'))
|
||||
->toContain('text-primary')
|
||||
->not->toContain('bg-primary');
|
||||
function buttonCss(): ComponentStylesheet
|
||||
{
|
||||
return ComponentStylesheet::read('button');
|
||||
}
|
||||
|
||||
it('is a text button in the primary colour by default, drawn by its stylesheet', function () {
|
||||
expect(buttonAttributes('<x-button label="Cancel" />'))->toBe([
|
||||
'data-md-button' => 'data-md-button',
|
||||
'data-md-variant' => 'text',
|
||||
'data-md-color' => 'primary',
|
||||
'data-md-size' => 'sm',
|
||||
'data-md-shape' => 'round',
|
||||
'type' => 'button',
|
||||
])
|
||||
->and(buttonCss()->declarations('[data-md-button]'))->toMatchArray([
|
||||
'--md-button-container' => 'transparent',
|
||||
'--md-button-label' => 'var(--md-button-color)',
|
||||
'--md-button-color' => 'var(--md-sys-color-primary)',
|
||||
'background-color' => 'var(--md-button-container)',
|
||||
'color' => 'var(--md-button-label)',
|
||||
]);
|
||||
});
|
||||
|
||||
it('draws each M3 variant', function (string $variant, string $classes) {
|
||||
expect(buttonClasses("<x-button label=\"Go\" variant=\"{$variant}\" />"))->toContain($classes);
|
||||
it('draws each M3 variant', function (string $variant, array $declarations) {
|
||||
expect(buttonAttributes("<x-button label=\"Go\" variant=\"{$variant}\" />"))->toMatchArray(['data-md-variant' => $variant])
|
||||
->and(buttonCss()->declarations("[data-md-button][data-md-variant='{$variant}']"))->toMatchArray($declarations);
|
||||
})->with([
|
||||
'filled' => ['filled', 'bg-primary text-on-primary'],
|
||||
'tonal' => ['tonal', 'bg-secondary-container text-on-secondary-container'],
|
||||
'outlined' => ['outlined', 'border-outline-variant text-on-surface-variant'],
|
||||
'elevated' => ['elevated', 'bg-surface-container-low text-primary'],
|
||||
'text' => ['text', 'text-primary'],
|
||||
'filled' => ['filled', ['--md-button-container' => 'var(--md-button-color)', '--md-button-label' => 'var(--md-button-on-color)']],
|
||||
'tonal' => ['tonal', ['--md-button-container' => 'var(--md-button-tone)', '--md-button-label' => 'var(--md-button-on-tone)']],
|
||||
'outlined' => ['outlined', ['--md-button-label' => 'var(--md-button-quiet)', '--md-button-outline' => 'var(--md-sys-color-outline-variant)', 'border' => 'var(--md-button-outline-width) solid var(--md-button-outline)']],
|
||||
'elevated' => ['elevated', ['--md-button-container' => 'var(--md-sys-color-surface-container-low)', '--md-button-elevation' => 'var(--md-sys-elevation-1)']],
|
||||
]);
|
||||
|
||||
it('draws a variant in another colour role', function () {
|
||||
expect(buttonClasses('<x-button label="Go" variant="filled" color="tertiary" />'))->toContain('bg-tertiary text-on-tertiary')
|
||||
->and(buttonClasses('<x-button label="Go" variant="tonal" color="error" />'))->toContain('bg-error-container text-on-error-container')
|
||||
->and(buttonClasses('<x-button label="Go" variant="outlined" tone="success" />'))->toContain('text-success');
|
||||
it('draws primary\'s tonal button in secondary-container and its quiet ink in on-surface-variant', function () {
|
||||
expect(buttonCss()->declarations('[data-md-button]'))->toMatchArray([
|
||||
'--md-button-tone' => 'var(--md-sys-color-secondary-container)',
|
||||
'--md-button-on-tone' => 'var(--md-sys-color-on-secondary-container)',
|
||||
'--md-button-tone-selected' => 'var(--md-sys-color-secondary)',
|
||||
'--md-button-quiet' => 'var(--md-sys-color-on-surface-variant)',
|
||||
])
|
||||
->and(buttonCss()->declarations("[data-md-button]:not([data-md-color='primary'])"))->toBe([
|
||||
'--md-button-tone-selected' => 'var(--md-button-color)',
|
||||
'--md-button-on-tone-selected' => 'var(--md-button-on-color)',
|
||||
'--md-button-quiet' => 'var(--md-button-color)',
|
||||
]);
|
||||
});
|
||||
|
||||
it('draws a variant in another colour role', function (string $color) {
|
||||
expect(buttonCss()->declarations("[data-md-button][data-md-color='{$color}']"))->toBe([
|
||||
'--md-button-color' => "var(--md-sys-color-{$color})",
|
||||
'--md-button-on-color' => "var(--md-sys-color-on-{$color})",
|
||||
'--md-button-tone' => "var(--md-sys-color-{$color}-container)",
|
||||
'--md-button-on-tone' => "var(--md-sys-color-on-{$color}-container)",
|
||||
]);
|
||||
})->with(['secondary', 'tertiary', 'error', 'success', 'warning', 'info']);
|
||||
|
||||
it('keeps the maryUI and ReStride shorthands', function () {
|
||||
expect(buttonClasses('<x-button label="Save" primary />'))->toContain('bg-primary text-on-primary')
|
||||
->and(buttonClasses('<x-button label="Delete" danger />'))->toContain('bg-error text-on-error')
|
||||
->and(buttonClasses('<x-button label="Take down" caution />'))->toContain('bg-warning text-on-warning');
|
||||
expect(buttonAttributes('<x-button label="Save" primary />'))->toMatchArray(['data-md-variant' => 'filled', 'data-md-color' => 'primary'])
|
||||
->and(buttonAttributes('<x-button label="Delete" danger />'))->toMatchArray(['data-md-variant' => 'filled', 'data-md-color' => 'error'])
|
||||
->and(buttonAttributes('<x-button label="Take down" caution />'))->toMatchArray(['data-md-variant' => 'filled', 'data-md-color' => 'warning'])
|
||||
->and(buttonAttributes('<x-button label="Go" variant="outlined" tone="success" />'))->toMatchArray(['data-md-color' => 'success']);
|
||||
});
|
||||
|
||||
it('falls back to the defaults for values it does not know', function () {
|
||||
expect(buttonClasses('<x-button label="Go" variant="glass" color="purple" size="huge" />'))
|
||||
->toContain('text-primary')
|
||||
->toContain('h-10 gap-2 px-4 type-label-lg');
|
||||
expect(buttonAttributes('<x-button label="Go" variant="glass" color="purple" size="huge" shape="blob" />'))
|
||||
->toMatchArray(['data-md-variant' => 'text', 'data-md-color' => 'primary', 'data-md-size' => 'sm', 'data-md-shape' => 'round']);
|
||||
});
|
||||
|
||||
it('sizes a label button on Expressive\'s scale', function (string $size, string $classes, string $icon) {
|
||||
it('sizes a label button on Expressive\'s scale', function (string $size, array $geometry, string $type, int $icon) {
|
||||
$html = (string) $this->blade("<x-button label=\"Upload\" icon=\"upload\" size=\"{$size}\" />");
|
||||
|
||||
expect($html)->toContain($classes)->toContain($icon);
|
||||
expect($html)->toContain("data-md-size=\"{$size}\"")->toContain("--md-icon-size: {$icon}px")
|
||||
->and(buttonCss()->declarations("[data-md-button][data-md-size='{$size}']"))->toMatchArray($geometry)
|
||||
->and(buttonCss()->declarations(in_array($size, ['xs', 'sm'], true)
|
||||
? "[data-md-button]:not([data-md-icon-button]):is([data-md-size='xs'], [data-md-size='sm'])"
|
||||
: "[data-md-button]:not([data-md-icon-button])[data-md-size='{$size}']"))->toMatchArray(['font' => "var(--md-sys-typescale-{$type})"]);
|
||||
})->with([
|
||||
'xs' => ['xs', 'h-8 gap-2 px-4 type-label-lg', 'size-5'],
|
||||
'sm' => ['sm', 'h-10 gap-2 px-4 type-label-lg', 'size-5'],
|
||||
'md' => ['md', 'h-14 gap-2 px-6 type-title-md', 'size-6'],
|
||||
'lg' => ['lg', 'h-24 gap-3 px-12 type-headline-sm', 'size-8'],
|
||||
'xl' => ['xl', 'h-34 gap-4 px-16 type-headline-lg', 'size-10'],
|
||||
'xs' => ['xs', ['--md-button-height' => '32px', '--md-button-padding' => 'var(--md-sys-measurement-space200)', '--md-button-gap' => 'var(--md-sys-measurement-space100)', '--md-button-icon' => '20px'], 'label-lg', 20],
|
||||
'sm' => ['sm', ['--md-button-height' => '40px', '--md-button-padding' => 'var(--md-sys-measurement-space200)', '--md-button-gap' => 'var(--md-sys-measurement-space100)', '--md-button-icon' => '20px'], 'label-lg', 20],
|
||||
'md' => ['md', ['--md-button-height' => '56px', '--md-button-padding' => 'var(--md-sys-measurement-space300)', '--md-button-gap' => 'var(--md-sys-measurement-space100)', '--md-button-icon' => '24px'], 'title-md', 24],
|
||||
'lg' => ['lg', ['--md-button-height' => '96px', '--md-button-padding' => 'var(--md-sys-measurement-space600)', '--md-button-gap' => '12px', '--md-button-icon' => '32px'], 'headline-sm', 32],
|
||||
'xl' => ['xl', ['--md-button-height' => '136px', '--md-button-padding' => 'var(--md-sys-measurement-space800)', '--md-button-gap' => 'var(--md-sys-measurement-space200)', '--md-button-icon' => '40px'], 'headline-lg', 40],
|
||||
]);
|
||||
|
||||
it('rounds by default, squares on request, and squares off further while pressed', function () {
|
||||
expect(buttonClasses('<x-button label="Go" size="md" />'))->toContain('rounded-corner-full')->toContain('active:rounded-corner-md')
|
||||
->and(buttonClasses('<x-button label="Go" size="md" shape="square" />'))->toContain('rounded-corner-lg')
|
||||
->and(buttonClasses('<x-button label="Go" size="xl" shape="square" />'))->toContain('rounded-corner-xl')->toContain('active:rounded-corner-lg');
|
||||
it('rounds by default, squares on request, and squares off further while pressed — without specificity', function () {
|
||||
expect(buttonAttributes('<x-button label="Go" size="md" shape="square" />'))->toMatchArray(['data-md-shape' => 'square'])
|
||||
->and(buttonCss()->declarations(':where([data-md-button])'))->toBe(['border-radius' => 'var(--md-sys-shape-corner-full)'])
|
||||
->and(buttonCss()->declarations(":where([data-md-button]):where([data-md-shape='square'], [data-md-selected='true'])"))->toBe(['border-radius' => 'var(--md-button-square)'])
|
||||
->and(buttonCss()->declarations(':where([data-md-button]):where(:active)'))->toBe(['border-radius' => 'var(--md-button-pressed)'])
|
||||
->and(buttonCss()->declarations("[data-md-button][data-md-size='md']"))->toMatchArray(['--md-button-square' => 'var(--md-sys-shape-corner-lg)', '--md-button-pressed' => 'var(--md-sys-shape-corner-md)'])
|
||||
->and(buttonCss()->declarations("[data-md-button][data-md-size='xl']"))->toMatchArray(['--md-button-square' => 'var(--md-sys-shape-corner-xl)', '--md-button-pressed' => 'var(--md-sys-shape-corner-lg)']);
|
||||
});
|
||||
|
||||
it('reaches a 48px touch target below the medium size', function () {
|
||||
expect(buttonClasses('<x-button label="Go" size="sm" />'))->toContain('touch-target')
|
||||
->and(buttonClasses('<x-button label="Go" size="md" />'))->not->toContain('touch-target');
|
||||
expect(buttonCss()->declarations("[data-md-button]:is([data-md-size='xs'], [data-md-size='sm'])::after"))->toMatchArray([
|
||||
'content' => "''",
|
||||
'min-width' => 'var(--md-sys-measurement-space600)',
|
||||
'min-height' => 'var(--md-sys-measurement-space600)',
|
||||
])
|
||||
->and(buttonCss()->has("[data-md-button]:is([data-md-size='md'])::after"))->toBeFalse();
|
||||
});
|
||||
|
||||
it('draws M3\'s state layer and focus ring on the button itself', function () {
|
||||
$css = buttonCss();
|
||||
|
||||
expect($css->declarations('[data-md-button]::before'))->toMatchArray(['background-color' => 'currentColor', 'opacity' => '0', 'border-radius' => 'inherit'])
|
||||
->and($css->declarations('[data-md-button]:hover::before', ['@media (hover: hover)']))->toBe(['opacity' => 'var(--md-sys-state-hover-state-layer-opacity)'])
|
||||
->and($css->declarations('[data-md-button]:focus-visible::before'))->toBe(['opacity' => 'var(--md-sys-state-focus-state-layer-opacity)'])
|
||||
->and($css->declarations('[data-md-button]:active::before'))->toBe(['opacity' => 'var(--md-sys-state-pressed-state-layer-opacity)'])
|
||||
->and($css->declarations('[data-md-button]:focus-visible'))->toBe(['outline' => '3px solid var(--md-sys-color-secondary)', 'outline-offset' => '2px']);
|
||||
});
|
||||
|
||||
it('moves shape on the spatial spring and colour on the effects spring', function () {
|
||||
$declarations = buttonCss()->declarations('[data-md-button]');
|
||||
|
||||
expect($declarations['transition-property'])->toBe('border-radius, padding, margin, bottom, background-color, color, box-shadow')
|
||||
->and($declarations['transition-timing-function'])->toBe('var(--md-sys-motion-spatial-fast), var(--md-sys-motion-spatial-fast), var(--md-sys-motion-spatial-fast), var(--md-sys-motion-spatial-fast), var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast)');
|
||||
});
|
||||
|
||||
it('fills a default icon button\'s glyph, and draws a 20px one from the 20px cut', function () {
|
||||
@@ -87,31 +154,39 @@ it('fills a default icon button\'s glyph, and draws a 20px one from the 20px cut
|
||||
it('is an icon button named by its tooltip when it has no label', function () {
|
||||
$html = (string) $this->blade('<x-button icon="close" tooltip="Close menu" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('aria-label="Close menu"')
|
||||
->toContain('size-10')
|
||||
->toContain('text-on-surface-variant')
|
||||
->toContain('popover="manual"')
|
||||
->toContain('aria-hidden="true"');
|
||||
expect(buttonAttributes('<x-button icon="close" tooltip="Close menu" />'))
|
||||
->toMatchArray(['data-md-icon-button' => 'data-md-icon-button', 'data-md-width' => 'default', 'aria-label' => 'Close menu'])
|
||||
->and($html)->toContain('--md-icon-size: 24px')->toContain('popover="manual"')->toContain('aria-hidden="true"')
|
||||
// M3's standard icon button: on-surface-variant, unless selected.
|
||||
->and(buttonCss()->declarations("[data-md-button][data-md-variant='text'][data-md-icon-button]:not([data-md-selected='true'])"))->toBe(['--md-button-label' => 'var(--md-button-quiet)']);
|
||||
});
|
||||
|
||||
it('sizes an icon button by width', function () {
|
||||
expect(buttonClasses('<x-button icon="share" label="" width="wide" aria-label="Share" />'))->toContain('h-10 w-13')
|
||||
->and(buttonClasses('<x-button icon="share" size="xl" width="narrow" aria-label="Share" />'))->toContain('h-34 w-26');
|
||||
expect(buttonAttributes('<x-button icon="share" label="" width="wide" aria-label="Share" />'))->toMatchArray(['data-md-width' => 'wide'])
|
||||
->and(buttonAttributes('<x-button icon="share" size="xl" width="narrow" aria-label="Share" />'))->toMatchArray(['data-md-width' => 'narrow', 'data-md-size' => 'xl'])
|
||||
->and(buttonAttributes('<x-button label="Share" width="wide" />'))->not->toHaveKey('data-md-width')
|
||||
->and(buttonCss()->declarations("[data-md-button][data-md-size='sm']"))->toMatchArray(['--md-button-narrow' => '32px', '--md-button-default' => '40px', '--md-button-wide' => '52px'])
|
||||
->and(buttonCss()->declarations("[data-md-button][data-md-size='xl']"))->toMatchArray(['--md-button-narrow' => '104px', '--md-button-default' => '136px', '--md-button-wide' => '184px'])
|
||||
->and(buttonCss()->declarations("[data-md-button][data-md-icon-button][data-md-width='wide']"))->toBe(['--md-button-width' => 'var(--md-button-wide)']);
|
||||
});
|
||||
|
||||
it('toggles with aria-pressed and M3\'s selected colours and shape', function () {
|
||||
expect((string) $this->blade('<x-button label="Bold" variant="filled" :selected="true" />'))->toContain('aria-pressed="true"')
|
||||
->and(buttonClasses('<x-button label="Bold" variant="filled" :selected="true" />'))->toContain('bg-primary')->toContain('rounded-corner-md')
|
||||
->and(buttonClasses('<x-button label="Bold" variant="filled" :selected="false" />'))->toContain('bg-surface-container text-on-surface-variant')->toContain('rounded-corner-full')
|
||||
->and(buttonClasses('<x-button label="Bold" variant="tonal" :selected="true" />'))->toContain('bg-secondary text-on-secondary')
|
||||
->and(buttonClasses('<x-button label="Bold" variant="outlined" :selected="true" />'))->toContain('bg-inverse-surface text-inverse-on-surface');
|
||||
$css = buttonCss();
|
||||
|
||||
expect(buttonAttributes('<x-button label="Bold" variant="filled" :selected="true" />'))->toMatchArray(['aria-pressed' => 'true', 'data-md-selected' => 'true'])
|
||||
->and(buttonAttributes('<x-button label="Bold" variant="filled" :selected="false" />'))->toMatchArray(['aria-pressed' => 'false', 'data-md-selected' => 'false'])
|
||||
->and(buttonAttributes('<x-button label="Bold" variant="filled" />'))->not->toHaveKey('data-md-selected')
|
||||
->and($css->declarations("[data-md-button][data-md-variant='filled'][data-md-selected='false']"))->toBe(['--md-button-container' => 'var(--md-sys-color-surface-container)', '--md-button-label' => 'var(--md-sys-color-on-surface-variant)'])
|
||||
->and($css->declarations("[data-md-button][data-md-variant='tonal'][data-md-selected='true']"))->toBe(['--md-button-container' => 'var(--md-button-tone-selected)', '--md-button-label' => 'var(--md-button-on-tone-selected)'])
|
||||
->and($css->declarations("[data-md-button][data-md-variant='outlined'][data-md-selected='true']"))->toBe(['--md-button-container' => 'var(--md-sys-color-inverse-surface)', '--md-button-label' => 'var(--md-sys-color-inverse-on-surface)', '--md-button-outline' => 'transparent'])
|
||||
// Text buttons are not toggles in M3: a selected one takes the tonal container.
|
||||
->and($css->declarations("[data-md-button][data-md-variant='text']:not([data-md-icon-button])[data-md-selected='true']"))->toBe(['--md-button-container' => 'var(--md-button-tone)', '--md-button-label' => 'var(--md-button-on-tone)']);
|
||||
});
|
||||
|
||||
it('squares a selected round icon button and rounds a selected square one', function () {
|
||||
expect(buttonClasses('<x-button icon="favorite" aria-label="Like" variant="tonal" :selected="true" />'))->toContain('rounded-corner-md')
|
||||
->and(buttonClasses('<x-button icon="favorite" aria-label="Like" variant="tonal" shape="square" :selected="true" />'))->toContain('rounded-corner-full')
|
||||
->and((string) $this->blade('<x-button icon="favorite" aria-label="Like" :selected="true" />'))->toContain('text-primary');
|
||||
expect(buttonCss()->declarations(":where([data-md-button]):where([data-md-icon-button][data-md-shape='square'][data-md-selected='true'])"))->toBe(['border-radius' => 'var(--md-sys-shape-corner-full)'])
|
||||
->and(buttonAttributes('<x-button icon="favorite" aria-label="Like" variant="tonal" shape="square" :selected="true" />'))
|
||||
->toMatchArray(['data-md-icon-button' => 'data-md-icon-button', 'data-md-shape' => 'square', 'data-md-selected' => 'true']);
|
||||
});
|
||||
|
||||
it('navigates inside the app, and leaves it for an external link', function () {
|
||||
@@ -128,21 +203,31 @@ it('navigates inside the app, and leaves it for an external link', function () {
|
||||
});
|
||||
|
||||
it('disables a button, and a link as far as a link can be', function () {
|
||||
$this->blade('<x-button label="Save" variant="filled" disabled />')
|
||||
->assertSee('disabled="disabled"', false)
|
||||
->assertSee('disabled:bg-on-surface/10', false);
|
||||
|
||||
$this->blade('<x-button label="Shares" link="/admin/shares" disabled />')
|
||||
->assertSee('aria-disabled="true"', false)
|
||||
->assertSee('tabindex="-1"', false);
|
||||
expect(buttonAttributes('<x-button label="Save" variant="filled" disabled />'))->toMatchArray(['disabled' => 'disabled'])
|
||||
->and(buttonAttributes('<x-button label="Shares" link="/admin/shares" disabled />'))->toMatchArray(['aria-disabled' => 'true', 'tabindex' => '-1'])
|
||||
->and(buttonCss()->declarations("[data-md-button]:is(:disabled, [aria-disabled='true'])"))->toBe([
|
||||
'box-shadow' => 'none',
|
||||
'color' => 'color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent)',
|
||||
])
|
||||
->and(buttonCss()->declarations("[data-md-button]:is([data-md-variant='filled'], [data-md-variant='tonal'], [data-md-variant='elevated'], [data-md-variant='outlined'][data-md-selected='true']):is(:disabled, [aria-disabled='true'])"))
|
||||
->toBe(['background-color' => 'color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent)'])
|
||||
->and(buttonCss()->declarations("[data-md-button]:is(:disabled, [aria-disabled='true'])::before"))->toBe(['display' => 'none'])
|
||||
->and(buttonCss()->declarations("[data-md-button][aria-disabled='true']"))->toBe(['pointer-events' => 'none']);
|
||||
});
|
||||
|
||||
it('shows the loading indicator while its own action runs, in the button\'s own ink', function () {
|
||||
$this->blade('<x-button label="Save" wire:click="save" spinner />')
|
||||
->assertSee('wire:loading.attr="disabled"', false)
|
||||
->assertSee('wire:target="save"', false)
|
||||
->assertSee('size-5 text-current', false)
|
||||
->assertDontSee('text-primary"', false);
|
||||
$html = (string) $this->blade('<x-button label="Save" wire:click="save" spinner />');
|
||||
|
||||
expect($html)
|
||||
->toContain('wire:loading.attr="disabled"')
|
||||
->toContain('wire:target="save"')
|
||||
->toMatch('/<span wire:loading.flex wire:target="save" data-md-button-spinner>\s*<span data-md-loading="data-md-loading" aria-hidden="true">/')
|
||||
->toMatch('/<span data-md-button-icon\s+wire:loading.remove wire:target="save"\s*>|<span data-md-button-label/')
|
||||
->and(buttonCss()->declarations('[data-md-button-spinner] > [data-md-loading]'))->toBe([
|
||||
'inline-size' => 'var(--md-button-icon)',
|
||||
'block-size' => 'var(--md-button-icon)',
|
||||
'color' => 'currentColor',
|
||||
]);
|
||||
|
||||
$this->blade('<x-button label="Save" wire:click="save" spinner="upload" />')
|
||||
->assertSee('wire:target="upload"', false);
|
||||
@@ -150,7 +235,9 @@ it('shows the loading indicator while its own action runs, in the button\'s own
|
||||
|
||||
it('hides a responsive label below expanded', function () {
|
||||
$this->blade('<x-button label="New share" icon="add" responsive />')
|
||||
->assertSee('<span class="max-expanded:hidden">New share</span>', false);
|
||||
->assertSee('<span data-md-button-label data-md-responsive >New share</span>', false);
|
||||
|
||||
expect(buttonCss()->declarations('[data-md-button] > [data-md-responsive]', ['@media (width < 840px)']))->toBe(['display' => 'none']);
|
||||
});
|
||||
|
||||
it('anchors its tooltip to itself', function () {
|
||||
@@ -166,11 +253,21 @@ it('anchors its tooltip to itself', function () {
|
||||
|
||||
it('is a FAB on a compact window and a filled button from medium, in one element', function () {
|
||||
$html = (string) $this->blade('<x-button label="New share" icon="add" fab />');
|
||||
$fab = buttonCss()->declarations('[data-md-button][data-md-compact-fab]', ['@media (width < 600px)']);
|
||||
|
||||
expect(substr_count($html, '<button'))->toBe(1)
|
||||
->and($html)->toContain('max-medium:fixed')->toContain('max-medium:bg-primary-container')->toContain('bg-primary text-on-primary')
|
||||
->and(buttonAttributes('<x-button label="New share" icon="add" fab />'))->toMatchArray(['data-md-compact-fab' => 'data-md-compact-fab', 'data-md-variant' => 'filled'])
|
||||
->and($fab)->toMatchArray([
|
||||
'position' => 'fixed',
|
||||
'inset-inline-end' => 'var(--md-sys-measurement-space200)',
|
||||
'--md-button-height' => '56px',
|
||||
'border-radius' => 'var(--md-sys-shape-corner-lg)',
|
||||
'background-color' => 'var(--md-sys-color-primary-container)',
|
||||
'color' => 'var(--md-sys-color-on-primary-container)',
|
||||
'box-shadow' => 'var(--md-sys-elevation-3)',
|
||||
])
|
||||
// M3 puts a snackbar above a FAB, never over one: the host publishes its height.
|
||||
->toContain('max-medium:bottom-[calc(var(--material-bottom-bar,0px)+var(--material-snackbar-height,0px)+1rem)]');
|
||||
->and($fab['bottom'])->toBe('calc(var(--material-bottom-bar, 0px) + var(--material-snackbar-height, 0px) + var(--md-sys-measurement-space200))');
|
||||
});
|
||||
|
||||
it('submits a form when asked', function () {
|
||||
@@ -181,6 +278,13 @@ it('submits a form when asked', function () {
|
||||
it('keeps aria-pressed off a selected link, which is not a toggle', function () {
|
||||
expect((string) $this->blade('<x-button label="Plans" link="/plans" :selected="true" />'))
|
||||
->not->toContain('aria-pressed')
|
||||
->toContain('data-md-selected="true"')
|
||||
->and((string) $this->blade('<x-button label="Bold" :selected="true" />'))
|
||||
->toContain('aria-pressed="true"');
|
||||
});
|
||||
|
||||
it('lands a caller\'s class and style on the button untouched', function () {
|
||||
expect(buttonAttributes('<x-button label="Save" class="w-full" style="margin: 0" tooltip="Save" />'))
|
||||
->toMatchArray(['class' => 'w-full'])
|
||||
->and(buttonAttributes('<x-button label="Save" class="w-full" style="margin: 0" tooltip="Save" />')['style'])->toStartWith('anchor-name: --material-button-')->toEndWith('margin: 0;');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user