Draw the text field's chrome without Tailwind
<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
This commit is contained in:
co-authored by
Claude Opus 5
parent
a6e5a593d5
commit
0341f9d4ca
@@ -1,10 +1,11 @@
|
||||
{{-- 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. The states — the
|
||||
label resting or floating, the notch, focus, disabled, required — are selectors in
|
||||
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-invalid`, because only the server knows that.
|
||||
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
|
||||
@@ -12,7 +13,8 @@
|
||||
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.
|
||||
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
|
||||
@@ -21,12 +23,12 @@
|
||||
"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` 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 `max-w-*` class from the call
|
||||
site beats that, and `full` takes it off altogether.
|
||||
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. --}}
|
||||
a width is safe here; `hint-class` lands on the hint. --}}
|
||||
|
||||
@props([
|
||||
'id',
|
||||
@@ -50,6 +52,7 @@
|
||||
? $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';
|
||||
@@ -57,67 +60,67 @@
|
||||
@endphp
|
||||
|
||||
<div
|
||||
{{ $attributes->class(['field']) }}
|
||||
data-variant="{{ $variant }}"
|
||||
data-size="{{ $density }}"
|
||||
@if ($messages !== []) data-invalid @endif
|
||||
@if ($floated) data-floated @endif
|
||||
@if ($mono) data-mono @endif
|
||||
@if ($full) data-full @endif
|
||||
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 class="field-box">
|
||||
<div data-md-field-box>
|
||||
@if ($icon)
|
||||
<x-livewire-material::icon :name="$icon" class="field-icon size-(--field-icon)" />
|
||||
<x-livewire-material::icon :name="$icon" :size="$iconSize" data-md-field-icon />
|
||||
@endif
|
||||
|
||||
@if (filled($prefix))
|
||||
<span class="field-affix">{{ $prefix }}</span>
|
||||
<span data-md-field-affix>{{ $prefix }}</span>
|
||||
@endif
|
||||
|
||||
{{ $slot }}
|
||||
|
||||
@if (filled($suffix))
|
||||
<span class="field-affix">{{ $suffix }}</span>
|
||||
<span data-md-field-affix>{{ $suffix }}</span>
|
||||
@endif
|
||||
|
||||
{{ $trailing ?? '' }}
|
||||
|
||||
@if ($errorIcon)
|
||||
<x-livewire-material::icon name="error" class="field-trailing field-error size-(--field-icon)" :label="__('Error')" />
|
||||
<x-livewire-material::icon name="error" :size="$iconSize" data-md-field-trailing data-md-field-error :label="__('Error')" />
|
||||
@endif
|
||||
|
||||
<fieldset aria-hidden="true" class="field-outline">
|
||||
<fieldset aria-hidden="true" data-md-field-outline>
|
||||
<legend>@if (filled($label))<span>{{ $label }}</span>@endif</legend>
|
||||
</fieldset>
|
||||
|
||||
@if (filled($label))
|
||||
<label for="{{ $id }}" class="field-label">{{ $label }}</label>
|
||||
<label for="{{ $id }}" data-md-field-label>{{ $label }}</label>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($messages !== [] || filled($hint) || $counter !== null)
|
||||
<div class="field-support-row">
|
||||
<div data-md-field-support-row>
|
||||
@if ($messages !== [])
|
||||
<div id="{{ $id }}-support" class="field-support" role="alert">
|
||||
<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" @class(array_filter(['field-support', $hintClass]))>{{ $hint }}</p>
|
||||
<p id="{{ $id }}-support" data-md-field-support @if (filled($hintClass)) class="{{ $hintClass }}" @endif>{{ $hint }}</p>
|
||||
@endif
|
||||
|
||||
@if ($counter !== null)
|
||||
<span
|
||||
class="field-counter"
|
||||
data-field-counter
|
||||
data-md-field-counter
|
||||
x-data="{
|
||||
used: 0,
|
||||
max: {{ $counter }},
|
||||
spoken: '',
|
||||
settle: null,
|
||||
init() {
|
||||
const control = this.$el.closest('.field').querySelector('.field-control');
|
||||
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));
|
||||
@@ -131,10 +134,10 @@
|
||||
clearTimeout(this.settle);
|
||||
},
|
||||
}"
|
||||
x-bind:data-over="used > max"
|
||||
x-bind:data-md-over="used > max"
|
||||
>
|
||||
<span aria-hidden="true" x-text="`${used}/${max}`">0/{{ $counter }}</span>
|
||||
<span class="sr-only" aria-live="polite" aria-atomic="true" x-text="spoken"></span>
|
||||
<span class="md-visually-hidden" aria-live="polite" aria-atomic="true" x-text="spoken"></span>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user