From c8c2d2e13e55f40b752ae40292881d6c08ce3e55 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 06:21:22 +0200 Subject: [PATCH 1/7] Count a text field's characters against its maximum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M3 lists "supporting text + character counter" as a configuration of both text-field variants; nothing in the package drew one (plan step 24, audit docs/audits/m3-alignment/inputs.md § Missing). `counter` on `` and `` now puts `n/max` at the end of the supporting-text row, counted from the control on every input, in the error colour past the maximum, and said as M3's own "Character count, 5/20" from a polite region once typing settles. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- .../livewire-material-development/SKILL.md | 5 +- resources/css/components/field.css | 29 +++++++++- resources/views/components/field.blade.php | 58 ++++++++++++++++--- resources/views/components/input.blade.php | 8 ++- resources/views/components/textarea.blade.php | 8 ++- .../views/showcase/sections/fields.blade.php | 10 ++++ tests/Feature/Components/FieldTest.php | 23 ++++++++ 7 files changed, 127 insertions(+), 14 deletions(-) diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 30784477..c9c00ee7 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -552,9 +552,10 @@ A one-column grid of fields with an `actions` slot at the foot (the slot takes i M3 text fields. `variant`: `outlined` or `filled`; without it, `config('livewire-material.fields.variant')` (`outlined`). All take `label`, `hint`, `variant` (and all but `` a `hint-class`, classes added to the hint: `hint-class="text-warning"`), and read their errors from the bag under the `wire:model` name, or the `name` in a plain form (`photos[]` → `photos`, `address[city]` → `address.city`); the error replaces the hint, sets `aria-invalid`, and puts an `error` icon at the end of the row as M3's second indicator (not on `size="xs"`, and not when the field already trails something — `icon-right`, `clearable`, `copyable`). `class` lands on the field's outer element (margins, widths); every other attribute (`wire:model`, `type`, `required`, `readonly`, `autocomplete`) reaches the control. Never pass `placeholder` expecting it to show while a label rests in the field: it shows once the field has focus. -- ``: `icon`, `icon-right`, `prefix`, `suffix`, `clearable`, `copyable` (copies the value, confirms with a snackbar), `size` (`sm` 40px, `xs` 32px — for unlabelled toolbar controls; give them `aria-label`), `mono`. +- ``: `icon`, `icon-right`, `prefix`, `suffix`, `clearable`, `copyable` (copies the value, confirms with a snackbar), `counter`, `size` (`sm` 40px, `xs` 32px — for unlabelled toolbar controls; give them `aria-label`), `mono`. - ``: a reveal button; `icon`, `size`. -- ``: grows from `rows` (3) to `max-rows`, then scrolls; `:autogrow="false"` for a fixed, hand-resizable one. +- ``: grows from `rows` (3) to `max-rows`, then scrolls; `:autogrow="false"` for a fixed, hand-resizable one; `counter`. +- `counter` (on `` and ``) puts M3's character counter at the end of the supporting-text row, beside the hint or the error: `n/max`, counted on every keystroke against the field's own `maxlength`, and in the error colour once the value is past it. It needs `maxlength` — without one there is nothing to count against and nothing is drawn. It is said as "Character count, 5/20" from a polite region a second after typing stops. - ``: native ``: `type`, `min`, `step`, + (a share link, a token). `counter` counts the characters used against `maxlength`, at the end of + the supporting-text row. Every other attribute reaches the ``: `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 @@ -21,6 +22,7 @@ 'suffix' => null, 'clearable' => false, 'copyable' => false, + 'counter' => false, 'size' => 'md', 'variant' => null, 'mono' => false, @@ -33,9 +35,11 @@ // 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; @endphp - + except(['class', 'id', 'placeholder'])->merge(['type' => 'text']) }} id="{{ $id }}" diff --git a/resources/views/components/textarea.blade.php b/resources/views/components/textarea.blade.php index 2789afcc..84f5e5a9 100644 --- a/resources/views/components/textarea.blade.php +++ b/resources/views/components/textarea.blade.php @@ -3,7 +3,8 @@ It starts at `rows` lines (3) and grows with what is typed up to `max-rows`, then scrolls; `: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`; every other attribute reaches the `