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-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-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`.
|
- `<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.
|
- `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-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.
|
- `<x-file>`: native file input; errors from `photos` and `photos.*`. Show previews of what was chosen yourself.
|
||||||
|
|||||||
@@ -42,6 +42,20 @@
|
|||||||
min-width: 0;
|
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. */
|
/* A filled field's resting indicator line is on-surface-variant, where an outline is outline. */
|
||||||
.field[data-variant="filled"] {
|
.field[data-variant="filled"] {
|
||||||
--field-edge: var(--md-sys-color-on-surface-variant);
|
--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
|
"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.
|
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
|
`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. --}}
|
||||||
|
|
||||||
@@ -38,6 +42,7 @@
|
|||||||
'floated' => false,
|
'floated' => false,
|
||||||
'mono' => false,
|
'mono' => false,
|
||||||
'counter' => null,
|
'counter' => null,
|
||||||
|
'full' => false,
|
||||||
])
|
])
|
||||||
|
|
||||||
@php
|
@php
|
||||||
@@ -58,6 +63,7 @@
|
|||||||
@if ($messages !== []) data-invalid @endif
|
@if ($messages !== []) data-invalid @endif
|
||||||
@if ($floated) data-floated @endif
|
@if ($floated) data-floated @endif
|
||||||
@if ($mono) data-mono @endif
|
@if ($mono) data-mono @endif
|
||||||
|
@if ($full) data-full @endif
|
||||||
>
|
>
|
||||||
<div class="field-box">
|
<div class="field-box">
|
||||||
@if ($icon)
|
@if ($icon)
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
and `mono` for a field that holds code. `clearable` adds a trailing button that empties the
|
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
|
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
|
(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.
|
`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
|
The placeholder is a single space when none is given, because the label can only tell an
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
'clearable' => false,
|
'clearable' => false,
|
||||||
'copyable' => false,
|
'copyable' => false,
|
||||||
'counter' => false,
|
'counter' => false,
|
||||||
|
'full' => false,
|
||||||
'size' => 'md',
|
'size' => 'md',
|
||||||
'variant' => null,
|
'variant' => null,
|
||||||
'mono' => false,
|
'mono' => false,
|
||||||
@@ -39,7 +41,7 @@
|
|||||||
$max = $counter ? $attributes->get('maxlength') : null;
|
$max = $counter ? $attributes->get('maxlength') : null;
|
||||||
@endphp
|
@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
|
<input
|
||||||
{{ $attributes->except(['class', 'id', 'placeholder'])->merge(['type' => 'text']) }}
|
{{ $attributes->except(['class', 'id', 'placeholder'])->merge(['type' => 'text']) }}
|
||||||
id="{{ $id }}"
|
id="{{ $id }}"
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
It starts at `rows` lines (3) and grows with what is typed up to `max-rows`, then scrolls;
|
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
|
`: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
|
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`;
|
box. `label`, `hint`, `variant`, `counter` for M3's character counter against `maxlength`, and
|
||||||
every other attribute reaches the `<textarea>`. --}}
|
`full` to span the pane past the 40rem M3 bounds a field to from `medium`; every other
|
||||||
|
attribute reaches the `<textarea>`. --}}
|
||||||
|
|
||||||
@props([
|
@props([
|
||||||
'label' => null,
|
'label' => null,
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
'maxRows' => null,
|
'maxRows' => null,
|
||||||
'autogrow' => true,
|
'autogrow' => true,
|
||||||
'counter' => false,
|
'counter' => false,
|
||||||
|
'full' => false,
|
||||||
])
|
])
|
||||||
|
|
||||||
@php
|
@php
|
||||||
@@ -28,7 +30,7 @@
|
|||||||
$max = $counter ? $attributes->get('maxlength') : null;
|
$max = $counter ? $attributes->get('maxlength') : null;
|
||||||
@endphp
|
@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
|
<textarea
|
||||||
{{ $attributes->except(['class', 'id', 'placeholder']) }}
|
{{ $attributes->except(['class', 'id', 'placeholder']) }}
|
||||||
id="{{ $id }}"
|
id="{{ $id }}"
|
||||||
|
|||||||
@@ -41,6 +41,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
BLADE,
|
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'
|
'Character counter' => <<<'BLADE'
|
||||||
<div class="grid w-full gap-6 medium:grid-cols-2">
|
<div class="grid w-full gap-6 medium:grid-cols-2">
|
||||||
<div class="grid content-start gap-4">
|
<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');
|
->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 () {
|
it('lays out a form with its actions under an optional divider', function () {
|
||||||
$html = (string) $this->blade(<<<'BLADE'
|
$html = (string) $this->blade(<<<'BLADE'
|
||||||
<x-form wire:submit="save" separator>
|
<x-form wire:submit="save" separator>
|
||||||
|
|||||||
Reference in New Issue
Block a user