Land a caller's style with its class on field and picker roots
Plan step 38 review: the showcase kept two unlayered width classes because <x-input> and <x-datepicker> "don't forward style usefully". They did not: the Phase F rule is that a caller's class and style land on the component root, and the field family sent class to the field's root but style to the inner control, where a width sizes nothing. Input, password, textarea, select and file now pass style to the field root with class; datepicker and timepicker put it on their root; <x-group> dropped style entirely and <x-split-button> gave it to the leading button; <x-search> rendered it twice, on the root and the input. A textarea given a style also lost its autogrow rows, since its own style attribute came second and the browser ignores it. A dataset test renders each component with both and requires them on the root, once. 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
3fb0a404db
commit
cd47aa4b0f
@@ -28,8 +28,9 @@
|
||||
`min` and `max` (`Y-m-d` or a date object) disable the days outside them and keep the
|
||||
keyboard inside them. `label`, `hint`, `icon`, `variant` (`outlined`, `filled`) and `size`
|
||||
are the field's; `clearable` adds a button that empties it (a date, or both ends of a range)
|
||||
once it holds one; `name` adds hidden inputs carrying `Y-m-d` for a plain form post, and every
|
||||
other attribute (`required`, `disabled`, `readonly`) reaches the text field. Errors under
|
||||
once it holds one; `name` adds hidden inputs carrying `Y-m-d` for a plain form post. `class` and
|
||||
`style` land on the root; every other attribute (`required`, `disabled`, `readonly`) reaches
|
||||
the text field. Errors under
|
||||
the `wire:model` name replace the hint, and so does a typed date that cannot be read.
|
||||
|
||||
Month and weekday names, the first day of the week and the typed format come from `Intl` for
|
||||
@@ -150,7 +151,7 @@
|
||||
@endphp
|
||||
|
||||
<div
|
||||
{{ $attributes->only(['class', 'wire:key', 'x-model']) }}
|
||||
{{ $attributes->only(['class', 'style', 'wire:key', 'x-model']) }}
|
||||
x-data="materialDatepicker({
|
||||
@if ($model !== null) value: @entangle($attributes->wire('model')), @else value: @js($current), @endif
|
||||
...@js($config),
|
||||
@@ -172,7 +173,7 @@
|
||||
x-bind:data-md-invalid="(fieldError !== '' || {{ $invalid ? 'true' : 'false' }}) ? '' : null"
|
||||
>
|
||||
<input
|
||||
{{ $attributes->whereDoesntStartWith('wire:model')->except(['class', 'id', 'wire:key', 'x-model', 'name', 'value', 'placeholder', 'type']) }}
|
||||
{{ $attributes->whereDoesntStartWith('wire:model')->except(['class', 'style', 'id', 'wire:key', 'x-model', 'name', 'value', 'placeholder', 'type']) }}
|
||||
x-ref="input"
|
||||
id="{{ $id }}"
|
||||
type="text"
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
span the full width of a large screen (§ Text Fields, Behaviour); a width rule from the call site
|
||||
beats that, and `full` takes it off altogether.
|
||||
|
||||
`class` from the call site lands on the outermost element, never the control, so a margin or
|
||||
a width is safe here; `hint-class` lands on the hint. --}}
|
||||
`class` and `style` from the call site land on the outermost element, never the control, so a
|
||||
margin or a width is safe here; `hint-class` lands on the hint. --}}
|
||||
|
||||
@props([
|
||||
'id',
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
: [];
|
||||
@endphp
|
||||
|
||||
<x-livewire-material::field :$id :$label :$hint :$messages :$variant floated :class="$attributes->get('class')" data-md-file>
|
||||
<x-livewire-material::field :$id :$label :$hint :$messages :$variant floated :class="$attributes->get('class')" :style="$attributes->get('style')" data-md-file>
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'type']) }}
|
||||
{{ $attributes->except(['class', 'style', 'id', 'type']) }}
|
||||
type="file"
|
||||
id="{{ $id }}"
|
||||
@if ($messages !== []) aria-invalid="true" @endif
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
// Below `medium` a segment draws under 48px, so it needs the foundation's touch target too.
|
||||
$segmentNeedsTouchTarget = in_array($size, ['xs', 'sm'], true);
|
||||
|
||||
$root = $attributes->only(['class', 'wire:key'])->merge(array_filter([
|
||||
$root = $attributes->only(['class', 'style', 'wire:key'])->merge(array_filter([
|
||||
'data-md-group' => true,
|
||||
'data-md-size' => $size,
|
||||
'data-md-variant' => $variant,
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
field once it holds something; `copyable` one that copies its value and says so in a snackbar
|
||||
(a share link, a token). `counter` counts the characters used against `maxlength`, at the end of
|
||||
the supporting-text row; `full` lets the field span its pane past the 40rem M3 bounds it to
|
||||
from `medium`. Every other attribute reaches the `<input>`: `type`, `min`, `step`,
|
||||
`readonly`, `wire:model` and the rest. Errors are read from the bag under the `wire:model` name.
|
||||
from `medium`. `class` and `style` land on the field's root, which is what a width or a margin
|
||||
sizes; every other attribute reaches the `<input>`: `type`, `min`, `step`, `readonly`,
|
||||
`wire:model` and the rest. Errors are read from the bag under the `wire:model` name.
|
||||
|
||||
The placeholder is a single space when none is given, because the label can only tell an
|
||||
empty field from a filled one through `:placeholder-shown`. With a label, a real placeholder
|
||||
@@ -45,9 +46,9 @@
|
||||
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||||
@endphp
|
||||
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$prefix :$suffix :$size :$variant :$mono :counter="$max" :$full :class="$attributes->get('class')" :data-md-readonly="$attributes->get('readonly') ? '' : null" data-md-input>
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$prefix :$suffix :$size :$variant :$mono :counter="$max" :$full :class="$attributes->get('class')" :style="$attributes->get('style')" :data-md-readonly="$attributes->get('readonly') ? '' : null" data-md-input>
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'placeholder'])->merge(['type' => 'text']) }}
|
||||
{{ $attributes->except(['class', 'style', 'id', 'placeholder'])->merge(['type' => 'text']) }}
|
||||
id="{{ $id }}"
|
||||
placeholder="{{ $placeholder }}"
|
||||
@if ($messages !== []) aria-invalid="true" @endif
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
password manager still sees a password input on load. Its label says what pressing it will do —
|
||||
M3's own pattern for an interactive trailing icon ("Show password" / "Hide password") — and it
|
||||
carries no `aria-pressed`, which would say the state a second time and the other way round.
|
||||
`label`, `hint`, `icon`, `variant`, `size`; every other attribute reaches the `<input>`
|
||||
`label`, `hint`, `icon`, `variant`, `size`; `class` and `style` land on the field's root, every
|
||||
other attribute reaches the `<input>`
|
||||
(`autocomplete="current-password"`, `wire:model`). The field's root carries
|
||||
`data-md-password`; the chrome and the eye's icon button are the field's
|
||||
(resources/css/components/field.css, brought by resources/css/components/password.css). --}}
|
||||
@@ -27,9 +28,9 @@
|
||||
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||||
@endphp
|
||||
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant :class="$attributes->get('class')" x-data="{ shown: false }" data-md-password>
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant :class="$attributes->get('class')" :style="$attributes->get('style')" x-data="{ shown: false }" data-md-password>
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'type', 'placeholder']) }}
|
||||
{{ $attributes->except(['class', 'style', 'id', 'type', 'placeholder']) }}
|
||||
id="{{ $id }}"
|
||||
type="password"
|
||||
x-bind:type="shown ? 'text' : 'password'"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
The root renders `data-md-search` with `data-md-trigger`, and `data-md-open` and
|
||||
`data-md-full-screen` while they hold; the parts are `data-md-search-*`, drawn by
|
||||
resources/css/components/search.css. `class` and `wire:key` land on the root. --}}
|
||||
resources/css/components/search.css. `class`, `style` and `wire:key` land on the root. --}}
|
||||
|
||||
@props([
|
||||
'placeholder' => null,
|
||||
@@ -103,7 +103,7 @@
|
||||
x-bind:aria-expanded="open.toString()"
|
||||
>
|
||||
<input
|
||||
{{ $attributes->except(['class', 'wire:key', 'id', 'placeholder', 'type']) }}
|
||||
{{ $attributes->except(['class', 'style', 'wire:key', 'id', 'placeholder', 'type']) }}
|
||||
x-ref="input"
|
||||
id="{{ $id }}"
|
||||
type="search"
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
`options` as a list of `['id' => …, 'name' => …]` (`'disabled' => true` greys one out),
|
||||
`option-value` and `option-label` to read other keys, and a `placeholder` that becomes the
|
||||
first option, valued `placeholder-value` (empty by default). Or pass `<option>`s in the slot.
|
||||
`label`, `hint`, `icon`, `variant`, `size`. The field's root carries `data-md-select`; what a
|
||||
`label`, `hint`, `icon`, `variant`, `size`; `class` and `style` land on the field's root, every
|
||||
other attribute on the `<select>`. The field's root carries `data-md-select`; what a
|
||||
select changes in the field's chrome is resources/css/components/select.css. --}}
|
||||
|
||||
@props([
|
||||
@@ -36,9 +37,9 @@
|
||||
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||||
@endphp
|
||||
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant floated :class="$attributes->get('class')" data-md-select>
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant floated :class="$attributes->get('class')" :style="$attributes->get('style')" data-md-select>
|
||||
<select
|
||||
{{ $attributes->except(['class', 'id']) }}
|
||||
{{ $attributes->except(['class', 'style', 'id']) }}
|
||||
id="{{ $id }}"
|
||||
@if ($messages !== []) aria-invalid="true" @endif
|
||||
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<x-menu-item label="Download files one by one" wire:click="downloadEach" />
|
||||
</x-split-button>
|
||||
|
||||
Attributes (wire:click, spinner…) go to the leading button, a `class` to the pair; the slot is
|
||||
the menu. `variant` is `filled` (the default), `tonal`, `outlined` or `elevated` — a text split
|
||||
button does not exist in M3 — with `color` and `size` as on `<x-button>`. The halves sit 2px
|
||||
apart; the trailing one rounds fully and turns its chevron over while the menu is open.
|
||||
`menu-label` names the trailing button and the menu for screen readers.
|
||||
Attributes (wire:click, spinner…) go to the leading button, `class` and `style` to the pair;
|
||||
the slot is the menu. `variant` is `filled` (the default), `tonal`, `outlined` or `elevated` —
|
||||
a text split button does not exist in M3 — with `color` and `size` as on `<x-button>`. The
|
||||
halves sit 2px apart; the trailing one rounds fully and turns its chevron over while the menu
|
||||
is open. `menu-label` names the trailing button and the menu for screen readers.
|
||||
|
||||
Padding from SplitButton*Tokens (androidx Compose Material 3, Apache-2.0): the leading button
|
||||
keeps less room on its inner side at the two smallest sizes, and the trailing button is 48px
|
||||
@@ -37,9 +37,9 @@
|
||||
$menuLabel ??= __('More options');
|
||||
@endphp
|
||||
|
||||
<div data-md-split-button data-md-size="{{ $size }}" {{ $attributes->only('class') }}>
|
||||
<div data-md-split-button data-md-size="{{ $size }}" {{ $attributes->only(['class', 'style']) }}>
|
||||
<x-livewire-material::button
|
||||
{{ $attributes->except('class') }}
|
||||
{{ $attributes->except(['class', 'style']) }}
|
||||
:label="$label"
|
||||
:icon="$icon"
|
||||
:variant="$variant"
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
`:autogrow="false"` keeps it at `rows` and lets it be resized by hand, vertically only, so a
|
||||
form's column never moves. The label rests on the first line rather than in the middle of the
|
||||
box. `label`, `hint`, `variant`, `counter` for M3's character counter against `maxlength`, and
|
||||
`full` to span the pane past the 40rem M3 bounds a field to from `medium`; every other
|
||||
attribute reaches the `<textarea>`. The field's root carries `data-md-textarea`; what a
|
||||
textarea changes in the field's chrome is resources/css/components/textarea.css. --}}
|
||||
`full` to span the pane past the 40rem M3 bounds a field to from `medium`; `class` and `style`
|
||||
land on the field's root, every other attribute reaches the `<textarea>`. The field's root
|
||||
carries `data-md-textarea`; what a textarea changes in the field's chrome is
|
||||
resources/css/components/textarea.css. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
@@ -31,9 +32,9 @@
|
||||
$max = $counter ? $attributes->get('maxlength') : null;
|
||||
@endphp
|
||||
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$variant :counter="$max" :$full :class="$attributes->get('class')" :data-md-readonly="$attributes->get('readonly') ? '' : null" data-md-textarea>
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$variant :counter="$max" :$full :class="$attributes->get('class')" :style="$attributes->get('style')" :data-md-readonly="$attributes->get('readonly') ? '' : null" data-md-textarea>
|
||||
<textarea
|
||||
{{ $attributes->except(['class', 'id', 'placeholder']) }}
|
||||
{{ $attributes->except(['class', 'style', 'id', 'placeholder']) }}
|
||||
id="{{ $id }}"
|
||||
rows="{{ (int) $rows }}"
|
||||
placeholder="{{ $placeholder }}"
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
a `min` later than `max` spans midnight) grey out and skip what lies outside them and turn typed
|
||||
values outside them into errors — validate on the server as well. `clearable` adds a button that
|
||||
empties the field; `name` posts the value from a hidden input. `label`, `hint`, `icon`, `variant`
|
||||
and `size` are the field's; other attributes (`required`, `disabled`, `placeholder`) reach the
|
||||
field's input.
|
||||
and `size` are the field's; `class` and `style` land on the root, other attributes (`required`,
|
||||
`disabled`, `placeholder`) reach the field's input.
|
||||
|
||||
From androidx Compose Material 3 at commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326 (Apache-2.0):
|
||||
TimePickerTokens and TimeInputTokens — a surface-container-high dialog with an extra-large
|
||||
@@ -132,14 +132,14 @@
|
||||
'minute' => array_map(fn (int $index): array => ['value' => $index * 5, 'text' => $index === 0 ? '00' : $index * 5, 'index' => $index, 'inner' => false, 'allowed' => 'minuteAllowed('.($index * 5).')'], range(0, 11)),
|
||||
];
|
||||
|
||||
$inputAttributes = $attributes->whereDoesntStartWith(['wire:model', 'x-model'])->except(['class', 'id', 'wire:key', 'placeholder']);
|
||||
$inputAttributes = $attributes->whereDoesntStartWith(['wire:model', 'x-model'])->except(['class', 'style', 'id', 'wire:key', 'placeholder']);
|
||||
$disabled = (bool) $attributes->get('disabled');
|
||||
$described = $messages !== [] || filled($hint);
|
||||
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||||
@endphp
|
||||
|
||||
<div
|
||||
{{ $attributes->only(['class', 'wire:key', 'x-model']) }}
|
||||
{{ $attributes->only(['class', 'style', 'wire:key', 'x-model']) }}
|
||||
x-data="materialTimepicker(@if ($model !== null) @entangle($attributes->wire('model')) @else @js($current) @endif, @js($config))"
|
||||
@if ($model === null) x-modelable="value" @endif
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user