Add M3 text fields, selects and selection controls
tests / feature (8.4) (push) Successful in 1m5s
tests / feature (8.5) (push) Successful in 1m4s
tests / lint (push) Successful in 59s
tests / browser (chrome, chromium) (push) Successful in 2m54s
tests / browser (firefox, firefox) (push) Successful in 3m3s
tests / browser (safari, webkit) (push) Successful in 4m13s
tests / feature (8.4) (push) Successful in 1m5s
tests / feature (8.5) (push) Successful in 1m4s
tests / lint (push) Successful in 59s
tests / browser (chrome, chromium) (push) Successful in 2m54s
tests / browser (firefox, firefox) (push) Successful in 3m3s
tests / browser (safari, webkit) (push) Successful in 4m13s
Outlined and filled text fields (input, password, auto-growing textarea, native select with the customizable select menu, file), checkbox with an indeterminate state, radio buttons, the M3 switch and form, ported from ReStride and extended. The first half of Phase 6. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
co-authored by
Claude Opus 5
parent
ad724662ca
commit
7f92f1cb29
@@ -0,0 +1,62 @@
|
||||
{{-- A checkbox, M3's: an 18px box that fills with primary when ticked, in a 40px state layer.
|
||||
|
||||
The label and its hint sit beside it and are its accessible name — the whole row is the
|
||||
`<label>`, so either can be pressed. `right` puts the box at the end of the row, for a setting
|
||||
read left to right. `indeterminate` shows the mixed state of a "select all" whose items are
|
||||
partly ticked: HTML has no attribute for it, so the input is marked `data-indeterminate` and
|
||||
resources/js/field.js keeps the property in step, on load and after every morph. Errors are
|
||||
read from the bag under the `wire:model` name. Every other attribute reaches the `<input>`
|
||||
(resources/css/components/selection.css). --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
'hint' => null,
|
||||
'right' => false,
|
||||
'indeterminate' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$model = $attributes->whereStartsWith('wire:model')->first();
|
||||
$id = $attributes->get('id') ?? 'check-'.substr(md5($model.'|'.$label.'|'.$attributes->get('value')), 0, 12);
|
||||
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : [];
|
||||
@endphp
|
||||
|
||||
<div {{ $attributes->only(['class', 'wire:key'])->class(['min-w-0']) }}>
|
||||
<label for="{{ $id }}" data-selection @if ($messages !== []) data-invalid @endif @class([
|
||||
'flex gap-4',
|
||||
'items-start' => filled($hint),
|
||||
'items-center' => blank($hint),
|
||||
'flex-row-reverse justify-between' => $right,
|
||||
])>
|
||||
<span data-checkbox @class(['mt-[3px]' => filled($hint)])>
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'wire:key']) }}
|
||||
id="{{ $id }}"
|
||||
type="checkbox"
|
||||
@if ($indeterminate) data-indeterminate @endif
|
||||
@if ($messages !== []) aria-invalid="true" aria-describedby="{{ $id }}-support" @endif
|
||||
/>
|
||||
<x-icon name="check" class="size-4" data-check />
|
||||
<x-icon name="remove" class="size-4" data-mixed />
|
||||
</span>
|
||||
|
||||
@if (filled($label) || filled($hint))
|
||||
<span class="min-w-0">
|
||||
@if (filled($label))
|
||||
<span class="block type-body-lg text-on-surface" data-selection-label>{{ $label }}</span>
|
||||
@endif
|
||||
@if (filled($hint))
|
||||
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $hint }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</label>
|
||||
|
||||
@if ($messages !== [])
|
||||
<div id="{{ $id }}-support" class="mt-1 type-body-sm text-error" role="alert">
|
||||
@foreach ($messages as $message)
|
||||
<p>{{ $message }}</p>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,82 @@
|
||||
{{-- 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
|
||||
resources/css/components/field.css, which explains the anatomy; the only state this file
|
||||
decides is `data-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
|
||||
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.
|
||||
|
||||
`class` from the call site lands on the outermost element, never the control, so a margin or
|
||||
a width is safe here. --}}
|
||||
|
||||
@props([
|
||||
'id',
|
||||
'label' => null,
|
||||
'hint' => null,
|
||||
'hintClass' => null,
|
||||
'messages' => [],
|
||||
'icon' => null,
|
||||
'prefix' => null,
|
||||
'suffix' => null,
|
||||
'size' => 'md',
|
||||
'variant' => null,
|
||||
'floated' => false,
|
||||
'mono' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$variant = in_array($variant, ['outlined', 'filled'], true)
|
||||
? $variant
|
||||
: (config('livewire-material.fields.variant') === 'filled' ? 'filled' : 'outlined');
|
||||
@endphp
|
||||
|
||||
<div
|
||||
{{ $attributes->class(['field']) }}
|
||||
data-variant="{{ $variant }}"
|
||||
data-size="{{ in_array($size, ['sm', 'xs'], true) ? $size : 'md' }}"
|
||||
@if ($messages !== []) data-invalid @endif
|
||||
@if ($floated) data-floated @endif
|
||||
@if ($mono) data-mono @endif
|
||||
>
|
||||
<div class="field-box">
|
||||
@if ($icon)
|
||||
<x-icon :name="$icon" class="field-icon size-(--field-icon)" />
|
||||
@endif
|
||||
|
||||
@if (filled($prefix))
|
||||
<span class="field-affix">{{ $prefix }}</span>
|
||||
@endif
|
||||
|
||||
{{ $slot }}
|
||||
|
||||
@if (filled($suffix))
|
||||
<span class="field-affix">{{ $suffix }}</span>
|
||||
@endif
|
||||
|
||||
{{ $trailing ?? '' }}
|
||||
|
||||
<fieldset aria-hidden="true" class="field-outline">
|
||||
<legend>@if (filled($label))<span>{{ $label }}</span>@endif</legend>
|
||||
</fieldset>
|
||||
|
||||
@if (filled($label))
|
||||
<label for="{{ $id }}" class="field-label">{{ $label }}</label>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($messages !== [])
|
||||
<div id="{{ $id }}-support" class="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>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
{{-- A file picker, in the text field's chrome (resources/views/components/field.blade.php).
|
||||
|
||||
M3 has no file field, and building one would redo what the browser's input already does — the
|
||||
picker, a file dropped on it, the keyboard, what a screen reader announces, the `change` a
|
||||
Livewire upload listens for. So the input stays native, its label always floats (the button
|
||||
stands where a resting label would), and only its `::file-selector-button` is restyled: a tonal
|
||||
pill, so it reads as the thing to press (resources/css/components/field.css).
|
||||
|
||||
The browser's own line beside the button ("No file chosen", "3 files") is drawn transparent:
|
||||
it cannot be truncated or wrapped, and the caller shows what was chosen anyway, as previews. A
|
||||
screen reader still hears it. The errors are the model's and, for a `multiple` input, each
|
||||
file's (`photos.*`): a file refused for its size belongs under this field as much as the
|
||||
batch's own count does. `label`, `hint`, `variant`; every other attribute reaches the `<input>`. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
'hint' => null,
|
||||
'variant' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$model = $attributes->whereStartsWith('wire:model')->first();
|
||||
$id = $attributes->get('id') ?? 'field-'.substr(md5('file|'.$model.'|'.$label), 0, 12);
|
||||
$messages = $model !== null && isset($errors)
|
||||
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($model), $errors->get($model.'.*')])))
|
||||
: [];
|
||||
@endphp
|
||||
|
||||
<x-field :$id :$label :$hint :$messages :$variant floated :class="$attributes->get('class')">
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'type']) }}
|
||||
type="file"
|
||||
id="{{ $id }}"
|
||||
@if ($messages !== []) aria-invalid="true" @endif
|
||||
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif
|
||||
class="field-control"
|
||||
/>
|
||||
</x-field>
|
||||
@@ -0,0 +1,26 @@
|
||||
{{-- A form: its fields in one column, and its actions at the foot.
|
||||
|
||||
`gap-4` between fields, because a text field floats its label half above its outline: any less
|
||||
and the hint under one field runs into the label of the next. The `actions` slot takes a class
|
||||
of its own; `separator` draws a divider above it.
|
||||
|
||||
`grid-cols-1`, because a bare `grid` has no columns and its implicit one floors at the
|
||||
content's min-content: on a phone a wide field would push the form past its column. --}}
|
||||
|
||||
@props([
|
||||
'separator' => false,
|
||||
])
|
||||
|
||||
<form {{ $attributes->class(['grid grid-cols-1 auto-rows-min gap-4']) }}>
|
||||
{{ $slot }}
|
||||
|
||||
@isset($actions)
|
||||
@if ($separator)
|
||||
<x-divider />
|
||||
@endif
|
||||
|
||||
<div {{ $actions->attributes->class(['flex flex-wrap items-center justify-end gap-2']) }}>
|
||||
{{ $actions }}
|
||||
</div>
|
||||
@endisset
|
||||
</form>
|
||||
@@ -0,0 +1,79 @@
|
||||
{{-- A text field: M3's outlined or filled one (resources/views/components/field.blade.php).
|
||||
|
||||
`label`, `hint`, leading `icon`, trailing `icon-right`, `prefix` and `suffix` (text beside the
|
||||
value, such as a currency or a unit), `variant`, `size` for unlabelled controls in toolbars,
|
||||
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). 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
|
||||
empty field from a filled one through `:placeholder-shown`. With a label, a real placeholder
|
||||
shows only while the field has focus — until then the label stands where it would be. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
'hint' => null,
|
||||
'hintClass' => null,
|
||||
'icon' => null,
|
||||
'iconRight' => null,
|
||||
'prefix' => null,
|
||||
'suffix' => null,
|
||||
'clearable' => false,
|
||||
'copyable' => false,
|
||||
'size' => 'md',
|
||||
'variant' => null,
|
||||
'mono' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$model = $attributes->whereStartsWith('wire:model')->first();
|
||||
$placeholder = filled($attributes->get('placeholder')) ? $attributes->get('placeholder') : ' ';
|
||||
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|'.$placeholder), 0, 12);
|
||||
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : [];
|
||||
@endphp
|
||||
|
||||
<x-field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$prefix :$suffix :$size :$variant :$mono :class="$attributes->get('class')" :data-readonly="$attributes->get('readonly') ? '' : null">
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'placeholder'])->merge(['type' => 'text']) }}
|
||||
id="{{ $id }}"
|
||||
placeholder="{{ $placeholder }}"
|
||||
@if ($messages !== []) aria-invalid="true" @endif
|
||||
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif
|
||||
class="field-control"
|
||||
/>
|
||||
|
||||
@if ($clearable || $copyable || $iconRight)
|
||||
<x-slot:trailing>
|
||||
@if ($clearable)
|
||||
<button
|
||||
type="button"
|
||||
x-data
|
||||
x-on:click="const control = $el.closest('.field').querySelector('.field-control'); control.value = ''; control.dispatchEvent(new Event('input', { bubbles: true })); control.dispatchEvent(new Event('change', { bubbles: true })); control.focus();"
|
||||
class="field-trailing field-clear field-button"
|
||||
aria-label="{{ __('Clear') }}"
|
||||
data-field-clear
|
||||
>
|
||||
<x-icon name="close" class="size-(--field-icon)" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@if ($copyable)
|
||||
<button
|
||||
type="button"
|
||||
x-data
|
||||
x-on:click="navigator.clipboard.writeText($el.closest('.field').querySelector('.field-control').value).then(() => window.materialToast(@js(__('Copied to the clipboard')), { type: 'success' }))"
|
||||
class="field-trailing field-button"
|
||||
aria-label="{{ __('Copy') }}"
|
||||
data-field-copy
|
||||
>
|
||||
<x-icon name="content_copy" class="size-(--field-icon)" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@if ($iconRight)
|
||||
<x-icon :name="$iconRight" class="field-trailing size-(--field-icon)" />
|
||||
@endif
|
||||
</x-slot:trailing>
|
||||
@endif
|
||||
</x-field>
|
||||
@@ -0,0 +1,50 @@
|
||||
{{-- A password field, with M3's trailing eye to show what was typed.
|
||||
|
||||
The eye flips the control's `type` and nothing else, so the value never leaves the field and a
|
||||
password manager still sees a password input on load. Its label says what pressing it will do.
|
||||
`label`, `hint`, `icon`, `variant`, `size`; every other attribute reaches the `<input>`
|
||||
(`autocomplete="current-password"`, `wire:model`). --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
'hint' => null,
|
||||
'hintClass' => null,
|
||||
'icon' => null,
|
||||
'size' => 'md',
|
||||
'variant' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$model = $attributes->whereStartsWith('wire:model')->first();
|
||||
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|password'), 0, 12);
|
||||
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : [];
|
||||
@endphp
|
||||
|
||||
<x-field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant :class="$attributes->get('class')" x-data="{ shown: false }">
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'type', 'placeholder']) }}
|
||||
id="{{ $id }}"
|
||||
type="password"
|
||||
x-bind:type="shown ? 'text' : 'password'"
|
||||
placeholder="{{ filled($attributes->get('placeholder')) ? $attributes->get('placeholder') : ' ' }}"
|
||||
@if ($messages !== []) aria-invalid="true" @endif
|
||||
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif
|
||||
class="field-control"
|
||||
/>
|
||||
|
||||
<x-slot:trailing>
|
||||
<button
|
||||
type="button"
|
||||
x-on:click="shown = ! shown"
|
||||
x-bind:aria-label="shown ? @js(__('Hide password')) : @js(__('Show password'))"
|
||||
x-bind:aria-pressed="shown"
|
||||
aria-label="{{ __('Show password') }}"
|
||||
aria-pressed="false"
|
||||
class="field-trailing field-button"
|
||||
data-field-reveal
|
||||
>
|
||||
<x-icon name="visibility" class="size-(--field-icon)" x-show="! shown" />
|
||||
<x-icon name="visibility_off" class="size-(--field-icon)" x-show="shown" x-cloak />
|
||||
</button>
|
||||
</x-slot:trailing>
|
||||
</x-field>
|
||||
@@ -0,0 +1,78 @@
|
||||
{{-- One choice from a few, each with room for a line of explanation: M3's radio buttons.
|
||||
|
||||
A `<fieldset>` whose legend is the question, so a screen reader reads the group's name with each
|
||||
option, and native radios sharing a name, so the arrow keys move between them. `options` as
|
||||
`['id' => …, 'name' => …, 'hint' => …]` (`'disabled' => true` greys one out), `option-value`,
|
||||
`option-label` and `option-hint` to read other keys, `inline` to lay them in a row from `sm`, and
|
||||
`value` for the option chosen when the page loads (with `wire:model`, Livewire sets it).
|
||||
Errors are read from the bag under the `wire:model` name
|
||||
(resources/css/components/selection.css). --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
'hint' => null,
|
||||
'options' => [],
|
||||
'optionValue' => 'id',
|
||||
'optionLabel' => 'name',
|
||||
'optionHint' => 'hint',
|
||||
'inline' => false,
|
||||
'value' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$model = $attributes->whereStartsWith('wire:model')->first();
|
||||
$name = $attributes->get('name') ?? $model ?? 'radio-'.substr(md5($label.'|'.json_encode($options)), 0, 12);
|
||||
$id = 'radio-'.substr(md5($name.'|'.$label), 0, 12);
|
||||
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : [];
|
||||
@endphp
|
||||
|
||||
<fieldset
|
||||
{{ $attributes->only(['class', 'wire:key'])->class(['min-w-0']) }}
|
||||
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif
|
||||
>
|
||||
@if (filled($label))
|
||||
<legend class="mb-3 type-label-lg text-on-surface-variant">{{ $label }}</legend>
|
||||
@endif
|
||||
|
||||
<div @class(['grid gap-4', 'sm:flex sm:flex-wrap sm:gap-x-6' => $inline])>
|
||||
@foreach ($options as $option)
|
||||
@php($optionHintText = data_get($option, $optionHint))
|
||||
|
||||
<label data-selection @if ($messages !== []) data-invalid @endif @class([
|
||||
'flex gap-4',
|
||||
'items-start' => filled($optionHintText),
|
||||
'items-center' => blank($optionHintText),
|
||||
])>
|
||||
<span data-radio @class(['mt-0.5' => filled($optionHintText)])>
|
||||
<input
|
||||
{{ $attributes->except(['class', 'wire:key', 'name', 'id']) }}
|
||||
type="radio"
|
||||
name="{{ $name }}"
|
||||
value="{{ data_get($option, $optionValue) }}"
|
||||
@disabled(data_get($option, 'disabled'))
|
||||
@checked($value !== null && (string) $value === (string) data_get($option, $optionValue))
|
||||
@if ($messages !== []) aria-invalid="true" @endif
|
||||
/>
|
||||
<span data-dot></span>
|
||||
</span>
|
||||
|
||||
<span class="min-w-0">
|
||||
<span class="block type-body-lg text-on-surface" data-selection-label>{{ data_get($option, $optionLabel) }}</span>
|
||||
@if (filled($optionHintText))
|
||||
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $optionHintText }}</span>
|
||||
@endif
|
||||
</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if ($messages !== [])
|
||||
<div id="{{ $id }}-support" class="mt-2 type-body-sm text-error" role="alert">
|
||||
@foreach ($messages as $message)
|
||||
<p>{{ $message }}</p>
|
||||
@endforeach
|
||||
</div>
|
||||
@elseif (filled($hint))
|
||||
<p id="{{ $id }}-support" class="mt-2 type-body-sm text-on-surface-variant">{{ $hint }}</p>
|
||||
@endif
|
||||
</fieldset>
|
||||
@@ -0,0 +1,57 @@
|
||||
{{-- A select: the native `<select>`, drawn as M3's exposed dropdown menu.
|
||||
|
||||
Native on purpose: the keyboard, type-to-find, the screen reader and the form value are the
|
||||
browser's, and `wire:model` binds to it as to any select. Where the browser has the
|
||||
customizable select (`appearance: base-select`) its open list is M3's menu, under the field
|
||||
and as wide (resources/css/components/menu.css); elsewhere the native list stands in. So the
|
||||
options stay plain text: the native list could show nothing more. A select always shows a
|
||||
value, so its label always floats.
|
||||
|
||||
`options` as a list of `['id' => …, 'name' => …]` (`'disabled' => true` greys one out),
|
||||
`option-value` and `option-label` to read other keys, and a `placeholder` that becomes the
|
||||
first option, valued `placeholder-value` (empty by default). Or pass `<option>`s in the slot.
|
||||
`label`, `hint`, `icon`, `variant`, `size`. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
'hint' => null,
|
||||
'hintClass' => null,
|
||||
'icon' => null,
|
||||
'options' => [],
|
||||
'optionValue' => 'id',
|
||||
'optionLabel' => 'name',
|
||||
'placeholder' => null,
|
||||
'placeholderValue' => null,
|
||||
'size' => 'md',
|
||||
'variant' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$model = $attributes->whereStartsWith('wire:model')->first();
|
||||
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|select'), 0, 12);
|
||||
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : [];
|
||||
@endphp
|
||||
|
||||
<x-field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant floated :class="$attributes->get('class')">
|
||||
<select
|
||||
{{ $attributes->except(['class', 'id']) }}
|
||||
id="{{ $id }}"
|
||||
@if ($messages !== []) aria-invalid="true" @endif
|
||||
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif
|
||||
class="field-control"
|
||||
>
|
||||
@if (filled($placeholder))
|
||||
<option value="{{ $placeholderValue }}">{{ $placeholder }}</option>
|
||||
@endif
|
||||
|
||||
@foreach ($options as $option)
|
||||
<option value="{{ data_get($option, $optionValue) }}" @disabled(data_get($option, 'disabled'))>{{ data_get($option, $optionLabel) }}</option>
|
||||
@endforeach
|
||||
|
||||
{{ $slot }}
|
||||
</select>
|
||||
|
||||
<x-slot:trailing>
|
||||
<x-icon name="arrow_drop_down" class="field-trailing field-arrow size-(--field-icon)" />
|
||||
</x-slot:trailing>
|
||||
</x-field>
|
||||
@@ -0,0 +1,39 @@
|
||||
{{-- A multi-line field: the text field, grown to its content.
|
||||
|
||||
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 `<textarea>`. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
'hint' => null,
|
||||
'hintClass' => null,
|
||||
'variant' => null,
|
||||
'rows' => 3,
|
||||
'maxRows' => null,
|
||||
'autogrow' => true,
|
||||
])
|
||||
|
||||
@php
|
||||
$model = $attributes->whereStartsWith('wire:model')->first();
|
||||
$placeholder = filled($attributes->get('placeholder')) ? $attributes->get('placeholder') : ' ';
|
||||
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|textarea'), 0, 12);
|
||||
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : [];
|
||||
@endphp
|
||||
|
||||
<x-field :$id :$label :$hint :hint-class="$hintClass" :$messages :$variant :class="$attributes->get('class')" :data-readonly="$attributes->get('readonly') ? '' : null">
|
||||
<textarea
|
||||
{{ $attributes->except(['class', 'id', 'placeholder']) }}
|
||||
id="{{ $id }}"
|
||||
rows="{{ (int) $rows }}"
|
||||
placeholder="{{ $placeholder }}"
|
||||
@if ($autogrow)
|
||||
data-autogrow
|
||||
style="--field-rows: {{ (int) $rows }};@if ($maxRows) --field-max-rows: {{ (int) $maxRows }};@endif"
|
||||
@endif
|
||||
@if ($messages !== []) aria-invalid="true" @endif
|
||||
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif
|
||||
class="field-control"
|
||||
></textarea>
|
||||
</x-field>
|
||||
@@ -0,0 +1,56 @@
|
||||
{{-- A switch, M3's: a 52×32 track whose handle grows and slides across when it turns on.
|
||||
|
||||
On a real checkbox with `role="switch"`, so it is focusable, announced as on or off, and takes
|
||||
Space from the keyboard; the handle is drawn beside it. `label` and `hint` sit beside it and are
|
||||
its name — a switch without `label` needs an `aria-label`. `right` puts it at the end of the
|
||||
row, where a setting's switch usually is. `icons` draws a check on the handle when on and a
|
||||
cross when off; `icons="selected"` only the check. Every other attribute reaches the `<input>`
|
||||
(resources/css/components/selection.css). --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
'hint' => null,
|
||||
'right' => false,
|
||||
'icons' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$model = $attributes->whereStartsWith('wire:model')->first();
|
||||
$id = $attributes->get('id') ?? 'switch-'.substr(md5($model.'|'.$label), 0, 12);
|
||||
$icons = $icons === 'selected' ? 'selected' : ($icons ? 'both' : null);
|
||||
@endphp
|
||||
|
||||
<div {{ $attributes->only(['class', 'wire:key'])->class(['min-w-0']) }}>
|
||||
<label for="{{ $id }}" data-selection @class([
|
||||
'flex gap-4',
|
||||
'items-start' => filled($hint),
|
||||
'items-center' => blank($hint),
|
||||
'flex-row-reverse justify-between' => $right,
|
||||
])>
|
||||
<span data-switch @if ($icons) data-icons="{{ $icons }}" @endif>
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'wire:key']) }}
|
||||
id="{{ $id }}"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
/>
|
||||
<span data-handle>
|
||||
@if ($icons)
|
||||
<x-icon name="check" class="size-4" data-on />
|
||||
<x-icon name="close" class="size-4" data-off />
|
||||
@endif
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@if (filled($label) || filled($hint))
|
||||
<span class="min-w-0">
|
||||
@if (filled($label))
|
||||
<span class="block type-body-lg text-on-surface" data-selection-label>{{ $label }}</span>
|
||||
@endif
|
||||
@if (filled($hint))
|
||||
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $hint }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</label>
|
||||
</div>
|
||||
Reference in New Issue
Block a user