diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 7e234121..3112edb1 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -244,7 +244,7 @@ M3's plain tooltip, standalone around any trigger: ` ``` -``: `trigger` slot (its first button or link becomes the menu button, and the menu hangs on that button — a `position: fixed` trigger such as `` carries it along, and a menu with no room flips to the other side, end or both), `label`, `position` (`bottom-start` default, `bottom-end`, `top-start`, `top-end`), `vibrant`. ``: `label`, `icon`, `icon-right`, `description`, `shortcut`, `link`, `external`, `selected` (makes it a `menuitemcheckbox`), `disabled`, `keep-open`. Choosing an item closes the menu unless `keep-open`. Keyboard: arrows, Home, End, a letter, Escape (focus returns to the trigger), Tab. +``: `trigger` slot (its first button or link becomes the menu button, and the menu hangs on that button — a `position: fixed` trigger such as `` carries it along, and a menu with no room flips to the other side, end or both), `label`, `position` (`bottom-start` default, `bottom-end`, `top-start`, `top-end`), `vibrant`. ``: `label`, `icon`, `icon-class` (classes for the leading icon; a colour there paints it, a selected item's too, but not a disabled one's — `icon-class="text-sport-run"`), `icon-right`, `description`, `shortcut`, `link`, `external`, `selected` (makes it a `menuitemcheckbox`), `disabled`, `keep-open`. Choosing an item closes the menu unless `keep-open`. Keyboard: arrows, Home, End, a letter, Escape (focus returns to the trigger), Tab. ### `` @@ -262,7 +262,7 @@ A choice between a few options as a connected button group of native radios (che ]" hint="Recipients lose access after that" /> ``` -Props: `label`, `hint`, `name` (required with `x-model`), `options`, `option-value` (`id`), `option-label` (`name`), `option-icon` (`icon`), `size`, `variant` (`tonal`, `filled`, `outlined`), `multiple`, `inline` (intrinsic width instead of sharing the row). A validation error for the bound property replaces the hint. +Props: `label`, `hint`, `hint-class` (classes for the hint, as on the fields; a colour there paints it), `name` (required with `x-model`), `options`, `option-value` (`id`), `option-label` (`name`), `option-icon` (`icon`), `size`, `variant` (`tonal`, `filled`, `outlined`), `multiple`, `inline` (intrinsic width instead of sharing the row). A validation error for the bound property replaces the hint. ### `` @@ -535,7 +535,7 @@ A one-column grid of fields with an `actions` slot at the foot (the slot takes i ### ``, ``, ``, ``, ``, `` -M3 text fields. `variant`: `outlined` or `filled`; without it, `config('livewire-material.fields.variant')` (`outlined`). All take `label`, `hint`, `variant`, and read their errors from the bag under the `wire:model` name, or the `name` in a plain form (`photos[]` → `photos`, `address[city]` → `address.city`); the error replaces the hint and sets `aria-invalid`. `class` lands on the field's outer element (margins, widths); every other attribute (`wire:model`, `type`, `required`, `readonly`, `autocomplete`) reaches the control. Never pass `placeholder` expecting it to show while a label rests in the field: it shows once the field has focus. +M3 text fields. `variant`: `outlined` or `filled`; without it, `config('livewire-material.fields.variant')` (`outlined`). All take `label`, `hint`, `variant` (and all but `` a `hint-class`, classes added to the hint: `hint-class="text-warning"`), and read their errors from the bag under the `wire:model` name, or the `name` in a plain form (`photos[]` → `photos`, `address[city]` → `address.city`); the error replaces the hint and sets `aria-invalid`. `class` lands on the field's outer element (margins, widths); every other attribute (`wire:model`, `type`, `required`, `readonly`, `autocomplete`) reaches the control. Never pass `placeholder` expecting it to show while a label rests in the field: it shows once the field has focus. - ``: `icon`, `icon-right`, `prefix`, `suffix`, `clearable`, `copyable` (copies the value, confirms with a snackbar), `size` (`sm` 40px, `xs` 32px — for unlabelled toolbar controls; give them `aria-label`), `mono`. - ``: a reveal button; `icon`, `size`. diff --git a/resources/views/components/group.blade.php b/resources/views/components/group.blade.php index 11d2848f..d5a19826 100644 --- a/resources/views/components/group.blade.php +++ b/resources/views/components/group.blade.php @@ -12,13 +12,19 @@ default), `filled` or `outlined`, as for toggle buttons. The segments share the row unless `inline`. An option with `'disabled' => true` greys its own segment. - ReStride's props, kept: `label`, `hint`, `name` (needed with `x-model`, which names no - property), `options`, `option-value`, `option-label`; plus `option-icon`, `size`, `variant`, - `multiple`, `inline`. A validation message for the bound property replaces the hint. --}} + ReStride's props, kept: `label`, `hint`, `hint-class`, `name` (needed with `x-model`, which + names no property), `options`, `option-value`, `option-label`; plus `option-icon`, `size`, + `variant`, `multiple`, `inline`. A validation message for the bound property replaces the hint. + + `hint-class` adds classes to the hint, as on ``: a colour there paints it + (`hint-class="text-warning"` for a hint that warns). The hint's own colour then carries no + specificity, as the field's does in the components layer, because which of two colour + utilities wins depends on the order Tailwind emits them. --}} @props([ 'label' => null, 'hint' => null, + 'hintClass' => null, 'name' => null, 'options' => [], 'optionValue' => 'id', @@ -46,6 +52,10 @@ 'xl' => 'h-34 gap-4 px-16 type-headline-lg', ][$size]; + $hintClasses = filled($hintClass) + ? \Illuminate\Support\Arr::toCssClasses(['mt-1 type-body-sm [:where(&)]:text-on-surface-variant', $hintClass]) + : 'mt-1 type-body-sm text-on-surface-variant'; + $iconSize = ['xs' => 'size-5', 'sm' => 'size-5', 'md' => 'size-6', 'lg' => 'size-8', 'xl' => 'size-10'][$size]; $colours = match ($variant) { @@ -94,6 +104,6 @@

