Say a field is in error twice, and stop dashing a read-only one
M3 wants two visual indicators per state and names the pair for a text field: the error colours and a trailing error icon. The field now draws one, labelled "Error", unless the caller already trails it or the field is xs. A read-only field loses its dashed outline: M3 keeps "the same visual style as an editable field", and the native readonly attribute is what tells a screen reader. The data-readonly hook stays. Plan step 20, findings IN-18 and IN-19. 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
42d62ed00c
commit
856b6f476b
@@ -550,7 +550,7 @@ A one-column grid of fields with an `actions` slot at the foot (the slot takes i
|
||||
|
||||
### `<x-field>`, `<x-input>`, `<x-password>`, `<x-textarea>`, `<x-select>`, `<x-file>`
|
||||
|
||||
M3 text fields. `variant`: `outlined` or `filled`; without it, `config('livewire-material.fields.variant')` (`outlined`). All take `label`, `hint`, `variant` (and all but `<x-file>` 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 and sets `aria-invalid`. `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.
|
||||
M3 text fields. `variant`: `outlined` or `filled`; without it, `config('livewire-material.fields.variant')` (`outlined`). All take `label`, `hint`, `variant` (and all but `<x-file>` 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.
|
||||
|
||||
- `<x-input>`: `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`.
|
||||
- `<x-password>`: a reveal button; `icon`, `size`.
|
||||
|
||||
@@ -238,6 +238,11 @@
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
/* The error's second indicator: M3 asks that the colour change never stand alone. */
|
||||
.field-error {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
.field-icon,
|
||||
.field-arrow {
|
||||
pointer-events: none;
|
||||
@@ -421,11 +426,11 @@
|
||||
--field-ink: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
/* Dashed for a field the caller made read-only — marked on the field, not read off the control,
|
||||
because a date picker makes its own input read-only too. */
|
||||
.field[data-readonly] .field-outline {
|
||||
border-style: dashed;
|
||||
}
|
||||
/* `data-readonly` marks a field the caller made read-only — on the field, not read off the
|
||||
control, because a date picker makes its own input read-only too. Nothing here draws it
|
||||
differently: M3 says a read-only field keeps "the same visual style as an editable field, but
|
||||
clearly labeled read-only", and the native `readonly` attribute is what carries that. The hook
|
||||
stays for an application that wants a mark of its own. */
|
||||
|
||||
.field:has(.field-control:disabled) {
|
||||
--field-edge: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
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.
|
||||
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.
|
||||
|
||||
`class` from the call site lands on the outermost element, never the control, so a margin or
|
||||
a width is safe here. --}}
|
||||
@@ -34,12 +36,16 @@
|
||||
$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';
|
||||
// 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';
|
||||
@endphp
|
||||
|
||||
<div
|
||||
{{ $attributes->class(['field']) }}
|
||||
data-variant="{{ $variant }}"
|
||||
data-size="{{ in_array($size, ['sm', 'xs'], true) ? $size : 'md' }}"
|
||||
data-size="{{ $density }}"
|
||||
@if ($messages !== []) data-invalid @endif
|
||||
@if ($floated) data-floated @endif
|
||||
@if ($mono) data-mono @endif
|
||||
@@ -61,6 +67,10 @@
|
||||
|
||||
{{ $trailing ?? '' }}
|
||||
|
||||
@if ($errorIcon)
|
||||
<x-livewire-material::icon name="error" class="field-trailing field-error size-(--field-icon)" :label="__('Error')" />
|
||||
@endif
|
||||
|
||||
<fieldset aria-hidden="true" class="field-outline">
|
||||
<legend>@if (filled($label))<span>{{ $label }}</span>@endif</legend>
|
||||
</fieldset>
|
||||
|
||||
@@ -45,6 +45,20 @@ it('replaces the hint with the errors for its model', function () {
|
||||
->not->toContain('We never share it');
|
||||
});
|
||||
|
||||
it('pairs the error colours with a trailing error icon', function () {
|
||||
$errors = ['email' => ['Enter a valid email address.']];
|
||||
|
||||
expect((string) $this->withViewErrors($errors)->blade('<x-input id="email" label="Email" wire:model="email" />'))
|
||||
->toContain('field-trailing field-error')
|
||||
->toContain('aria-label="Error"')
|
||||
->and((string) $this->withViewErrors($errors)->blade('<x-input id="email" label="Email" size="xs" wire:model="email" />'))
|
||||
->not->toContain('field-error')
|
||||
->and((string) $this->withViewErrors($errors)->blade('<x-input id="email" label="Email" icon-right="info" wire:model="email" />'))
|
||||
->not->toContain('field-error')
|
||||
->and((string) $this->blade('<x-input id="email" label="Email" />'))
|
||||
->not->toContain('field-error');
|
||||
});
|
||||
|
||||
it('puts icons, affixes and the size on the field, and every other attribute on the input', function () {
|
||||
$html = (string) $this->blade('<x-input label="Price" icon="payments" icon-right="info" prefix="CHF" suffix="per month" size="sm" type="number" min="0" class="w-40" mono readonly />');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user