<x-field> renders data-md-field with its props and parts as data-md-* attributes (box, control, label, outline, support, counter, trailing buttons), and field.css moves into material.components on tokens: px geometry, spacing and state tokens, the 600px breakpoint (plan step 36). Its icons take a size prop, 24/20/16 by the field's size. Every control the field wraps now marks itself data-md-field-control, so the inputs, the pickers, choices, field.js, menu.css's select and listbox rules and timepicker.css follow the renamed hooks. Browser tests cover the error icon, the disabled field's hover, the counter and the width bound from 600px. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
91 lines
4.6 KiB
PHP
91 lines
4.6 KiB
PHP
{{-- A text field: M3's outlined or filled one (resources/views/components/field.blade.php).
|
|
|
|
`label`, `hint`, leading `icon`, trailing `icon-right`, `prefix` and `suffix` (text beside the
|
|
value, such as a currency or a unit), `variant`, `size` for unlabelled controls in toolbars,
|
|
and `mono` for a field that holds code. `clearable` adds a trailing button that empties the
|
|
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.
|
|
|
|
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
|
|
shows only while the field has focus — until then the label stands where it would be. --}}
|
|
|
|
@props([
|
|
'label' => null,
|
|
'hint' => null,
|
|
'hintClass' => null,
|
|
'icon' => null,
|
|
'iconRight' => null,
|
|
'prefix' => null,
|
|
'suffix' => null,
|
|
'clearable' => false,
|
|
'copyable' => false,
|
|
'counter' => false,
|
|
'full' => false,
|
|
'size' => 'md',
|
|
'variant' => null,
|
|
'mono' => false,
|
|
])
|
|
|
|
@php
|
|
$model = $attributes->whereStartsWith('wire:model')->first();
|
|
$placeholder = filled($attributes->get('placeholder')) ? $attributes->get('placeholder') : ' ';
|
|
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|'.$placeholder), 0, 12);
|
|
// A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
|
|
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
|
|
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
|
|
// M3's counter needs a maximum to count against: without `maxlength` there is nothing to show.
|
|
$max = $counter ? $attributes->get('maxlength') : null;
|
|
$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">
|
|
<input
|
|
{{ $attributes->except(['class', 'id', 'placeholder'])->merge(['type' => 'text']) }}
|
|
id="{{ $id }}"
|
|
placeholder="{{ $placeholder }}"
|
|
@if ($messages !== []) aria-invalid="true" @endif
|
|
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif
|
|
data-md-field-control
|
|
/>
|
|
|
|
@if ($clearable || $copyable || $iconRight)
|
|
<x-slot:trailing>
|
|
@if ($clearable)
|
|
<button
|
|
type="button"
|
|
x-data
|
|
x-on:click="const control = $el.closest('[data-md-field]').querySelector('[data-md-field-control]'); control.value = ''; control.dispatchEvent(new Event('input', { bubbles: true })); control.dispatchEvent(new Event('change', { bubbles: true })); control.focus();"
|
|
aria-label="{{ __('Clear') }}"
|
|
data-md-field-trailing
|
|
data-md-field-button
|
|
data-md-field-clear
|
|
>
|
|
<x-livewire-material::icon name="close" :size="$fieldIconSize" />
|
|
</button>
|
|
@endif
|
|
|
|
@if ($copyable)
|
|
<button
|
|
type="button"
|
|
x-data
|
|
x-on:click="navigator.clipboard.writeText($el.closest('[data-md-field]').querySelector('[data-md-field-control]').value).then(() => window.materialToast(@js(__('Copied to the clipboard')), { type: 'success' }))"
|
|
aria-label="{{ __('Copy') }}"
|
|
data-md-field-trailing
|
|
data-md-field-button
|
|
data-md-field-copy
|
|
>
|
|
<x-livewire-material::icon name="content_copy" :size="$fieldIconSize" />
|
|
</button>
|
|
@endif
|
|
|
|
@if ($iconRight)
|
|
<x-livewire-material::icon :name="$iconRight" :size="$fieldIconSize" data-md-field-trailing />
|
|
@endif
|
|
</x-slot:trailing>
|
|
@endif
|
|
</x-livewire-material::field>
|