<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
146 lines
6.3 KiB
PHP
146 lines
6.3 KiB
PHP
{{-- The chrome of M3's text fields, around whichever control is in the slot.
|
|
|
|
Not used at call sites: `<x-input>`, `<x-password>`, `<x-textarea>`, `<x-select>`, `<x-file>`
|
|
and the pickers wrap their control in it, so a form reads as one family. A custom control the
|
|
package does not have goes in the slot marked `data-md-field-control`. The states — the label
|
|
resting or floating, the notch, focus, disabled, required — are selectors in
|
|
resources/css/components/field.css, which explains the anatomy; the only state this file
|
|
decides is `data-md-invalid`, because only the server knows that.
|
|
|
|
`variant` is M3's two: `outlined` (a notched outline) and `filled` (a surface-container-highest
|
|
box with an indicator line); without it, `config('livewire-material.fields.variant')`. A field
|
|
with a label is a form field, M3's 56px. A field without one is a control in a toolbar or an
|
|
editor row, and is given `size="sm"` (40px, a button's height) or `size="xs"` (32px). The
|
|
error replaces the hint rather than stacking under it, as M3 has it, and an `error` icon joins it
|
|
at the end of the row so the state has two indicators and not only a colour — unless the caller
|
|
trails the field with something of its own, or the field is `xs` and has no room. The field's
|
|
icons are 24px, 20px in an `sm` field and 16px in an `xs` one.
|
|
|
|
`counter` is the maximum number of characters, and puts M3's character counter at the end of the
|
|
supporting-text row: `n/max`, counted from the control on every `input`, in the error colour once
|
|
the value is past the maximum
|
|
(docs/reference/m3/components-navigation-selection-inputs.md § Text Fields). It is said as
|
|
"Character count, 5/20", M3's own label, from a polite region a second after typing stops rather
|
|
than on every keystroke, which would talk over the typing.
|
|
|
|
From `medium` (600px) the field is no wider than 40rem, because M3 asks that a text field never
|
|
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. --}}
|
|
|
|
@props([
|
|
'id',
|
|
'label' => null,
|
|
'hint' => null,
|
|
'hintClass' => null,
|
|
'messages' => [],
|
|
'icon' => null,
|
|
'prefix' => null,
|
|
'suffix' => null,
|
|
'size' => 'md',
|
|
'variant' => null,
|
|
'floated' => false,
|
|
'mono' => false,
|
|
'counter' => null,
|
|
'full' => false,
|
|
])
|
|
|
|
@php
|
|
$variant = in_array($variant, ['outlined', 'filled'], true)
|
|
? $variant
|
|
: (config('livewire-material.fields.variant') === 'filled' ? 'filled' : 'outlined');
|
|
$density = in_array($size, ['sm', 'xs'], true) ? $size : 'md';
|
|
$iconSize = ['md' => 24, 'sm' => 20, 'xs' => 16][$density];
|
|
// M3 pairs the error colours with a trailing error icon so the state has two indicators. It
|
|
// gives way to whatever the caller trails the field with, and to `xs`, which has no room.
|
|
$errorIcon = $messages !== [] && ! isset($trailing) && $density !== 'xs';
|
|
$counter = filled($counter) && (int) $counter > 0 ? (int) $counter : null;
|
|
@endphp
|
|
|
|
<div
|
|
data-md-field
|
|
data-md-variant="{{ $variant }}"
|
|
data-md-size="{{ $density }}"
|
|
@if ($messages !== []) data-md-invalid @endif
|
|
@if ($floated) data-md-floated @endif
|
|
@if ($mono) data-md-mono @endif
|
|
@if ($full) data-md-full @endif
|
|
{{ $attributes }}
|
|
>
|
|
<div data-md-field-box>
|
|
@if ($icon)
|
|
<x-livewire-material::icon :name="$icon" :size="$iconSize" data-md-field-icon />
|
|
@endif
|
|
|
|
@if (filled($prefix))
|
|
<span data-md-field-affix>{{ $prefix }}</span>
|
|
@endif
|
|
|
|
{{ $slot }}
|
|
|
|
@if (filled($suffix))
|
|
<span data-md-field-affix>{{ $suffix }}</span>
|
|
@endif
|
|
|
|
{{ $trailing ?? '' }}
|
|
|
|
@if ($errorIcon)
|
|
<x-livewire-material::icon name="error" :size="$iconSize" data-md-field-trailing data-md-field-error :label="__('Error')" />
|
|
@endif
|
|
|
|
<fieldset aria-hidden="true" data-md-field-outline>
|
|
<legend>@if (filled($label))<span>{{ $label }}</span>@endif</legend>
|
|
</fieldset>
|
|
|
|
@if (filled($label))
|
|
<label for="{{ $id }}" data-md-field-label>{{ $label }}</label>
|
|
@endif
|
|
</div>
|
|
|
|
@if ($messages !== [] || filled($hint) || $counter !== null)
|
|
<div data-md-field-support-row>
|
|
@if ($messages !== [])
|
|
<div id="{{ $id }}-support" data-md-field-support role="alert">
|
|
@foreach ($messages as $message)
|
|
<p>{{ $message }}</p>
|
|
@endforeach
|
|
</div>
|
|
@elseif (filled($hint))
|
|
<p id="{{ $id }}-support" data-md-field-support @if (filled($hintClass)) class="{{ $hintClass }}" @endif>{{ $hint }}</p>
|
|
@endif
|
|
|
|
@if ($counter !== null)
|
|
<span
|
|
data-md-field-counter
|
|
x-data="{
|
|
used: 0,
|
|
max: {{ $counter }},
|
|
spoken: '',
|
|
settle: null,
|
|
init() {
|
|
const control = this.$el.closest('[data-md-field]').querySelector('[data-md-field-control]');
|
|
|
|
this.used = control.value.length;
|
|
control.addEventListener('input', () => (this.used = control.value.length));
|
|
|
|
this.$watch('used', () => {
|
|
clearTimeout(this.settle);
|
|
this.settle = setTimeout(() => (this.spoken = @js(__('Character count, :used/:max')).replace(':used', this.used).replace(':max', this.max)), 1000);
|
|
});
|
|
},
|
|
destroy() {
|
|
clearTimeout(this.settle);
|
|
},
|
|
}"
|
|
x-bind:data-md-over="used > max"
|
|
>
|
|
<span aria-hidden="true" x-text="`${used}/${max}`">0/{{ $counter }}</span>
|
|
<span class="md-visually-hidden" aria-live="polite" aria-atomic="true" x-text="spoken"></span>
|
|
</span>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|