{{ $message }}

@endforeach @elseif (filled($hint)) -

{{ $hint }}

+

{{ $hint }}

@endif diff --git a/resources/views/components/menu-item.blade.php b/resources/views/components/menu-item.blade.php index d7db6f2d..3c37ba77 100644 --- a/resources/views/components/menu-item.blade.php +++ b/resources/views/components/menu-item.blade.php @@ -1,11 +1,18 @@ {{-- One item in an ``: an action, a link, or a choice. - `label`, a leading `icon`, an `icon-right`, a `description` under the label and a - `shortcut` at the end (M3's trailing supporting text: "⌘C"). `link` makes it an anchor, with - `wire:navigate` unless `external` or `no-wire-navigate`. `selected` (true or false) makes it a - `menuitemcheckbox` with `aria-checked`; a selected item takes Expressive's selected shape and - tertiary-container. `disabled` keeps it in the list, out of reach. `keep-open` leaves the menu - open when it is activated — for a choice the person may want to change twice. + `label`, a leading `icon` (`icon-class` adds classes to it), an `icon-right`, a `description` + under the label and a `shortcut` at the end (M3's trailing supporting text: "⌘C"). `link` + makes it an anchor, with `wire:navigate` unless `external` or `no-wire-navigate`. `selected` + (true or false) makes it a `menuitemcheckbox` with `aria-checked`; a selected item takes + Expressive's selected shape and tertiary-container. `disabled` keeps it in the list, out of + reach. `keep-open` leaves the menu open when it is activated — for a choice the person may + want to change twice. + + `icon-class` is for an icon whose colour means something of its own, a sport's glyph in the + sport's colour (`icon-class="text-sport-run"`). A colour there paints the icon, a selected + item's too: the icon's own colour then carries no specificity, because which of two colour + utilities wins depends on the order Tailwind emits them. A disabled item's icon stays + disabled. 44px tall (SegmentedMenuTokens.Item), body-large label, 20px icons, 4px corners that open to 12px at the ends of the list. --}} @@ -13,6 +20,7 @@ @props([ 'label' => null, 'icon' => null, + 'iconClass' => null, 'iconRight' => null, 'description' => null, 'shortcut' => null, @@ -56,11 +64,18 @@ $selected === true => 'text-on-tertiary-container', default => 'text-on-surface-variant', }; + + $leadingIcon = match (true) { + blank($iconClass) => 'size-5 '.$iconInk, + $disabled => \Illuminate\Support\Arr::toCssClasses(['size-5', $iconClass, 'text-on-surface/38!']), + $selected === true => \Illuminate\Support\Arr::toCssClasses(['size-5 [:where(&)]:text-on-tertiary-container', $iconClass]), + default => \Illuminate\Support\Arr::toCssClasses(['size-5 [:where(&)]:text-on-surface-variant', $iconClass]), + }; @endphp <{{ $tag }} {{ $attributes }}> @if ($icon) - + @endif diff --git a/resources/views/showcase/sections/buttons.blade.php b/resources/views/showcase/sections/buttons.blade.php index 6cbaf1af..6cda1d36 100644 --- a/resources/views/showcase/sections/buttons.blade.php +++ b/resources/views/showcase/sections/buttons.blade.php @@ -84,7 +84,7 @@ ['id' => 'system', 'name' => 'System', 'icon' => 'computer'], ]" /> - BLADE, + 'Icons in their own colour' => <<<'BLADE' + + + + + + + + + + + BLADE, ]; @endphp diff --git a/tests/Browser/ActionsTest.php b/tests/Browser/ActionsTest.php index 05791424..c5c577f7 100644 --- a/tests/Browser/ActionsTest.php +++ b/tests/Browser/ActionsTest.php @@ -221,3 +221,33 @@ it('hangs a menu on its menu button, even when the button is fixed to a corner o ->assertScript("(() => { const names = getComputedStyle(document.querySelector('[data-test=\"more\"]')).getPropertyValue('anchor-name'); return names.includes('--material-button-') && names.includes('--material-menu-'); })()") ->assertScript('(({ control, menu }) => menu.top >= control.bottom && menu.top - control.bottom <= 16 && Math.abs(menu.left - control.left) <= 16)('.menuAgainst('more', 'Share actions').')'); }); + +it('paints a group\'s hint and a menu item\'s icon in the colour their classes name', function () { + Route::middleware('web')->get('/colour-class-probe', fn () => Blade::render(<<<'BLADE' + + + + + @vite(config('livewire-material.showcase.vite')) + + + Reference +
+ +
+ + + + + + BLADE)); + + $ink = fn (string $element): string => "getComputedStyle({$element}).color"; + $error = $ink("document.querySelector('#error-ink')"); + + visit('/colour-class-probe')->waitForEvent('networkidle') + ->assertScript($ink("document.querySelector('#terrain p')")." === {$error}") + ->assertScript($ink("document.querySelector('#run svg')")." === {$error}") + ->assertScript($ink("document.querySelector('#chosen svg')")." === {$error}") + ->assertScript($ink("document.querySelector('#off svg')")." !== {$error}"); +}); diff --git a/tests/Feature/Components/ButtonGroupTest.php b/tests/Feature/Components/ButtonGroupTest.php index 5f7f26b4..366b04a9 100644 --- a/tests/Feature/Components/ButtonGroupTest.php +++ b/tests/Feature/Components/ButtonGroupTest.php @@ -83,3 +83,17 @@ it('binds to a Livewire property and shows its validation message', function () ->assertSee('Pick light.') ->assertDontSee('How it looks'); }); + +it('adds hint-class to the hint, which a validation message still replaces', function () { + $options = [['id' => 'flat', 'name' => 'Flat'], ['id' => 'hilly', 'name' => 'Hilly']]; + + expect((string) $this->blade('', ['options' => $options])) + ->toContain('

No elevation data here

') + ->and((string) $this->blade('', ['options' => $options])) + ->toContain('

No elevation data here

'); + + expect((string) $this->withViewErrors(['terrain' => 'Pick a terrain.'])->blade('', ['options' => $options])) + ->toContain('

Pick a terrain.

') + ->not->toContain('No elevation data here') + ->not->toContain('text-warning'); +}); diff --git a/tests/Feature/Components/MenuTest.php b/tests/Feature/Components/MenuTest.php index e6501b21..07d8ae82 100644 --- a/tests/Feature/Components/MenuTest.php +++ b/tests/Feature/Components/MenuTest.php @@ -69,3 +69,18 @@ it('separates and labels groups', function () { $this->blade('') ->assertSee('role="group" aria-label="Sort by"', false); }); + +it('adds icon-class to the leading icon, over its own colour but not over disabled', function () { + $leading = fn (string $html): string => preg_match('/]*class="([^"]*)"/', $html, $icon) ? $icon[1] : ''; + + expect($leading((string) $this->blade(''))) + ->toBe('shrink-0 size-5 [:where(&)]:text-on-surface-variant text-sport-run') + ->and($leading((string) $this->blade(''))) + ->toBe('shrink-0 size-5 [:where(&)]:text-on-tertiary-container text-sport-run') + ->and($leading((string) $this->blade(''))) + ->toBe('shrink-0 size-5 text-sport-run text-on-surface/38!') + ->and($leading((string) $this->blade(''))) + ->toBe('shrink-0 size-5 text-on-surface-variant') + ->and((string) $this->blade('')) + ->not->toContain('text-sport-run'); +});