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
|
`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`
|
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)
|
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
|
once it holds one; `name` adds hidden inputs carrying `Y-m-d` for a plain form post. `class` and
|
||||||
other attribute (`required`, `disabled`, `readonly`) reaches the text field. Errors under
|
`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.
|
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
|
Month and weekday names, the first day of the week and the typed format come from `Intl` for
|
||||||
@@ -150,7 +151,7 @@
|
|||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div
|
<div
|
||||||
{{ $attributes->only(['class', 'wire:key', 'x-model']) }}
|
{{ $attributes->only(['class', 'style', 'wire:key', 'x-model']) }}
|
||||||
x-data="materialDatepicker({
|
x-data="materialDatepicker({
|
||||||
@if ($model !== null) value: @entangle($attributes->wire('model')), @else value: @js($current), @endif
|
@if ($model !== null) value: @entangle($attributes->wire('model')), @else value: @js($current), @endif
|
||||||
...@js($config),
|
...@js($config),
|
||||||
@@ -172,7 +173,7 @@
|
|||||||
x-bind:data-md-invalid="(fieldError !== '' || {{ $invalid ? 'true' : 'false' }}) ? '' : null"
|
x-bind:data-md-invalid="(fieldError !== '' || {{ $invalid ? 'true' : 'false' }}) ? '' : null"
|
||||||
>
|
>
|
||||||
<input
|
<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"
|
x-ref="input"
|
||||||
id="{{ $id }}"
|
id="{{ $id }}"
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -27,8 +27,8 @@
|
|||||||
span the full width of a large screen (§ Text Fields, Behaviour); a width rule from the call site
|
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.
|
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
|
`class` and `style` from the call site land on the outermost element, never the control, so a
|
||||||
a width is safe here; `hint-class` lands on the hint. --}}
|
margin or a width is safe here; `hint-class` lands on the hint. --}}
|
||||||
|
|
||||||
@props([
|
@props([
|
||||||
'id',
|
'id',
|
||||||
|
|||||||
@@ -29,9 +29,9 @@
|
|||||||
: [];
|
: [];
|
||||||
@endphp
|
@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
|
<input
|
||||||
{{ $attributes->except(['class', 'id', 'type']) }}
|
{{ $attributes->except(['class', 'style', 'id', 'type']) }}
|
||||||
type="file"
|
type="file"
|
||||||
id="{{ $id }}"
|
id="{{ $id }}"
|
||||||
@if ($messages !== []) aria-invalid="true" @endif
|
@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.
|
// Below `medium` a segment draws under 48px, so it needs the foundation's touch target too.
|
||||||
$segmentNeedsTouchTarget = in_array($size, ['xs', 'sm'], true);
|
$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-group' => true,
|
||||||
'data-md-size' => $size,
|
'data-md-size' => $size,
|
||||||
'data-md-variant' => $variant,
|
'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
|
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
|
(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
|
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`,
|
from `medium`. `class` and `style` land on the field's root, which is what a width or a margin
|
||||||
`readonly`, `wire:model` and the rest. Errors are read from the bag under the `wire:model` name.
|
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
|
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
|
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;
|
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||||||
@endphp
|
@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
|
<input
|
||||||
{{ $attributes->except(['class', 'id', 'placeholder'])->merge(['type' => 'text']) }}
|
{{ $attributes->except(['class', 'style', 'id', 'placeholder'])->merge(['type' => 'text']) }}
|
||||||
id="{{ $id }}"
|
id="{{ $id }}"
|
||||||
placeholder="{{ $placeholder }}"
|
placeholder="{{ $placeholder }}"
|
||||||
@if ($messages !== []) aria-invalid="true" @endif
|
@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 —
|
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
|
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.
|
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
|
(`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
|
`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). --}}
|
(resources/css/components/field.css, brought by resources/css/components/password.css). --}}
|
||||||
@@ -27,9 +28,9 @@
|
|||||||
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||||||
@endphp
|
@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
|
<input
|
||||||
{{ $attributes->except(['class', 'id', 'type', 'placeholder']) }}
|
{{ $attributes->except(['class', 'style', 'id', 'type', 'placeholder']) }}
|
||||||
id="{{ $id }}"
|
id="{{ $id }}"
|
||||||
type="password"
|
type="password"
|
||||||
x-bind:type="shown ? 'text' : '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
|
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
|
`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([
|
@props([
|
||||||
'placeholder' => null,
|
'placeholder' => null,
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
x-bind:aria-expanded="open.toString()"
|
x-bind:aria-expanded="open.toString()"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
{{ $attributes->except(['class', 'wire:key', 'id', 'placeholder', 'type']) }}
|
{{ $attributes->except(['class', 'style', 'wire:key', 'id', 'placeholder', 'type']) }}
|
||||||
x-ref="input"
|
x-ref="input"
|
||||||
id="{{ $id }}"
|
id="{{ $id }}"
|
||||||
type="search"
|
type="search"
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
`options` as a list of `['id' => …, 'name' => …]` (`'disabled' => true` greys one out),
|
`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
|
`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.
|
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. --}}
|
select changes in the field's chrome is resources/css/components/select.css. --}}
|
||||||
|
|
||||||
@props([
|
@props([
|
||||||
@@ -36,9 +37,9 @@
|
|||||||
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||||||
@endphp
|
@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
|
<select
|
||||||
{{ $attributes->except(['class', 'id']) }}
|
{{ $attributes->except(['class', 'style', 'id']) }}
|
||||||
id="{{ $id }}"
|
id="{{ $id }}"
|
||||||
@if ($messages !== []) aria-invalid="true" @endif
|
@if ($messages !== []) aria-invalid="true" @endif
|
||||||
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @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-menu-item label="Download files one by one" wire:click="downloadEach" />
|
||||||
</x-split-button>
|
</x-split-button>
|
||||||
|
|
||||||
Attributes (wire:click, spinner…) go to the leading button, a `class` to the pair; the slot is
|
Attributes (wire:click, spinner…) go to the leading button, `class` and `style` to the pair;
|
||||||
the menu. `variant` is `filled` (the default), `tonal`, `outlined` or `elevated` — a text split
|
the slot is the menu. `variant` is `filled` (the default), `tonal`, `outlined` or `elevated` —
|
||||||
button does not exist in M3 — with `color` and `size` as on `<x-button>`. The halves sit 2px
|
a text split button does not exist in M3 — with `color` and `size` as on `<x-button>`. The
|
||||||
apart; the trailing one rounds fully and turns its chevron over while the menu is open.
|
halves sit 2px apart; the trailing one rounds fully and turns its chevron over while the menu
|
||||||
`menu-label` names the trailing button and the menu for screen readers.
|
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
|
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
|
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');
|
$menuLabel ??= __('More options');
|
||||||
@endphp
|
@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
|
<x-livewire-material::button
|
||||||
{{ $attributes->except('class') }}
|
{{ $attributes->except(['class', 'style']) }}
|
||||||
:label="$label"
|
:label="$label"
|
||||||
:icon="$icon"
|
:icon="$icon"
|
||||||
:variant="$variant"
|
:variant="$variant"
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
`:autogrow="false"` keeps it at `rows` and lets it be resized by hand, vertically only, so a
|
`: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
|
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
|
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
|
`full` to span the pane past the 40rem M3 bounds a field to from `medium`; `class` and `style`
|
||||||
attribute reaches the `<textarea>`. The field's root carries `data-md-textarea`; what a
|
land on the field's root, every other attribute reaches the `<textarea>`. The field's root
|
||||||
textarea changes in the field's chrome is resources/css/components/textarea.css. --}}
|
carries `data-md-textarea`; what a textarea changes in the field's chrome is
|
||||||
|
resources/css/components/textarea.css. --}}
|
||||||
|
|
||||||
@props([
|
@props([
|
||||||
'label' => null,
|
'label' => null,
|
||||||
@@ -31,9 +32,9 @@
|
|||||||
$max = $counter ? $attributes->get('maxlength') : null;
|
$max = $counter ? $attributes->get('maxlength') : null;
|
||||||
@endphp
|
@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
|
<textarea
|
||||||
{{ $attributes->except(['class', 'id', 'placeholder']) }}
|
{{ $attributes->except(['class', 'style', 'id', 'placeholder']) }}
|
||||||
id="{{ $id }}"
|
id="{{ $id }}"
|
||||||
rows="{{ (int) $rows }}"
|
rows="{{ (int) $rows }}"
|
||||||
placeholder="{{ $placeholder }}"
|
placeholder="{{ $placeholder }}"
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
a `min` later than `max` spans midnight) grey out and skip what lies outside them and turn typed
|
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
|
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`
|
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
|
and `size` are the field's; `class` and `style` land on the root, other attributes (`required`,
|
||||||
field's input.
|
`disabled`, `placeholder`) reach the field's input.
|
||||||
|
|
||||||
From androidx Compose Material 3 at commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326 (Apache-2.0):
|
From androidx Compose Material 3 at commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326 (Apache-2.0):
|
||||||
TimePickerTokens and TimeInputTokens — a surface-container-high dialog with an extra-large
|
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)),
|
'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');
|
$disabled = (bool) $attributes->get('disabled');
|
||||||
$described = $messages !== [] || filled($hint);
|
$described = $messages !== [] || filled($hint);
|
||||||
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div
|
<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))"
|
x-data="materialTimepicker(@if ($model !== null) @entangle($attributes->wire('model')) @else @js($current) @endif, @js($config))"
|
||||||
@if ($model === null) x-modelable="value" @endif
|
@if ($model === null) x-modelable="value" @endif
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A caller's `class` and `style` land on a component's root, untouched and once (the Phase F rule
|
||||||
|
* in docs/plans/material-3-alignment-brief.md): an application's width or margin sizes the whole
|
||||||
|
* component, whichever element its other attributes reach. The field family, the pickers and the
|
||||||
|
* components that split their attributes between a root and an inner control are the ones that
|
||||||
|
* can get it wrong, so each is rendered here with both.
|
||||||
|
*/
|
||||||
|
it('lands a caller\'s class and style on the root, and only there', function (string $blade) {
|
||||||
|
$html = (string) $this->blade($blade);
|
||||||
|
|
||||||
|
preg_match('/<([a-z]+)((?:[^>"\']|"[^"]*"|\'[^\']*\')*)>/', ltrim($html), $root);
|
||||||
|
|
||||||
|
expect($root[2] ?? '')
|
||||||
|
->toContain('class="caller-class')
|
||||||
|
->toContain('--caller-width: 192px')
|
||||||
|
->and(substr_count($html, 'caller-class'))->toBe(1, 'the class lands more than once')
|
||||||
|
->and(substr_count($html, '--caller-width'))->toBe(1, 'the style lands more than once');
|
||||||
|
})->with([
|
||||||
|
'input' => '<x-input label="Name" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'password' => '<x-password label="Password" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'textarea' => '<x-textarea label="Message" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'textarea, fixed height' => '<x-textarea label="Message" :autogrow="false" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'select' => '<x-select label="Expiry" :options="[]" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'file' => '<x-file label="Attachments" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'datepicker' => '<x-datepicker label="From" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'timepicker' => '<x-timepicker label="At" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'choices' => '<x-choices label="Days" :options="[]" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'search' => '<x-search placeholder="Search" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'group' => '<x-group label="Theme" name="theme" :options="[[\'id\' => 1, \'name\' => \'One\']]" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'split button' => '<x-split-button label="Save" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'checkbox' => '<x-checkbox label="Tick" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'radio' => '<x-radio label="Pick" name="pick" :options="[[\'id\' => 1, \'name\' => \'One\']]" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'toggle' => '<x-toggle label="On" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
'slider' => '<x-slider label="Volume" class="caller-class" style="--caller-width: 192px" />',
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('keeps a textarea\'s own row count beside a caller\'s style', function () {
|
||||||
|
$html = (string) $this->blade('<x-textarea label="Message" rows="2" max-rows="6" style="max-width: 320px" />');
|
||||||
|
|
||||||
|
// A second style attribute on the <textarea> would be ignored by the browser, rows and all.
|
||||||
|
preg_match('/<textarea\b(?:[^>"]|"[^"]*")*>/', $html, $control);
|
||||||
|
|
||||||
|
expect(substr_count($control[0] ?? '', 'style="'))->toBe(1)
|
||||||
|
->and($control[0] ?? '')->toMatch('/style="--field-rows: 2;\s*--field-max-rows: 6;"/');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user