Bound a text field's width from medium
M3 § Text Fields, Behaviour: compact may let a field span full width, but medium and expanded must bound it — "never let it span the full width of a large screen". Nothing in the package capped a field (plan step 24, audit docs/audits/m3-alignment/inputs.md § Missing). From `medium` a field now stops at 40rem — the site names no number, so the header says where this one comes from — which a `max-w-*` class beats and `full` takes off. The search bar already carries M3's own 720px. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
c8c2d2e13e
commit
72a3e22fbc
@@ -555,6 +555,7 @@ M3 text fields. `variant`: `outlined` or `filled`; without it, `config('livewire
|
||||
- `<x-input>`: `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`.
|
||||
- `<x-password>`: a reveal button; `icon`, `size`.
|
||||
- `<x-textarea>`: grows from `rows` (3) to `max-rows`, then scrolls; `:autogrow="false"` for a fixed, hand-resizable one; `counter`.
|
||||
- Width: M3 asks that a text field never span the full width of a large screen, so from `medium` (600px) every field stops at **40rem**; below that it fills its pane. A `max-w-*` class on the component narrows or widens it, and `full` (on `<x-field>`, `<x-input>`, `<x-textarea>`) takes the bound off for a field that really is the width of its pane — a search row, an editor. `<x-search>`'s bar carries M3's own bound, 720px.
|
||||
- `counter` (on `<x-input>` and `<x-textarea>`) 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.
|
||||
- `<x-select>`: native `<select>` (M3 menu where the browser supports customizable selects). `options` as `['id' => …, 'name' => …, 'disabled' => bool]`, `option-value`, `option-label`, `placeholder` + `placeholder-value`, or `<option>`s in the slot; `icon`, `size`.
|
||||
- `<x-file>`: native file input; errors from `photos` and `photos.*`. Show previews of what was chosen yourself.
|
||||
|
||||
@@ -42,6 +42,20 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* M3 § Text Fields, Behaviour: "compact breakpoints can let a text field span full width;
|
||||
* medium/expanded should bound it with flexible margins/other containers — never let it span the
|
||||
* full width of a large screen." The site names no number, so 40rem is this package's: it is
|
||||
* under half the `large` class's 1200px, a little over the `expanded` class's half, and holds
|
||||
* about the 70 characters body-large reads best at. It is a ceiling, not a width — a narrower
|
||||
* pane still gets a narrower field. A `max-w-*` class from the call site beats it, because
|
||||
* utilities come after this layer, and `full` takes it off for a field that really is the width
|
||||
* of its pane (a search-and-filter row, an editor). Below `medium` nothing is bounded. */
|
||||
@media (width >= 37.5rem) {
|
||||
.field:not([data-full]) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* A filled field's resting indicator line is on-surface-variant, where an outline is outline. */
|
||||
.field[data-variant="filled"] {
|
||||
--field-edge: var(--md-sys-color-on-surface-variant);
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
"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.
|
||||
|
||||
`class` from the call site lands on the outermost element, never the control, so a margin or
|
||||
a width is safe here. --}}
|
||||
|
||||
@@ -38,6 +42,7 @@
|
||||
'floated' => false,
|
||||
'mono' => false,
|
||||
'counter' => null,
|
||||
'full' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
@@ -58,6 +63,7 @@
|
||||
@if ($messages !== []) data-invalid @endif
|
||||
@if ($floated) data-floated @endif
|
||||
@if ($mono) data-mono @endif
|
||||
@if ($full) data-full @endif
|
||||
>
|
||||
<div class="field-box">
|
||||
@if ($icon)
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
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. Every other attribute reaches the `<input>`: `type`, `min`, `step`,
|
||||
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
|
||||
@@ -23,6 +24,7 @@
|
||||
'clearable' => false,
|
||||
'copyable' => false,
|
||||
'counter' => false,
|
||||
'full' => false,
|
||||
'size' => 'md',
|
||||
'variant' => null,
|
||||
'mono' => false,
|
||||
@@ -39,7 +41,7 @@
|
||||
$max = $counter ? $attributes->get('maxlength') : null;
|
||||
@endphp
|
||||
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$prefix :$suffix :$size :$variant :$mono :counter="$max" :class="$attributes->get('class')" :data-readonly="$attributes->get('readonly') ? '' : null">
|
||||
<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-readonly="$attributes->get('readonly') ? '' : null">
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'placeholder'])->merge(['type' => 'text']) }}
|
||||
id="{{ $id }}"
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
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`, and `counter` for M3's character counter against `maxlength`;
|
||||
every other attribute reaches the `<textarea>`. --}}
|
||||
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
|
||||
attribute reaches the `<textarea>`. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
@@ -15,6 +16,7 @@
|
||||
'maxRows' => null,
|
||||
'autogrow' => true,
|
||||
'counter' => false,
|
||||
'full' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
@@ -28,7 +30,7 @@
|
||||
$max = $counter ? $attributes->get('maxlength') : null;
|
||||
@endphp
|
||||
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$variant :counter="$max" :class="$attributes->get('class')" :data-readonly="$attributes->get('readonly') ? '' : null">
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$variant :counter="$max" :$full :class="$attributes->get('class')" :data-readonly="$attributes->get('readonly') ? '' : null">
|
||||
<textarea
|
||||
{{ $attributes->except(['class', 'id', 'placeholder']) }}
|
||||
id="{{ $id }}"
|
||||
|
||||
@@ -41,6 +41,14 @@
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'Width at medium and above' => <<<'BLADE'
|
||||
<div class="w-full space-y-4">
|
||||
{{-- M3: never let a text field span the full width of a large screen. From medium it stops at 40rem. --}}
|
||||
<x-input label="Bounded, the default" hint="No wider than 40rem from medium, however wide the pane is" />
|
||||
<x-input label="Narrower" class="max-w-xs" hint="A max-w-* class from the call site wins" />
|
||||
<x-input label="The width of the pane" full icon="search" hint="`full` takes the bound off — for a search row or an editor" />
|
||||
</div>
|
||||
BLADE,
|
||||
'Character counter' => <<<'BLADE'
|
||||
<div class="grid w-full gap-6 medium:grid-cols-2">
|
||||
<div class="grid content-start gap-4">
|
||||
|
||||
@@ -170,6 +170,16 @@ it('shows no counter without `counter` or without a maximum to count against', f
|
||||
->and((string) $this->blade('<x-textarea label="Note" counter />'))->not->toContain('field-counter');
|
||||
});
|
||||
|
||||
it('bounds a field\'s width from medium unless it is told to fill its pane', function () {
|
||||
expect(file_get_contents(__DIR__.'/../../../resources/css/components/field.css'))
|
||||
->toContain('@media (width >= 37.5rem)')
|
||||
->toMatch('/\.field:not\(\[data-full\]\)\s*\{\s*max-width: 40rem;/');
|
||||
|
||||
expect((string) $this->blade('<x-input label="Share name" full />'))->toContain('data-full')
|
||||
->and((string) $this->blade('<x-textarea label="Message" full />'))->toContain('data-full')
|
||||
->and((string) $this->blade('<x-input label="Share name" />'))->not->toContain('data-full');
|
||||
});
|
||||
|
||||
it('lays out a form with its actions under an optional divider', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-form wire:submit="save" separator>
|
||||
|
||||
Reference in New Issue
Block a user