Merge branch 'worktree-agent-a41c756369d016a29'
This commit is contained in:
@@ -4,19 +4,20 @@
|
||||
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
|
||||
partly ticked: HTML has no attribute for it, so the input is marked `data-md-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).
|
||||
read from the bag under the `wire:model` name. Every other attribute reaches the `<input>`.
|
||||
The drawing is resources/css/components/checkbox.css, on the row the three selection controls
|
||||
share (resources/css/components/selection.css).
|
||||
|
||||
Without a label — a "select all" in a table header, a row's tick — the row is only the 18px box,
|
||||
so the box carries `touch-target` and catches presses over M3's 48px minimum.
|
||||
so the box carries `md-touch-target` and catches presses over M3's 48px minimum.
|
||||
|
||||
Grouping is the caller's, not this component's: M3 asks that from `expanded` (840px) a set of
|
||||
related checkboxes be gathered into a contained region rather than left as one long column
|
||||
(docs/reference/m3/components-navigation-selection-inputs.md § Checkbox, Behaviour). Lay the
|
||||
group out yourself — `<div class="grid gap-4 expanded:grid-cols-2">` around the checkboxes, or a
|
||||
`<x-card>` or side sheet holding them — and give the group a heading that names what it asks. --}}
|
||||
group out yourself — a grid of one column that takes two from `expanded`, or an `<x-card>` or
|
||||
side sheet holding them — and give the group a heading that names what it asks. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
@@ -33,42 +34,34 @@
|
||||
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
|
||||
@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),
|
||||
'touch-target' => blank($label) && blank($hint),
|
||||
])>
|
||||
<div data-md-checkbox {{ $attributes->only(['class', 'style', 'wire:key']) }}>
|
||||
<label for="{{ $id }}" data-md-selection-row @if ($right) data-md-right @endif @if ($messages !== []) data-md-invalid @endif>
|
||||
<span data-md-checkbox-box @if (blank($label) && blank($hint)) class="md-touch-target" @endif>
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'wire:key']) }}
|
||||
{{ $attributes->except(['class', 'style', 'id', 'wire:key']) }}
|
||||
id="{{ $id }}"
|
||||
type="checkbox"
|
||||
@if ($indeterminate) data-indeterminate @endif
|
||||
@if ($indeterminate) data-md-indeterminate @endif
|
||||
@if ($messages !== []) aria-invalid="true" aria-describedby="{{ $id }}-support" @endif
|
||||
/>
|
||||
<x-livewire-material::icon name="check" class="size-4.5" optical="20" data-check />
|
||||
<x-livewire-material::icon name="remove" class="size-4.5" optical="20" data-mixed />
|
||||
<x-livewire-material::icon name="check" size="18" data-md-checkbox-check />
|
||||
<x-livewire-material::icon name="remove" size="18" data-md-checkbox-mixed />
|
||||
</span>
|
||||
|
||||
@if (filled($label) || filled($hint))
|
||||
<span class="min-w-0">
|
||||
<span data-md-selection-text>
|
||||
@if (filled($label))
|
||||
<span class="block type-body-lg text-on-surface" data-selection-label>{{ $label }}</span>
|
||||
<span data-md-selection-label>{{ $label }}</span>
|
||||
@endif
|
||||
@if (filled($hint))
|
||||
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $hint }}</span>
|
||||
<span data-md-selection-hint>{{ $hint }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</label>
|
||||
|
||||
@if ($messages !== [])
|
||||
<div id="{{ $id }}-support" class="mt-1 type-body-sm text-error" role="alert">
|
||||
<div id="{{ $id }}-support" data-md-selection-support role="alert">
|
||||
@foreach ($messages as $message)
|
||||
<p>{{ $message }}</p>
|
||||
@endforeach
|
||||
|
||||
@@ -19,7 +19,11 @@
|
||||
|
||||
The set is one tab stop and the arrow keys move between the chips inside it, with Home and End
|
||||
at the ends — M3's chip keyboard table. Backspace and Delete still remove a focused input
|
||||
chip. --}}
|
||||
chip.
|
||||
|
||||
The root renders `data-md-chip-set` (and `data-md-scroll`), the row `data-md-chip-set-row`, which
|
||||
resources/js/chips.js marks `data-md-scroll-start` and `data-md-scroll-end`; the drawing is
|
||||
resources/css/components/chip-set.css. `class` and every other attribute land on the root. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
@@ -37,24 +41,25 @@
|
||||
@endphp
|
||||
|
||||
<div
|
||||
data-md-chip-set
|
||||
@if ($scroll) data-md-scroll @endif
|
||||
role="group"
|
||||
@if (filled($label)) aria-labelledby="material-chip-set-{{ $key }}-label" @endif
|
||||
@if ($describedBy) aria-describedby="{{ $describedBy }}" @endif
|
||||
{{ $attributes->class('min-w-0') }}
|
||||
{{ $attributes }}
|
||||
>
|
||||
@if (filled($label))
|
||||
<p id="material-chip-set-{{ $key }}-label" class="mb-2 type-label-lg text-on-surface-variant">{{ $label }}</p>
|
||||
<p id="material-chip-set-{{ $key }}-label" data-md-chip-set-label>{{ $label }}</p>
|
||||
@endif
|
||||
|
||||
@if ($scroll)
|
||||
<div x-data="materialChipSet" class="relative">
|
||||
<div x-data="materialChipSet" data-md-chip-set-scroller>
|
||||
<div
|
||||
data-chip-set
|
||||
data-md-chip-set-row
|
||||
x-ref="row"
|
||||
x-on:keydown="key($event)"
|
||||
x-on:focusin="rove($event.target)"
|
||||
wire:ignore.self
|
||||
class="peer -mx-1.5 -my-2 flex scroll-px-6 gap-2 overflow-x-auto px-1.5 py-2 pointer-fine:scroll-px-10 [scrollbar-width:none] [--chip-fade-end:0px] [--chip-fade-start:0px] data-scroll-end:[--chip-fade-end:1.5rem] data-scroll-start:[--chip-fade-start:1.5rem] [mask-image:linear-gradient(to_right,transparent,#000_var(--chip-fade-start),#000_calc(100%_-_var(--chip-fade-end)),transparent)] rtl:[mask-image:linear-gradient(to_left,transparent,#000_var(--chip-fade-start),#000_calc(100%_-_var(--chip-fade-end)),transparent)]"
|
||||
>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@@ -65,31 +70,26 @@
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
data-chip-scroll="{{ $edge }}"
|
||||
data-md-chip-scroll="{{ $edge }}"
|
||||
x-on:click="nudge('{{ $edge }}')"
|
||||
@class([
|
||||
'absolute inset-y-0 my-auto hidden size-8 place-items-center rounded-corner-full bg-surface-container-high text-on-surface-variant shadow-elevation-1',
|
||||
'start-0 peer-data-scroll-start:pointer-fine:grid' => $edge === 'start',
|
||||
'end-0 peer-data-scroll-end:pointer-fine:grid' => $edge === 'end',
|
||||
])
|
||||
>
|
||||
<x-livewire-material::icon :name="$glyph" class="size-4.5 rtl:-scale-x-100" optical="20" />
|
||||
<x-livewire-material::icon :name="$glyph" size="18" mirror-rtl />
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div data-chip-set x-data="materialChipSet" x-on:keydown="key($event)" x-on:focusin="rove($event.target)" class="flex flex-wrap gap-2">
|
||||
<div data-md-chip-set-row x-data="materialChipSet" x-on:keydown="key($event)" x-on:focusin="rove($event.target)">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($messages !== [])
|
||||
<div id="{{ $describedBy }}">
|
||||
<div id="{{ $describedBy }}" data-md-chip-set-errors>
|
||||
@foreach ($messages as $message)
|
||||
<p class="mt-1 type-body-sm text-error">{{ $message }}</p>
|
||||
<p>{{ $message }}</p>
|
||||
@endforeach
|
||||
</div>
|
||||
@elseif (filled($hint))
|
||||
<p id="{{ $describedBy }}" class="mt-1 type-body-sm text-on-surface-variant">{{ $hint }}</p>
|
||||
<p id="{{ $describedBy }}" data-md-chip-set-hint>{{ $hint }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,11 @@
|
||||
button each catch presses over 48×48, which M3 asks for even though it means reaching past the
|
||||
chip and over the label's own strip ("the target may extend beyond the visible chip
|
||||
container"). The check grows in on the fast spatial spring and fades in on the slow effects
|
||||
one; it shrinks on default effects and fades on fast effects, as AnimatingChipContent does. --}}
|
||||
one; it shrinks on default effects and fades on fast effects, as AnimatingChipContent does.
|
||||
|
||||
The chip renders `data-md-chip` with its type (`data-md-chip="filter"`), `data-md-elevated`,
|
||||
`data-md-disabled` and, on an input chip, `data-md-selected`; its colours, sizes and states are
|
||||
resources/css/components/chip.css. --}}
|
||||
|
||||
@props([
|
||||
'type' => 'assist',
|
||||
@@ -87,44 +91,21 @@
|
||||
|
||||
$outer = ['class', 'style', 'wire:key'];
|
||||
|
||||
// Compose draws the 1px border inside the chip; a CSS border takes room, so the padding
|
||||
// beside it is 1px short of the token (7px for 8, 15px for 16).
|
||||
|
||||
$base = 'group/chip inline-flex h-8 max-w-full shrink-0 select-none items-center whitespace-nowrap rounded-corner-sm border type-label-lg transition-[background-color,border-color,color,box-shadow] duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast';
|
||||
// Below 48px the touch target reaches past the chip, as M3 requires.
|
||||
$target = 'after:absolute after:inset-x-0 after:top-1/2 after:h-12 after:-translate-y-1/2';
|
||||
|
||||
// Every colour class written out whole, so Tailwind compiles it. Selected is a state the browser
|
||||
// owns on a filter chip (a checked checkbox, or aria-pressed), so it is written as a variant.
|
||||
$colours = match (true) {
|
||||
$kind === 'filter' && $raised && $disabled => 'border-transparent bg-on-surface/12 text-on-surface/38',
|
||||
$kind === 'filter' && $raised => 'border-transparent bg-surface-container-low text-on-surface-variant shadow-elevation-1 hover:shadow-elevation-2 has-checked:bg-secondary-container has-checked:text-on-secondary-container aria-pressed:bg-secondary-container aria-pressed:text-on-secondary-container',
|
||||
$kind === 'filter' && $disabled => 'border-on-surface/12 text-on-surface/38 has-checked:border-transparent has-checked:bg-on-surface/12 aria-pressed:border-transparent aria-pressed:bg-on-surface/12',
|
||||
$kind === 'filter' => 'border-outline-variant text-on-surface-variant focus-visible:border-on-surface-variant has-focus-visible:border-on-surface-variant has-checked:border-transparent has-checked:bg-secondary-container has-checked:text-on-secondary-container hover:has-checked:shadow-elevation-1 aria-pressed:border-transparent aria-pressed:bg-secondary-container aria-pressed:text-on-secondary-container hover:aria-pressed:shadow-elevation-1',
|
||||
$kind === 'input' && $disabled => $selected ? 'border-transparent bg-on-surface/12 text-on-surface/38' : 'border-on-surface/12 text-on-surface/38',
|
||||
$kind === 'input' => $selected ? 'border-transparent bg-secondary-container text-on-secondary-container' : 'border-outline-variant text-on-surface-variant has-[[data-chip-action]:focus-visible]:border-on-surface-variant',
|
||||
$raised && $disabled => 'border-transparent bg-on-surface/12 text-on-surface/38',
|
||||
$raised => $kind === 'suggestion'
|
||||
? 'border-transparent bg-surface-container-low text-on-surface-variant shadow-elevation-1 hover:shadow-elevation-2'
|
||||
: 'border-transparent bg-surface-container-low text-on-surface shadow-elevation-1 hover:shadow-elevation-2',
|
||||
$disabled => 'border-on-surface/12 text-on-surface/38',
|
||||
default => $kind === 'suggestion'
|
||||
? 'border-outline-variant text-on-surface-variant focus-visible:border-on-surface-variant'
|
||||
: 'border-outline-variant text-on-surface focus-visible:border-on-surface',
|
||||
};
|
||||
|
||||
// Leading icons: primary on assist, suggestion and filter chips; on-surface-variant on an input
|
||||
// chip, primary while it is hovered, focused or pressed, and primary on a selected one.
|
||||
// An input chip's leading icon turns primary while a chip with an action of its own is hovered,
|
||||
// focused or pressed.
|
||||
$clicks = $attributes->whereStartsWith(['wire:click', 'x-on:click', '@click'])->isNotEmpty();
|
||||
$leadingInk = match (true) {
|
||||
$disabled => null,
|
||||
$kind !== 'input', (bool) $selected => 'text-primary',
|
||||
$isLink || $clicks => 'transition-colors duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast group-hover/chip:text-primary group-active/chip:text-primary group-has-focus-visible/chip:text-primary',
|
||||
default => null,
|
||||
};
|
||||
$trailingInk = in_array($kind, ['assist', 'suggestion'], true) && ! $disabled ? 'text-primary' : null;
|
||||
$actionable = $kind === 'input' && ! $disabled && ! $selected && ($isLink || $clicks);
|
||||
|
||||
$check = 'size-4.5 opacity-0 transition-opacity duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast group-has-checked/chip:opacity-100 group-has-checked/chip:duration-(--md-sys-motion-effects-slow-duration) group-has-checked/chip:ease-effects-slow group-aria-pressed/chip:opacity-100 group-aria-pressed/chip:duration-(--md-sys-motion-effects-slow-duration) group-aria-pressed/chip:ease-effects-slow';
|
||||
$state = fn (array $more = []): array => array_filter([
|
||||
'data-md-chip' => $kind,
|
||||
'data-md-elevated' => $raised ? '' : null,
|
||||
'data-md-disabled' => $disabled ? '' : null,
|
||||
'data-md-selected' => $kind === 'input' && $selected ? '' : null,
|
||||
'data-md-actionable' => $actionable ? '' : null,
|
||||
'data-md-icon-start' => filled($icon) || ($kind === 'input' && filled($avatar)) ? '' : null,
|
||||
'data-md-icon-end' => filled($iconRight) && $kind !== 'input' ? '' : null,
|
||||
...$more,
|
||||
], fn ($attribute): bool => $attribute !== null);
|
||||
|
||||
if ($kind === 'input') {
|
||||
$actionTag = match (true) {
|
||||
@@ -132,7 +113,6 @@
|
||||
$clicks || $selected !== null => 'button',
|
||||
default => 'span',
|
||||
};
|
||||
$interactive = $actionTag !== 'span';
|
||||
$removeExpression = implode('; ', array_filter([
|
||||
'removeChip($event)',
|
||||
filled($remove) ? $remove : null,
|
||||
@@ -140,30 +120,15 @@
|
||||
]));
|
||||
|
||||
$container = $attributes->only($outer)
|
||||
->class([
|
||||
$base,
|
||||
'relative isolate',
|
||||
$colours,
|
||||
'has-[[data-chip-action]:focus-visible]:outline-3 has-[[data-chip-action]:focus-visible]:outline-offset-2 has-[[data-chip-action]:focus-visible]:outline-secondary',
|
||||
])
|
||||
->merge(array_filter(['style' => $anchor ? "anchor-name: {$anchor}" : null]));
|
||||
->merge($state([
|
||||
'data-md-avatar' => filled($avatar) ? '' : null,
|
||||
'data-md-removable' => $removable ? '' : null,
|
||||
'style' => $anchor ? "anchor-name: {$anchor}" : null,
|
||||
]));
|
||||
|
||||
$action = $attributes->except($outer)
|
||||
->class([
|
||||
'flex h-full min-w-0 items-center outline-none',
|
||||
match (true) {
|
||||
filled($avatar) => 'ps-0.75',
|
||||
filled($icon) => 'ps-1.75',
|
||||
default => 'ps-2.75',
|
||||
},
|
||||
$removable ? 'pe-2' : 'pe-2.75',
|
||||
'cursor-pointer before:absolute before:-inset-px before:-z-10 before:rounded-corner-sm before:bg-current before:opacity-0 before:transition-opacity before:duration-(--md-sys-motion-effects-fast-duration) before:ease-effects-fast hover:before:opacity-8 focus-visible:before:opacity-10 active:before:opacity-10' => $interactive && ! $disabled,
|
||||
$target => $interactive,
|
||||
'cursor-not-allowed' => $interactive && $disabled,
|
||||
'pointer-events-none' => $inert,
|
||||
])
|
||||
->merge(array_filter([
|
||||
'data-chip-action' => $interactive ? true : null,
|
||||
'data-md-chip-action' => '',
|
||||
'href' => $isLink ? $link : null,
|
||||
'target' => $isLink && $external ? '_blank' : null,
|
||||
'rel' => $isLink && $external ? 'noopener' : null,
|
||||
@@ -187,25 +152,12 @@
|
||||
'value' => $value,
|
||||
'checked' => $checked ? true : null,
|
||||
'disabled' => $disabled ? true : null,
|
||||
], fn ($attribute): bool => $attribute !== null))->class('peer sr-only')
|
||||
'class' => 'md-visually-hidden',
|
||||
], fn ($attribute): bool => $attribute !== null))
|
||||
: null;
|
||||
|
||||
$container = ($checkbox ? $attributes->only($outer) : $attributes)
|
||||
->class([
|
||||
$base,
|
||||
'state-layer relative',
|
||||
'focus-ring' => ! $checkbox,
|
||||
'has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-secondary has-focus-visible:before:opacity-10' => $checkbox,
|
||||
$kind === 'filter' || filled($icon) ? 'ps-1.75' : 'ps-3.75',
|
||||
$iconRight ? 'pe-1.75' : 'pe-3.75',
|
||||
$colours,
|
||||
$target,
|
||||
'cursor-pointer' => ! $disabled,
|
||||
'cursor-not-allowed' => $disabled && ! $isLink,
|
||||
'pointer-events-none' => $inert,
|
||||
])
|
||||
->merge(array_filter([
|
||||
'data-chip' => $kind,
|
||||
->merge($state(array_filter([
|
||||
'href' => $isLink ? $link : null,
|
||||
'target' => $isLink && $external ? '_blank' : null,
|
||||
'rel' => $isLink && $external ? 'noopener' : null,
|
||||
@@ -218,42 +170,35 @@
|
||||
'value' => $tag === 'button' ? $value : null,
|
||||
'aria-pressed' => $kind === 'filter' && ! $checkbox ? ($selected ? 'true' : 'false') : null,
|
||||
'style' => $anchor ? "anchor-name: {$anchor}" : null,
|
||||
], fn ($attribute): bool => $attribute !== null));
|
||||
], fn ($attribute): bool => $attribute !== null)));
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if ($kind === 'input')
|
||||
<span data-chip="input" @if ($removable) x-data="materialChip" x-on:keydown="removeOnKey($event)" @endif {{ $container }}>
|
||||
<span @if ($removable) x-data="materialChip" x-on:keydown="removeOnKey($event)" @endif {{ $container }}>
|
||||
<{{ $actionTag }} {{ $action }}>
|
||||
@if ($avatar)
|
||||
@if ($initials)
|
||||
<span aria-hidden="true" @class(['me-2 grid size-6 shrink-0 place-items-center rounded-corner-full bg-primary-container type-label-sm text-on-primary-container', 'opacity-38' => $disabled])>{{ $avatar }}</span>
|
||||
<span aria-hidden="true" data-md-chip-avatar>{{ $avatar }}</span>
|
||||
@else
|
||||
<img src="{{ $avatar }}" alt="" @class(['me-2 size-6 shrink-0 rounded-corner-full object-cover', 'opacity-38' => $disabled]) />
|
||||
<img src="{{ $avatar }}" alt="" data-md-chip-avatar />
|
||||
@endif
|
||||
@elseif ($icon)
|
||||
<x-livewire-material::icon :name="$icon" optical="20" :class="\Illuminate\Support\Arr::toCssClasses(['me-2 size-4.5', $leadingInk])" />
|
||||
<x-livewire-material::icon :name="$icon" size="18" data-md-chip-icon />
|
||||
@endif
|
||||
|
||||
<span data-chip-label class="truncate">{{ $label ?? $slot }}</span>
|
||||
<span data-md-chip-label>{{ $label ?? $slot }}</span>
|
||||
</{{ $actionTag }}>
|
||||
|
||||
@if ($removable)
|
||||
<button
|
||||
type="button"
|
||||
@if (filled($text)) data-chip-remove aria-label="{{ __('Remove :label', ['label' => $text]) }}" @else data-chip-remove="{{ __('Remove :label') }}" @endif
|
||||
@if (filled($text)) data-md-chip-remove aria-label="{{ __('Remove :label', ['label' => $text]) }}" @else data-md-chip-remove="{{ __('Remove :label') }}" @endif
|
||||
@if ($wireRemove) wire:click="{{ $wireRemove }}" @endif
|
||||
x-on:click="{{ $removeExpression }}"
|
||||
@disabled($disabled)
|
||||
@class([
|
||||
'relative me-1.75 grid size-4.5 shrink-0 place-items-center rounded-corner-full',
|
||||
'cursor-pointer focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-secondary' => ! $disabled,
|
||||
'before:absolute before:-inset-0.75 before:rounded-corner-full before:bg-current before:opacity-0 before:transition-opacity before:duration-(--md-sys-motion-effects-fast-duration) before:ease-effects-fast hover:before:opacity-8 focus-visible:before:opacity-10 active:before:opacity-10' => ! $disabled,
|
||||
'after:absolute after:-inset-3.75',
|
||||
'cursor-not-allowed' => $disabled,
|
||||
])
|
||||
>
|
||||
<x-livewire-material::icon name="close" class="size-4.5" optical="20" />
|
||||
<x-livewire-material::icon name="close" size="18" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@@ -273,24 +218,20 @@
|
||||
|
||||
@if ($kind === 'filter')
|
||||
{{-- The check grows in from nothing; beside an icon it takes the icon's place. --}}
|
||||
<span @class([
|
||||
'me-2 grid shrink-0 overflow-hidden *:col-start-1 *:row-start-1',
|
||||
'size-4.5' => filled($icon),
|
||||
'h-4.5 w-0 transition-[width] duration-(--md-sys-motion-effects-default-duration) ease-effects-default group-has-checked/chip:w-4.5 group-has-checked/chip:duration-(--md-sys-motion-spatial-fast-duration) group-has-checked/chip:ease-spatial-fast group-aria-pressed/chip:w-4.5 group-aria-pressed/chip:duration-(--md-sys-motion-spatial-fast-duration) group-aria-pressed/chip:ease-spatial-fast' => blank($icon),
|
||||
])>
|
||||
<span data-md-chip-check-slot>
|
||||
@if ($icon)
|
||||
<x-livewire-material::icon :name="$icon" optical="20" :class="\Illuminate\Support\Arr::toCssClasses(['size-4.5 transition-opacity duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast group-has-checked/chip:opacity-0 group-aria-pressed/chip:opacity-0', $leadingInk])" />
|
||||
<x-livewire-material::icon :name="$icon" size="18" data-md-chip-icon />
|
||||
@endif
|
||||
<x-livewire-material::icon name="check" data-chip-check optical="20" :class="$check" />
|
||||
<x-livewire-material::icon name="check" size="18" data-md-chip-check />
|
||||
</span>
|
||||
@elseif ($icon)
|
||||
<x-livewire-material::icon :name="$icon" optical="20" :class="\Illuminate\Support\Arr::toCssClasses(['me-2 size-4.5', $leadingInk])" />
|
||||
<x-livewire-material::icon :name="$icon" size="18" data-md-chip-icon />
|
||||
@endif
|
||||
|
||||
<span class="truncate">{{ $label ?? $slot }}</span>
|
||||
<span data-md-chip-label>{{ $label ?? $slot }}</span>
|
||||
|
||||
@if ($iconRight)
|
||||
<x-livewire-material::icon :name="$iconRight" optical="20" :class="\Illuminate\Support\Arr::toCssClasses(['ms-2 size-4.5', $trailingInk])" />
|
||||
<x-livewire-material::icon :name="$iconRight" size="18" data-md-chip-trailing />
|
||||
@endif
|
||||
|
||||
@if ($tooltip !== null)
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
string: a list of integers stays a list of integers. Without `wire:model` it works on its own
|
||||
and binds with `x-model`. Errors for the property and its items replace the hint. When the
|
||||
options change on the server, give the component a `wire:key` that changes with them: the
|
||||
options are baked into its Alpine state. --}}
|
||||
options are baked into its Alpine state.
|
||||
|
||||
The root renders `data-md-choices` (and `data-md-searchable`); the chips are `<x-chip-set>`'s, the
|
||||
field `<x-field>`'s, and the open list M3's menu (resources/css/components/menu.css, as
|
||||
`data-md-field-menu` and `data-md-field-option`). resources/css/components/choices.css brings
|
||||
them together. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
@@ -64,7 +69,9 @@
|
||||
|
||||
@if ($searchable)
|
||||
<div
|
||||
{{ $attributes->only(['class', 'wire:key', 'x-model']) }}
|
||||
data-md-choices
|
||||
data-md-searchable
|
||||
{{ $attributes->only(['class', 'style', 'wire:key', 'x-model']) }}
|
||||
x-data="{
|
||||
@if ($model !== null) value: @entangle($attributes->wire('model')), @else value: @js($current), @endif
|
||||
options: @js($choices),
|
||||
@@ -141,7 +148,7 @@
|
||||
@if ($messages !== []) aria-invalid="true" @endif
|
||||
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif
|
||||
placeholder="{{ filled($placeholder) ? $placeholder : ' ' }}"
|
||||
class="field-control"
|
||||
data-md-field-control
|
||||
x-model="query"
|
||||
x-on:focus="show(); $nextTick(() => $el.select())"
|
||||
x-on:pointerdown="selecting = document.activeElement !== $el"
|
||||
@@ -158,7 +165,7 @@
|
||||
/>
|
||||
|
||||
<x-slot:trailing>
|
||||
<x-livewire-material::icon name="arrow_drop_down" class="field-trailing field-arrow size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="arrow_drop_down" data-md-field-trailing data-md-field-arrow />
|
||||
</x-slot:trailing>
|
||||
</x-livewire-material::field>
|
||||
</div>
|
||||
@@ -170,7 +177,7 @@
|
||||
x-ref="list"
|
||||
@if (filled($label)) aria-label="{{ $label }}" @endif
|
||||
style="position-anchor: {{ $anchor }}"
|
||||
class="field-menu"
|
||||
data-md-field-menu
|
||||
>
|
||||
<template x-for="(option, index) in filtered" :key="option.value">
|
||||
<li
|
||||
@@ -181,18 +188,19 @@
|
||||
x-bind:data-active="index === active ? '' : null"
|
||||
x-on:mousedown.prevent="choose(option)"
|
||||
x-on:mousemove="active = index"
|
||||
class="field-option"
|
||||
data-md-field-option
|
||||
>
|
||||
<span x-text="option.label"></span>
|
||||
<x-livewire-material::icon name="check" class="field-check size-6" />
|
||||
<x-livewire-material::icon name="check" data-md-field-check />
|
||||
</li>
|
||||
</template>
|
||||
<li x-show="filtered.length === 0" class="px-4 py-3 type-body-md text-on-surface-variant">{{ __('Nothing matches') }}</li>
|
||||
<li x-show="filtered.length === 0" data-md-choices-empty>{{ __('Nothing matches') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
@else
|
||||
<div
|
||||
{{ $attributes->only(['class', 'wire:key', 'x-model']) }}
|
||||
data-md-choices
|
||||
{{ $attributes->only(['class', 'style', 'wire:key', 'x-model']) }}
|
||||
x-data="{
|
||||
@if ($model !== null) selection: @entangle($attributes->wire('model')), @else selection: @js($current ?? ($single ? null : [])), @endif
|
||||
options: @js(array_column($choices, 'value')),
|
||||
|
||||
@@ -142,6 +142,8 @@
|
||||
'invalidRange' => __('Invalid date range input'),
|
||||
],
|
||||
];
|
||||
|
||||
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||||
@endphp
|
||||
|
||||
<div
|
||||
@@ -162,9 +164,9 @@
|
||||
:$icon
|
||||
:$size
|
||||
:$variant
|
||||
:data-invalid="$invalid ? '' : null"
|
||||
:data-readonly="$attributes->get('readonly') ? '' : null"
|
||||
x-bind:data-invalid="(fieldError !== '' || {{ $invalid ? 'true' : 'false' }}) ? '' : null"
|
||||
:data-md-invalid="$invalid ? '' : null"
|
||||
:data-md-readonly="$attributes->get('readonly') ? '' : null"
|
||||
x-bind:data-md-invalid="(fieldError !== '' || {{ $invalid ? 'true' : 'false' }}) ? '' : null"
|
||||
>
|
||||
<input
|
||||
{{ $attributes->whereDoesntStartWith('wire:model')->except(['class', 'id', 'wire:key', 'x-model', 'name', 'value', 'placeholder', 'type']) }}
|
||||
@@ -182,7 +184,7 @@
|
||||
autocomplete="off"
|
||||
value="{{ $display }}"
|
||||
placeholder=" "
|
||||
class="field-control"
|
||||
data-md-field-control
|
||||
x-model="text"
|
||||
@if ($mode === 'docked')
|
||||
x-bind:placeholder="placeholder"
|
||||
@@ -205,19 +207,21 @@
|
||||
@if ($clearable)
|
||||
<button
|
||||
type="button"
|
||||
class="field-trailing field-clear field-button"
|
||||
aria-label="{{ __('Clear') }}"
|
||||
x-on:click="clear()"
|
||||
@disabled($attributes->get('disabled') || $attributes->get('readonly'))
|
||||
data-field-clear
|
||||
data-md-field-trailing
|
||||
data-md-field-button
|
||||
data-md-field-clear
|
||||
>
|
||||
<x-livewire-material::icon name="close" class="size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="close" :size="$fieldIconSize" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="field-trailing field-button"
|
||||
data-md-field-trailing
|
||||
data-md-field-button
|
||||
aria-label="{{ $title }}"
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="{{ $id }}-picker"
|
||||
@@ -227,7 +231,7 @@
|
||||
@disabled($attributes->get('disabled') || $attributes->get('readonly'))
|
||||
data-datepicker-toggle
|
||||
>
|
||||
<x-livewire-material::icon name="calendar_today" class="size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="calendar_today" :size="$fieldIconSize" />
|
||||
</button>
|
||||
</x-slot:trailing>
|
||||
</x-livewire-material::field>
|
||||
@@ -462,7 +466,7 @@
|
||||
|
||||
<div x-show="typing" data-datepicker-entry>
|
||||
<div @if ($range) data-range @endif data-datepicker-entry-fields>
|
||||
<x-livewire-material::field id="{{ $id }}-entry" :label="$range ? __('Start date') : __('Date')" :$variant x-bind:data-invalid="entryError !== '' ? '' : null">
|
||||
<x-livewire-material::field id="{{ $id }}-entry" :label="$range ? __('Start date') : __('Date')" :$variant x-bind:data-md-invalid="entryError !== '' ? '' : null">
|
||||
<input
|
||||
id="{{ $id }}-entry"
|
||||
type="text"
|
||||
@@ -475,12 +479,12 @@
|
||||
x-model="entry"
|
||||
x-on:input="typeEntry()"
|
||||
x-on:keydown.enter.prevent="confirm()"
|
||||
class="field-control"
|
||||
data-md-field-control
|
||||
/>
|
||||
</x-livewire-material::field>
|
||||
|
||||
@if ($range)
|
||||
<x-livewire-material::field id="{{ $id }}-entry-end" :label="__('End date')" :$variant x-bind:data-invalid="entryError !== '' ? '' : null">
|
||||
<x-livewire-material::field id="{{ $id }}-entry-end" :label="__('End date')" :$variant x-bind:data-md-invalid="entryError !== '' ? '' : null">
|
||||
<input
|
||||
id="{{ $id }}-entry-end"
|
||||
type="text"
|
||||
@@ -492,7 +496,7 @@
|
||||
x-model="entryEnd"
|
||||
x-on:input="typeEntry()"
|
||||
x-on:keydown.enter.prevent="confirm()"
|
||||
class="field-control"
|
||||
data-md-field-control
|
||||
/>
|
||||
</x-livewire-material::field>
|
||||
@endif
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{{-- 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
|
||||
and the pickers wrap their control in it, so a form reads as one family. A custom control the
|
||||
package does not have goes in the slot marked `data-md-field-control`. 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.
|
||||
decides is `data-md-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
|
||||
@@ -12,7 +13,8 @@
|
||||
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, 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.
|
||||
trails the field with something of its own, or the field is `xs` and has no room. The field's
|
||||
icons are 24px, 20px in an `sm` field and 16px in an `xs` one.
|
||||
|
||||
`counter` is the maximum number of characters, and puts M3's character counter at the end of the
|
||||
supporting-text row: `n/max`, counted from the control on every `input`, in the error colour once
|
||||
@@ -21,12 +23,12 @@
|
||||
"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.
|
||||
From `medium` (600px) 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 width rule 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. --}}
|
||||
a width is safe here; `hint-class` lands on the hint. --}}
|
||||
|
||||
@props([
|
||||
'id',
|
||||
@@ -50,6 +52,7 @@
|
||||
? $variant
|
||||
: (config('livewire-material.fields.variant') === 'filled' ? 'filled' : 'outlined');
|
||||
$density = in_array($size, ['sm', 'xs'], true) ? $size : 'md';
|
||||
$iconSize = ['md' => 24, 'sm' => 20, 'xs' => 16][$density];
|
||||
// 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';
|
||||
@@ -57,67 +60,67 @@
|
||||
@endphp
|
||||
|
||||
<div
|
||||
{{ $attributes->class(['field']) }}
|
||||
data-variant="{{ $variant }}"
|
||||
data-size="{{ $density }}"
|
||||
@if ($messages !== []) data-invalid @endif
|
||||
@if ($floated) data-floated @endif
|
||||
@if ($mono) data-mono @endif
|
||||
@if ($full) data-full @endif
|
||||
data-md-field
|
||||
data-md-variant="{{ $variant }}"
|
||||
data-md-size="{{ $density }}"
|
||||
@if ($messages !== []) data-md-invalid @endif
|
||||
@if ($floated) data-md-floated @endif
|
||||
@if ($mono) data-md-mono @endif
|
||||
@if ($full) data-md-full @endif
|
||||
{{ $attributes }}
|
||||
>
|
||||
<div class="field-box">
|
||||
<div data-md-field-box>
|
||||
@if ($icon)
|
||||
<x-livewire-material::icon :name="$icon" class="field-icon size-(--field-icon)" />
|
||||
<x-livewire-material::icon :name="$icon" :size="$iconSize" data-md-field-icon />
|
||||
@endif
|
||||
|
||||
@if (filled($prefix))
|
||||
<span class="field-affix">{{ $prefix }}</span>
|
||||
<span data-md-field-affix>{{ $prefix }}</span>
|
||||
@endif
|
||||
|
||||
{{ $slot }}
|
||||
|
||||
@if (filled($suffix))
|
||||
<span class="field-affix">{{ $suffix }}</span>
|
||||
<span data-md-field-affix>{{ $suffix }}</span>
|
||||
@endif
|
||||
|
||||
{{ $trailing ?? '' }}
|
||||
|
||||
@if ($errorIcon)
|
||||
<x-livewire-material::icon name="error" class="field-trailing field-error size-(--field-icon)" :label="__('Error')" />
|
||||
<x-livewire-material::icon name="error" :size="$iconSize" data-md-field-trailing data-md-field-error :label="__('Error')" />
|
||||
@endif
|
||||
|
||||
<fieldset aria-hidden="true" class="field-outline">
|
||||
<fieldset aria-hidden="true" data-md-field-outline>
|
||||
<legend>@if (filled($label))<span>{{ $label }}</span>@endif</legend>
|
||||
</fieldset>
|
||||
|
||||
@if (filled($label))
|
||||
<label for="{{ $id }}" class="field-label">{{ $label }}</label>
|
||||
<label for="{{ $id }}" data-md-field-label>{{ $label }}</label>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($messages !== [] || filled($hint) || $counter !== null)
|
||||
<div class="field-support-row">
|
||||
<div data-md-field-support-row>
|
||||
@if ($messages !== [])
|
||||
<div id="{{ $id }}-support" class="field-support" role="alert">
|
||||
<div id="{{ $id }}-support" data-md-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>
|
||||
<p id="{{ $id }}-support" data-md-field-support @if (filled($hintClass)) class="{{ $hintClass }}" @endif>{{ $hint }}</p>
|
||||
@endif
|
||||
|
||||
@if ($counter !== null)
|
||||
<span
|
||||
class="field-counter"
|
||||
data-field-counter
|
||||
data-md-field-counter
|
||||
x-data="{
|
||||
used: 0,
|
||||
max: {{ $counter }},
|
||||
spoken: '',
|
||||
settle: null,
|
||||
init() {
|
||||
const control = this.$el.closest('.field').querySelector('.field-control');
|
||||
const control = this.$el.closest('[data-md-field]').querySelector('[data-md-field-control]');
|
||||
|
||||
this.used = control.value.length;
|
||||
control.addEventListener('input', () => (this.used = control.value.length));
|
||||
@@ -131,10 +134,10 @@
|
||||
clearTimeout(this.settle);
|
||||
},
|
||||
}"
|
||||
x-bind:data-over="used > max"
|
||||
x-bind:data-md-over="used > max"
|
||||
>
|
||||
<span aria-hidden="true" x-text="`${used}/${max}`">0/{{ $counter }}</span>
|
||||
<span class="sr-only" aria-live="polite" aria-atomic="true" x-text="spoken"></span>
|
||||
<span class="md-visually-hidden" aria-live="polite" aria-atomic="true" x-text="spoken"></span>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
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).
|
||||
pill, so it reads as the thing to press (resources/css/components/file.css, on the field's
|
||||
chrome; the field's root carries `data-md-file`).
|
||||
|
||||
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
|
||||
@@ -28,13 +29,13 @@
|
||||
: [];
|
||||
@endphp
|
||||
|
||||
<x-livewire-material::field :$id :$label :$hint :$messages :$variant floated :class="$attributes->get('class')">
|
||||
<x-livewire-material::field :$id :$label :$hint :$messages :$variant floated :class="$attributes->get('class')" data-md-file>
|
||||
<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"
|
||||
data-md-field-control
|
||||
/>
|
||||
</x-livewire-material::field>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
{{-- 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. --}}
|
||||
16px 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
|
||||
attributes of its own (a `class` from the call site lands on the row untouched); `separator`
|
||||
draws a divider above it. The column is resources/css/components/form.css. --}}
|
||||
|
||||
@props([
|
||||
'separator' => false,
|
||||
])
|
||||
|
||||
<form {{ $attributes->class(['grid grid-cols-1 auto-rows-min gap-4']) }}>
|
||||
<form data-md-form {{ $attributes }}>
|
||||
{{ $slot }}
|
||||
|
||||
@isset($actions)
|
||||
@@ -19,7 +17,7 @@
|
||||
<x-livewire-material::divider />
|
||||
@endif
|
||||
|
||||
<div {{ $actions->attributes->class(['flex flex-wrap items-center justify-end gap-2']) }}>
|
||||
<div data-md-form-actions {{ $actions->attributes }}>
|
||||
{{ $actions }}
|
||||
</div>
|
||||
@endisset
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
|
||||
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. --}}
|
||||
shows only while the field has focus — until then the label stands where it would be.
|
||||
|
||||
The field's root carries `data-md-input`; everything it draws is the field's chrome
|
||||
(resources/css/components/field.css), which resources/css/components/input.css imports. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
@@ -39,16 +42,17 @@
|
||||
$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;
|
||||
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||||
@endphp
|
||||
|
||||
<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">
|
||||
<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-md-readonly="$attributes->get('readonly') ? '' : null" data-md-input>
|
||||
<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"
|
||||
data-md-field-control
|
||||
/>
|
||||
|
||||
@if ($clearable || $copyable || $iconRight)
|
||||
@@ -57,12 +61,13 @@
|
||||
<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"
|
||||
x-on:click="const control = $el.closest('[data-md-field]').querySelector('[data-md-field-control]'); control.value = ''; control.dispatchEvent(new Event('input', { bubbles: true })); control.dispatchEvent(new Event('change', { bubbles: true })); control.focus();"
|
||||
aria-label="{{ __('Clear') }}"
|
||||
data-field-clear
|
||||
data-md-field-trailing
|
||||
data-md-field-button
|
||||
data-md-field-clear
|
||||
>
|
||||
<x-livewire-material::icon name="close" class="size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="close" :size="$fieldIconSize" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@@ -70,17 +75,18 @@
|
||||
<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"
|
||||
x-on:click="navigator.clipboard.writeText($el.closest('[data-md-field]').querySelector('[data-md-field-control]').value).then(() => window.materialToast(@js(__('Copied to the clipboard')), { type: 'success' }))"
|
||||
aria-label="{{ __('Copy') }}"
|
||||
data-field-copy
|
||||
data-md-field-trailing
|
||||
data-md-field-button
|
||||
data-md-field-copy
|
||||
>
|
||||
<x-livewire-material::icon name="content_copy" class="size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="content_copy" :size="$fieldIconSize" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@if ($iconRight)
|
||||
<x-livewire-material::icon :name="$iconRight" class="field-trailing size-(--field-icon)" />
|
||||
<x-livewire-material::icon :name="$iconRight" :size="$fieldIconSize" data-md-field-trailing />
|
||||
@endif
|
||||
</x-slot:trailing>
|
||||
@endif
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
M3's own pattern for an interactive trailing icon ("Show password" / "Hide password") — and it
|
||||
carries no `aria-pressed`, which would say the state a second time and the other way round.
|
||||
`label`, `hint`, `icon`, `variant`, `size`; every other attribute reaches the `<input>`
|
||||
(`autocomplete="current-password"`, `wire:model`). --}}
|
||||
(`autocomplete="current-password"`, `wire:model`). The field's root carries
|
||||
`data-md-password`; the chrome and the eye's icon button are the field's
|
||||
(resources/css/components/field.css, brought by resources/css/components/password.css). --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
@@ -22,9 +24,10 @@
|
||||
// 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)) : [];
|
||||
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||||
@endphp
|
||||
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant :class="$attributes->get('class')" x-data="{ shown: false }">
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant :class="$attributes->get('class')" x-data="{ shown: false }" data-md-password>
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'type', 'placeholder']) }}
|
||||
id="{{ $id }}"
|
||||
@@ -33,7 +36,7 @@
|
||||
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"
|
||||
data-md-field-control
|
||||
/>
|
||||
|
||||
<x-slot:trailing>
|
||||
@@ -42,11 +45,12 @@
|
||||
x-on:click="shown = ! shown"
|
||||
x-bind:aria-label="shown ? @js(__('Hide password')) : @js(__('Show password'))"
|
||||
aria-label="{{ __('Show password') }}"
|
||||
class="field-trailing field-button"
|
||||
data-field-reveal
|
||||
data-md-field-trailing
|
||||
data-md-field-button
|
||||
data-md-field-reveal
|
||||
>
|
||||
<x-livewire-material::icon name="visibility" class="size-(--field-icon)" x-show="! shown" />
|
||||
<x-livewire-material::icon name="visibility_off" class="size-(--field-icon)" x-show="shown" x-cloak />
|
||||
<x-livewire-material::icon name="visibility" :size="$fieldIconSize" x-show="! shown" />
|
||||
<x-livewire-material::icon name="visibility_off" :size="$fieldIconSize" x-show="shown" x-cloak />
|
||||
</button>
|
||||
</x-slot:trailing>
|
||||
</x-livewire-material::field>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(600px, M3's compact/medium boundary — compact always stacks), 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).
|
||||
(resources/css/components/radio.css).
|
||||
|
||||
M3 stacks radio buttons vertically and cautions against a row at any width, so `inline` is for
|
||||
a group of two or three short labels that would waste a column, not the shape to reach for.
|
||||
@@ -35,28 +35,23 @@
|
||||
@endphp
|
||||
|
||||
<fieldset
|
||||
{{ $attributes->only(['class', 'wire:key'])->class(['min-w-0']) }}
|
||||
data-md-radio
|
||||
@if ($inline) data-md-inline @endif
|
||||
{{ $attributes->only(['class', 'style', 'wire:key']) }}
|
||||
@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>
|
||||
<legend data-md-radio-legend>{{ $label }}</legend>
|
||||
@endif
|
||||
|
||||
<div @class(['grid gap-4', 'medium:flex medium:flex-wrap medium:gap-x-6' => $inline])>
|
||||
<div data-md-radio-options>
|
||||
@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),
|
||||
'touch-target' => blank(data_get($option, $optionLabel)) && blank($optionHintText),
|
||||
])>
|
||||
<label data-md-selection-row @if ($messages !== []) data-md-invalid @endif>
|
||||
<span data-md-radio-button @if (blank(data_get($option, $optionLabel)) && blank($optionHintText)) class="md-touch-target" @endif>
|
||||
<input
|
||||
{{ $attributes->except(['class', 'wire:key', 'name', 'id']) }}
|
||||
{{ $attributes->except(['class', 'style', 'wire:key', 'name', 'id']) }}
|
||||
type="radio"
|
||||
name="{{ $name }}"
|
||||
value="{{ data_get($option, $optionValue) }}"
|
||||
@@ -64,13 +59,13 @@
|
||||
@checked($value !== null && (string) $value === (string) data_get($option, $optionValue))
|
||||
@if ($messages !== []) aria-invalid="true" @endif
|
||||
/>
|
||||
<span data-dot></span>
|
||||
<span data-md-radio-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>
|
||||
<span data-md-selection-text>
|
||||
<span data-md-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>
|
||||
<span data-md-selection-hint>{{ $optionHintText }}</span>
|
||||
@endif
|
||||
</span>
|
||||
</label>
|
||||
@@ -78,12 +73,12 @@
|
||||
</div>
|
||||
|
||||
@if ($messages !== [])
|
||||
<div id="{{ $id }}-support" class="mt-2 type-body-sm text-error" role="alert">
|
||||
<div id="{{ $id }}-support" data-md-selection-support 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>
|
||||
<p id="{{ $id }}-support" data-md-selection-support>{{ $hint }}</p>
|
||||
@endif
|
||||
</fieldset>
|
||||
|
||||
@@ -25,7 +25,11 @@
|
||||
The bar is a combobox: the wrapper around the input carries `role="combobox"` with
|
||||
`aria-expanded` and `aria-controls`, because ARIA gives a bare textbox neither. The results are
|
||||
a list, and a polite live region says how many of them there are whenever they change — M3 asks
|
||||
for both (docs/reference/m3/components-navigation-selection-inputs.md § Search). --}}
|
||||
for both (docs/reference/m3/components-navigation-selection-inputs.md § Search).
|
||||
|
||||
The root renders `data-md-search` with `data-md-trigger`, and `data-md-open` and
|
||||
`data-md-full-screen` while they hold; the parts are `data-md-search-*`, drawn by
|
||||
resources/css/components/search.css. `class` and `wire:key` land on the root. --}}
|
||||
|
||||
@props([
|
||||
'placeholder' => null,
|
||||
@@ -56,13 +60,13 @@
|
||||
x-on:focusout="leave($event)"
|
||||
x-on:pointerdown.outside="close()"
|
||||
x-trap.noscroll="fullScreen"
|
||||
x-bind:data-open="open ? '' : null"
|
||||
x-bind:data-full-screen="fullScreen ? '' : null"
|
||||
data-search
|
||||
data-trigger="{{ $trigger }}"
|
||||
{{ $attributes->only(['class', 'wire:key'])->class(['relative']) }}
|
||||
x-bind:data-md-open="open ? '' : null"
|
||||
x-bind:data-md-full-screen="fullScreen ? '' : null"
|
||||
data-md-search
|
||||
data-md-trigger="{{ $trigger }}"
|
||||
{{ $attributes->only(['class', 'style', 'wire:key']) }}
|
||||
>
|
||||
<div data-search-scrim x-cloak x-show="open && ! fullScreen" x-on:pointerdown="close()" aria-hidden="true"></div>
|
||||
<div data-md-search-scrim x-cloak x-show="open && ! fullScreen" x-on:pointerdown="close()" aria-hidden="true"></div>
|
||||
|
||||
@if ($trigger === 'icon')
|
||||
<button
|
||||
@@ -75,23 +79,23 @@
|
||||
aria-controls="{{ $id }}-view"
|
||||
aria-expanded="false"
|
||||
x-bind:aria-expanded="open.toString()"
|
||||
data-search-trigger
|
||||
data-md-search-trigger
|
||||
>
|
||||
<x-livewire-material::icon :name="$icon" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<div data-search-bar role="search" x-on:click="if ($event.target === $el) $refs.input.focus()">
|
||||
<span data-search-leading x-show="! fullScreen">
|
||||
<div data-md-search-bar role="search" x-on:click="if ($event.target === $el) $refs.input.focus()">
|
||||
<span data-md-search-leading x-show="! fullScreen">
|
||||
<x-livewire-material::icon :name="$icon" />
|
||||
</span>
|
||||
|
||||
<button type="button" data-search-leading data-search-back x-show="fullScreen" x-cloak x-on:click="close(true)" aria-label="{{ __('Back') }}">
|
||||
<button type="button" data-md-search-leading data-md-search-back x-show="fullScreen" x-cloak x-on:click="close(true)" aria-label="{{ __('Back') }}">
|
||||
<x-livewire-material::icon name="arrow_back" />
|
||||
</button>
|
||||
|
||||
<span
|
||||
data-search-field
|
||||
data-md-search-field
|
||||
role="combobox"
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="{{ $id }}-view"
|
||||
@@ -111,23 +115,23 @@
|
||||
x-on:click="show()"
|
||||
x-on:input="typed($event)"
|
||||
x-on:keydown.arrow-down.prevent="show(); $nextTick(() => step(1))"
|
||||
data-search-input
|
||||
data-md-search-input
|
||||
/>
|
||||
</span>
|
||||
|
||||
<button type="button" data-search-clear x-on:click="clear()" aria-label="{{ __('Clear') }}">
|
||||
<button type="button" data-md-search-clear x-on:click="clear()" aria-label="{{ __('Clear') }}">
|
||||
<x-livewire-material::icon name="close" />
|
||||
</button>
|
||||
|
||||
@isset($trailing)
|
||||
<span data-search-trailing>{{ $trailing }}</span>
|
||||
<span data-md-search-trailing>{{ $trailing }}</span>
|
||||
@endisset
|
||||
</div>
|
||||
|
||||
<div
|
||||
x-ref="view"
|
||||
id="{{ $id }}-view"
|
||||
data-search-view
|
||||
data-md-search-view
|
||||
x-cloak
|
||||
x-show="open"
|
||||
x-on:keydown.arrow-down.prevent="step(1)"
|
||||
@@ -135,15 +139,15 @@
|
||||
x-on:click="choose($event)"
|
||||
>
|
||||
@isset($suggestions)
|
||||
<div data-search-suggestions role="list" x-show="query === ''">{{ $suggestions }}</div>
|
||||
<div data-md-search-suggestions role="list" x-show="query === ''">{{ $suggestions }}</div>
|
||||
@endisset
|
||||
|
||||
@if ($slot->hasActualContent())
|
||||
<div data-search-results role="list" @isset($suggestions) x-cloak x-show="query !== ''" @endisset>{{ $slot }}</div>
|
||||
<div data-md-search-results role="list" @isset($suggestions) x-cloak x-show="query !== ''" @endisset>{{ $slot }}</div>
|
||||
@elseif (isset($empty))
|
||||
<div data-search-results @isset($suggestions) x-cloak x-show="query !== ''" @endisset><p class="px-4 py-3 type-body-md text-on-surface-variant">{{ $empty }}</p></div>
|
||||
<div data-md-search-results @isset($suggestions) x-cloak x-show="query !== ''" @endisset><p data-md-search-empty>{{ $empty }}</p></div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<span data-search-status class="sr-only" aria-live="polite" aria-atomic="true" x-text="announcement"></span>
|
||||
<span data-md-search-status class="md-visually-hidden" aria-live="polite" aria-atomic="true" x-text="announcement"></span>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
`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`. --}}
|
||||
`label`, `hint`, `icon`, `variant`, `size`. The field's root carries `data-md-select`; what a
|
||||
select changes in the field's chrome is resources/css/components/select.css. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
@@ -32,15 +33,16 @@
|
||||
// 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)) : [];
|
||||
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||||
@endphp
|
||||
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant floated :class="$attributes->get('class')">
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant floated :class="$attributes->get('class')" data-md-select>
|
||||
<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"
|
||||
data-md-field-control
|
||||
>
|
||||
@if (filled($placeholder))
|
||||
<option value="{{ $placeholderValue }}">{{ $placeholder }}</option>
|
||||
@@ -54,6 +56,6 @@
|
||||
</select>
|
||||
|
||||
<x-slot:trailing>
|
||||
<x-livewire-material::icon name="arrow_drop_down" class="field-trailing field-arrow size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="arrow_drop_down" :size="$fieldIconSize" data-md-field-trailing data-md-field-arrow />
|
||||
</x-slot:trailing>
|
||||
</x-livewire-material::field>
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
drawing, turned a quarter so the value grows upwards, with the value label beside the handle
|
||||
instead of above it and every size unchanged. The keyboard is the native range's, so Up and
|
||||
Right raise the value and Down and Left lower it. A vertical slider is as wide as a horizontal
|
||||
one is tall and takes its length from the wrapper, so give it an explicit height —
|
||||
`class="h-64"`, which the label and the hint share; without one it is 192px. M3 keeps range
|
||||
one is tall and takes its length from the wrapper, so give it an explicit height — a height
|
||||
rule on its `class`, which the label and the hint share; without one it is 192px. M3 keeps range
|
||||
sliders horizontal ("never use range sliders vertically — too much cognitive load"), so
|
||||
`range` wins over `orientation`.
|
||||
|
||||
@@ -63,7 +63,11 @@
|
||||
|
||||
The server draws the first frame; the drawing is `wire:ignore`, so a Livewire render never
|
||||
fights a handle being dragged, and a value the server changes moves the handle after the
|
||||
morph. --}}
|
||||
morph.
|
||||
|
||||
The root renders `data-md-slider` with `data-md-size`, `data-md-color`, `data-md-value-label`,
|
||||
`data-md-orientation` and `data-md-centered`; the parts are `data-md-slider-*`, and everything
|
||||
they look like is resources/css/components/slider.css. `class` and `style` land on the root. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
@@ -142,7 +146,6 @@
|
||||
$id = $attributes->get('id') ?? 'material-slider-'.substr(md5(($model ?? $name ?? '').'|'.$label.'|'.($model ?? $name ?? \Illuminate\Support\Str::random(10))), 0, 10);
|
||||
$described = $messages !== [] || filled($hint) ? "{$id}-hint" : null;
|
||||
|
||||
$sized = preg_match('/(^|\s)w-/', (string) $attributes->get('class')) === 1;
|
||||
$inputAttributes = $attributes->whereDoesntStartWith(['wire:model', 'x-model'])->except(['class', 'style', 'wire:key', 'id']);
|
||||
|
||||
// The first frame: Compose's layout and drawTrack, with every position a percentage of the
|
||||
@@ -234,182 +237,78 @@
|
||||
$segmentStyle = fn (?array $segment): string => $segment === null ? '' : 'left: '.$calc($segment[0]).'; width: '.$calc([$segment[1][0] - $segment[0][0], $segment[1][1] - $segment[0][1]])
|
||||
.'; border-radius: '.$segment[2].'px '.$segment[3].'px '.$segment[3].'px '.$segment[2].'px';
|
||||
|
||||
// Every colour class written out whole, so Tailwind compiles it.
|
||||
$activeInk = [
|
||||
'primary' => 'bg-primary', 'secondary' => 'bg-secondary', 'tertiary' => 'bg-tertiary', 'error' => 'bg-error',
|
||||
'success' => 'bg-success', 'warning' => 'bg-warning', 'info' => 'bg-info',
|
||||
];
|
||||
$inactiveInk = [
|
||||
'primary' => 'bg-secondary-container', 'secondary' => 'bg-secondary-container', 'tertiary' => 'bg-tertiary-container',
|
||||
'error' => 'bg-error-container', 'success' => 'bg-success-container', 'warning' => 'bg-warning-container', 'info' => 'bg-info-container',
|
||||
];
|
||||
// A tick, a stop indicator and the icon are drawn on the track, so each takes the "on" colour of
|
||||
// the part it sits on: on-primary over the active track, on-secondary-container over the
|
||||
// inactive one (M3's nine slider colour roles, in anatomy order). `data-active` marks the ones
|
||||
// over the active track.
|
||||
$tickInk = [
|
||||
'primary' => 'bg-on-secondary-container data-active:bg-on-primary', 'secondary' => 'bg-on-secondary-container data-active:bg-on-secondary',
|
||||
'tertiary' => 'bg-on-tertiary-container data-active:bg-on-tertiary', 'error' => 'bg-on-error-container data-active:bg-on-error',
|
||||
'success' => 'bg-on-success-container data-active:bg-on-success', 'warning' => 'bg-on-warning-container data-active:bg-on-warning',
|
||||
'info' => 'bg-on-info-container data-active:bg-on-info',
|
||||
];
|
||||
// A stop indicator only ever sits at an open end of the track, which is inactive.
|
||||
$stopInk = [
|
||||
'primary' => 'bg-on-secondary-container', 'secondary' => 'bg-on-secondary-container', 'tertiary' => 'bg-on-tertiary-container',
|
||||
'error' => 'bg-on-error-container', 'success' => 'bg-on-success-container', 'warning' => 'bg-on-warning-container',
|
||||
'info' => 'bg-on-info-container',
|
||||
];
|
||||
$iconInk = [
|
||||
'primary' => 'text-on-secondary-container data-active:text-on-primary', 'secondary' => 'text-on-secondary-container data-active:text-on-secondary',
|
||||
'tertiary' => 'text-on-tertiary-container data-active:text-on-tertiary', 'error' => 'text-on-error-container data-active:text-on-error',
|
||||
'success' => 'text-on-success-container data-active:text-on-success', 'warning' => 'text-on-warning-container data-active:text-on-warning',
|
||||
'info' => 'text-on-info-container data-active:text-on-info',
|
||||
];
|
||||
$accent = [
|
||||
'primary' => 'noscript:accent-primary', 'secondary' => 'noscript:accent-secondary', 'tertiary' => 'noscript:accent-tertiary',
|
||||
'error' => 'noscript:accent-error', 'success' => 'noscript:accent-success', 'warning' => 'noscript:accent-warning', 'info' => 'noscript:accent-info',
|
||||
];
|
||||
|
||||
$trackHeight = ['xs' => 'h-4', 'sm' => 'h-6', 'md' => 'h-10', 'lg' => 'h-14', 'xl' => 'h-24'][$size];
|
||||
// A focused handle is 6px shorter, so its ring stays clear of the label (Material Components'
|
||||
// m3_slider_focus_ring_thumb_height_decrease).
|
||||
$handleHeight = [
|
||||
'xs' => 'h-11 group-data-focused/thumb:h-9.5', 'sm' => 'h-11 group-data-focused/thumb:h-9.5', 'md' => 'h-13 group-data-focused/thumb:h-11.5',
|
||||
'lg' => 'h-17 group-data-focused/thumb:h-15.5', 'xl' => 'h-27 group-data-focused/thumb:h-25.5',
|
||||
][$size];
|
||||
// The label's bottom sits 4px above the handle: half of 44, 44, 52, 68 and 108px, plus 4.
|
||||
$labelBottom = ['xs' => 'bottom-[calc(50%+1.625rem)]', 'sm' => 'bottom-[calc(50%+1.625rem)]', 'md' => 'bottom-[calc(50%+1.875rem)]', 'lg' => 'bottom-[calc(50%+2.375rem)]', 'xl' => 'bottom-[calc(50%+3.625rem)]'][$size];
|
||||
// The measure across the slider — the handle's height, and so the slider's own. Standing up it
|
||||
// is the width instead, and the length comes from the wrapper the caller sized.
|
||||
$across = [
|
||||
'xs' => $vertical ? 'w-12' : 'h-12',
|
||||
'sm' => $vertical ? 'w-12' : 'h-12',
|
||||
'md' => $vertical ? 'w-13' : 'h-13',
|
||||
'lg' => $vertical ? 'w-17' : 'h-17',
|
||||
'xl' => $vertical ? 'w-27' : 'h-27',
|
||||
][$size];
|
||||
$thumbs = $range ? ['start', 'end'] : ['start'];
|
||||
@endphp
|
||||
|
||||
<div {{ $attributes->only(['class', 'style', 'wire:key'])->class(['min-w-0', 'w-full' => ! $sized && ! $vertical, 'flex w-fit flex-col' => $vertical]) }}>
|
||||
<div
|
||||
data-md-slider
|
||||
data-md-size="{{ $size }}"
|
||||
data-md-color="{{ $color }}"
|
||||
data-md-value-label="{{ $valueLabel }}"
|
||||
@if ($vertical) data-md-orientation="vertical" @endif
|
||||
@if ($centered) data-md-centered @endif
|
||||
{{ $attributes->only(['class', 'style', 'wire:key']) }}
|
||||
>
|
||||
@if (filled($label))
|
||||
@if ($range)
|
||||
<span id="{{ $id }}-label" class="mb-2 block type-label-lg text-on-surface-variant">{{ $label }}</span>
|
||||
<span id="{{ $id }}-label" data-md-slider-label>{{ $label }}</span>
|
||||
@else
|
||||
<label for="{{ $id }}" id="{{ $id }}-label" class="mb-2 block type-label-lg text-on-surface-variant">{{ $label }}</label>
|
||||
<label for="{{ $id }}" id="{{ $id }}-label" data-md-slider-label>{{ $label }}</label>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<div
|
||||
x-data="materialSlider"
|
||||
x-on:pointerdown="press($event)"
|
||||
data-slider
|
||||
data-size="{{ $size }}"
|
||||
@if ($vertical) data-orientation="vertical" @endif
|
||||
@if ($centered) data-centered @endif
|
||||
data-md-slider-control
|
||||
@if ($range) role="group" @if (filled($label)) aria-labelledby="{{ $id }}-label" @endif @endif
|
||||
@class([
|
||||
'group/slider relative cursor-pointer select-none has-disabled:cursor-not-allowed',
|
||||
$across,
|
||||
'touch-pan-y' => ! $vertical,
|
||||
// Standing up: 192px long unless the wrapper says otherwise, and a size container,
|
||||
// which is what lets the drawing below be as long as this is tall.
|
||||
'touch-pan-x h-48 min-h-0 flex-auto [container-type:size]' => $vertical,
|
||||
// Room for a label that never hides: half the handle, 4px and the 44px pill — above the
|
||||
// track when the slider lies down, beside it when it stands up.
|
||||
'mt-12' => $valueLabel === 'always' && ! $vertical,
|
||||
'ms-12' => $valueLabel === 'always' && $vertical,
|
||||
'noscript:h-auto noscript:cursor-auto noscript:space-y-2',
|
||||
])
|
||||
>
|
||||
<div data-slider-drawing wire:ignore aria-hidden="true" @class([
|
||||
'pointer-events-none absolute noscript:hidden',
|
||||
'inset-y-0 inset-x-0.5 rtl:-scale-x-100' => ! $vertical,
|
||||
// The same drawing, laid out as long as the slider is tall and then turned a quarter
|
||||
// anticlockwise so the value grows upwards. Everything inside it — the segments, the
|
||||
// ticks, the handle and its label — is placed as if the slider were lying down.
|
||||
'top-1/2 left-1/2 h-[100cqw] w-[calc(100cqh-0.25rem)] -translate-1/2 -rotate-90' => $vertical,
|
||||
])>
|
||||
<div @class(['absolute inset-x-0 top-1/2 -translate-y-1/2', $trackHeight])>
|
||||
@foreach (['start' => $inactiveInk[$color], 'active' => $activeInk[$color], 'end' => $inactiveInk[$color]] as $part => $ink)
|
||||
<div data-md-slider-drawing wire:ignore aria-hidden="true">
|
||||
<div data-md-slider-track>
|
||||
@foreach (['start', 'active', 'end'] as $part)
|
||||
<span
|
||||
data-segment="{{ $part }}"
|
||||
data-md-slider-segment="{{ $part }}"
|
||||
@if (! isset($segments[$part])) hidden @endif
|
||||
@class([
|
||||
'absolute inset-y-0',
|
||||
$ink,
|
||||
'group-has-disabled/slider:bg-on-surface/38' => $part === 'active',
|
||||
'group-has-disabled/slider:bg-on-surface/12' => $part !== 'active',
|
||||
])
|
||||
style="{{ $segmentStyle($segments[$part] ?? null) }}"
|
||||
></span>
|
||||
@endforeach
|
||||
|
||||
@foreach ($tickMarks as $mark)
|
||||
<span
|
||||
data-tick="{{ $format($mark['fraction']) }}"
|
||||
data-md-slider-tick="{{ $format($mark['fraction']) }}"
|
||||
@if ($mark['hidden']) hidden @endif
|
||||
@if ($mark['active']) data-active @endif
|
||||
@class(['absolute top-1/2 size-1 -translate-1/2 rounded-corner-full', $tickInk[$color], 'group-has-disabled/slider:bg-on-surface/38 group-has-disabled/slider:data-active:bg-on-surface/12'])
|
||||
@if ($mark['active']) data-md-active @endif
|
||||
style="left: {{ $calc($mark['at']) }}"
|
||||
></span>
|
||||
@endforeach
|
||||
|
||||
@foreach (['start', 'end'] as $part)
|
||||
<span
|
||||
data-stop="{{ $part }}"
|
||||
data-md-slider-stop="{{ $part }}"
|
||||
@if (! isset($stops[$part])) hidden @endif
|
||||
@class(['absolute top-1/2 size-1 -translate-1/2 rounded-corner-full', $stopInk[$color], 'group-has-disabled/slider:bg-on-surface/38'])
|
||||
style="{{ isset($stops[$part]) ? 'left: '.$calc($stops[$part]) : '' }}"
|
||||
></span>
|
||||
@endforeach
|
||||
|
||||
@if ($icon !== null)
|
||||
<span
|
||||
data-track-icon
|
||||
data-md-slider-icon
|
||||
@if (! $iconInActive && ! $iconInInactive) hidden @endif
|
||||
@if ($iconInActive) data-active @endif
|
||||
@class([
|
||||
'absolute top-1/2 flex -translate-y-1/2',
|
||||
'rtl:-scale-x-100' => ! $vertical,
|
||||
// Turned back the quarter the drawing turns, so the glyph stands up.
|
||||
'rotate-90' => $vertical,
|
||||
$iconInk[$color],
|
||||
'group-has-disabled/slider:text-on-surface/38',
|
||||
])
|
||||
@if ($iconInActive) data-md-active @endif
|
||||
style="left: {{ $calc($iconAt) }}"
|
||||
>
|
||||
<x-livewire-material::icon :name="$icon" :class="$iconSize === 32 ? 'size-8' : 'size-6'" />
|
||||
<x-livewire-material::icon :name="$icon" :size="$iconSize" />
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@foreach ($thumbs as $index => $thumb)
|
||||
<span data-handle="{{ $thumb }}" class="group/thumb absolute inset-y-0 w-0" style="left: {{ $calc($place($fractions[$index])) }}">
|
||||
<span @class([
|
||||
'absolute top-1/2 left-0 w-1 -translate-1/2 rounded-corner-full',
|
||||
'transition-[width,height] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast',
|
||||
'group-data-pressed/thumb:w-0.5 group-data-focused/thumb:w-0.5',
|
||||
'outline-offset-2 outline-secondary group-data-focused/thumb:outline-3',
|
||||
$handleHeight,
|
||||
$activeInk[$color],
|
||||
'group-has-disabled/slider:bg-on-surface/38',
|
||||
])></span>
|
||||
<span data-md-slider-handle="{{ $thumb }}" style="left: {{ $calc($place($fractions[$index])) }}">
|
||||
<span data-md-slider-thumb></span>
|
||||
|
||||
@if ($valueLabel !== 'never')
|
||||
<span @class(['absolute left-0 z-10 flex -translate-x-1/2 justify-center', 'rtl:-scale-x-100' => ! $vertical, $labelBottom])>
|
||||
<span
|
||||
data-value-label
|
||||
@class([
|
||||
'flex h-11 min-w-12 items-center justify-center whitespace-nowrap rounded-corner-full bg-inverse-surface px-2.5 text-inverse-on-surface type-label-lg',
|
||||
'origin-bottom' => ! $vertical,
|
||||
// Turned back the quarter the drawing turns: the pill lands
|
||||
// beside the handle rather than above it, and reads across.
|
||||
'rotate-90' => $vertical,
|
||||
'transition-[opacity,scale] duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast',
|
||||
'opacity-0 scale-75 group-data-pressed/thumb:opacity-100 group-data-pressed/thumb:scale-100 group-data-focused/thumb:opacity-100 group-data-focused/thumb:scale-100' => $valueLabel === 'drag',
|
||||
])
|
||||
>{{ $format($values[$index]) }}</span>
|
||||
<span data-md-slider-value-anchor>
|
||||
<span data-md-slider-value>{{ $format($values[$index]) }}</span>
|
||||
</span>
|
||||
@endif
|
||||
</span>
|
||||
@@ -425,7 +324,7 @@
|
||||
max="{{ $format($maximum) }}"
|
||||
step="{{ $continuous ? 'any' : $format($interval) }}"
|
||||
value="{{ $format($values[$index]) }}"
|
||||
data-thumb="{{ $thumb }}"
|
||||
data-md-slider-input="{{ $thumb }}"
|
||||
@if ($vertical) aria-orientation="vertical" @endif
|
||||
@if ($range) aria-label="{{ $thumb === 'start' ? __('Range start') : __('Range end') }}" @endif
|
||||
@if ($described) aria-describedby="{{ $described }}" @endif
|
||||
@@ -433,22 +332,17 @@
|
||||
@disabled($disabled)
|
||||
{{ new \Illuminate\View\ComponentAttributeBag($bindingsFor($index)) }}
|
||||
{{ $inputAttributes }}
|
||||
@class([
|
||||
'pointer-events-none absolute inset-0 m-0 size-full appearance-none bg-transparent opacity-0',
|
||||
'noscript:pointer-events-auto noscript:static noscript:block noscript:h-6 noscript:w-full noscript:appearance-auto noscript:opacity-100',
|
||||
$accent[$color],
|
||||
])
|
||||
/>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if ($messages !== [])
|
||||
<div id="{{ $id }}-hint">
|
||||
<div id="{{ $id }}-hint" data-md-slider-errors>
|
||||
@foreach ($messages as $message)
|
||||
<p class="mt-1 type-body-sm text-error">{{ $message }}</p>
|
||||
<p>{{ $message }}</p>
|
||||
@endforeach
|
||||
</div>
|
||||
@elseif (filled($hint))
|
||||
<p id="{{ $id }}-hint" class="mt-1 type-body-sm text-on-surface-variant">{{ $hint }}</p>
|
||||
<p id="{{ $id }}-hint" data-md-slider-hint>{{ $hint }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
Pressing it sorts by this column, ascending first and then flipping; the arrow says which way,
|
||||
and `aria-sort` says it to a screen reader. A column that cannot be sorted is a plain `<th>`.
|
||||
`class` lands on the `<th>` (`text-end` for a number column moves the button with it).
|
||||
`class` lands on the `<th>` (end-aligning a number column moves the button with it).
|
||||
|
||||
The button is only as tall as its title-small line, so it carries `touch-target` and catches
|
||||
presses over M3's 48px minimum; the cell's own padding belongs to the cell, not to it. --}}
|
||||
The button is only as tall as its title-small line, so it carries `md-touch-target` and catches
|
||||
presses over M3's 48px minimum; the cell's own padding belongs to the cell, not to it. The cell
|
||||
renders `data-md-sort-header` (`data-md-active` on the sorted column) and the button
|
||||
`data-md-sort-header-button`, drawn by resources/css/components/sort-header.css. --}}
|
||||
|
||||
@props([
|
||||
'sortBy' => [],
|
||||
@@ -20,21 +22,14 @@
|
||||
$next = $active && $direction === 'asc' ? 'desc' : 'asc';
|
||||
@endphp
|
||||
|
||||
<th @if ($active) aria-sort="{{ $direction === 'asc' ? 'ascending' : 'descending' }}" @endif {{ $attributes }}>
|
||||
<th data-md-sort-header @if ($active) data-md-active aria-sort="{{ $direction === 'asc' ? 'ascending' : 'descending' }}" @endif {{ $attributes }}>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="$set('{{ $model }}', {{ json_encode(['column' => $column, 'direction' => $next]) }})"
|
||||
data-sort-header
|
||||
@class([
|
||||
'group/sort focus-ring touch-target inline-flex cursor-pointer items-center gap-1 rounded-corner-xs',
|
||||
'text-on-surface' => $active,
|
||||
'hover:text-on-surface' => ! $active,
|
||||
])
|
||||
data-md-sort-header-button
|
||||
class="md-focus-ring md-touch-target"
|
||||
>
|
||||
{{ $slot }}
|
||||
<x-livewire-material::icon :name="$direction === 'desc' ? 'arrow_downward' : 'arrow_upward'" optical="20" :class="\Illuminate\Support\Arr::toCssClasses([
|
||||
'size-4 transition-opacity duration-(--md-sys-motion-effects-fast-duration)',
|
||||
'opacity-0 group-hover/sort:opacity-60 group-focus-visible/sort:opacity-60' => ! $active,
|
||||
])" />
|
||||
<x-livewire-material::icon :name="$direction === 'desc' ? 'arrow_downward' : 'arrow_upward'" size="16" data-md-sort-header-arrow />
|
||||
</button>
|
||||
</th>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{{-- A data table. Its callers write `<thead>`, `<tr>`, `<th>` and `<td>` as they always do; how they
|
||||
look is resources/css/components/table.css, keyed on the `data-table` attribute set here (the
|
||||
top of that file says why).
|
||||
look is resources/css/components/table.css, keyed on the `data-md-table` attribute set here (the
|
||||
top of that file says why), with `data-md-size` and `data-md-dense`.
|
||||
|
||||
Rows are 52px, a target a finger can hit. `dense` tightens them to 36px — M3 asks that density
|
||||
always be an opt-in and never a default, so it is the caller's decision and the caller's to
|
||||
justify. `size="xs"` for a table inside a panel inside a panel (32px rows, 24px with `dense`).
|
||||
|
||||
A selected row is `<tr aria-selected="true">`. Horizontal scrolling stays the caller's — wrap the
|
||||
table in `<div class="overflow-x-auto">` where the page needs it. A column that sorts is
|
||||
`<x-sort-header>`. --}}
|
||||
table in an element that scrolls sideways (`overflow-x: auto`) where the page needs it. A column
|
||||
that sorts is `<x-sort-header>`. --}}
|
||||
|
||||
@props([
|
||||
'size' => 'sm',
|
||||
'dense' => false,
|
||||
])
|
||||
|
||||
<table data-table data-size="{{ $size === 'xs' ? 'xs' : 'sm' }}"{{ $dense ? ' data-dense' : '' }} {{ $attributes }}>
|
||||
<table data-md-table data-md-size="{{ $size === 'xs' ? 'xs' : 'sm' }}"{{ $dense ? ' data-md-dense' : '' }} {{ $attributes }}>
|
||||
{{ $slot }}
|
||||
</table>
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
form's column never moves. The label rests on the first line rather than in the middle of the
|
||||
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>`. --}}
|
||||
attribute reaches the `<textarea>`. The field's root carries `data-md-textarea`; what a
|
||||
textarea changes in the field's chrome is resources/css/components/textarea.css. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
@@ -30,18 +31,18 @@
|
||||
$max = $counter ? $attributes->get('maxlength') : null;
|
||||
@endphp
|
||||
|
||||
<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">
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$variant :counter="$max" :$full :class="$attributes->get('class')" :data-md-readonly="$attributes->get('readonly') ? '' : null" data-md-textarea>
|
||||
<textarea
|
||||
{{ $attributes->except(['class', 'id', 'placeholder']) }}
|
||||
id="{{ $id }}"
|
||||
rows="{{ (int) $rows }}"
|
||||
placeholder="{{ $placeholder }}"
|
||||
@if ($autogrow)
|
||||
data-autogrow
|
||||
data-md-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"
|
||||
data-md-field-control
|
||||
></textarea>
|
||||
</x-livewire-material::field>
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
$inputAttributes = $attributes->whereDoesntStartWith(['wire:model', 'x-model'])->except(['class', 'id', 'wire:key', 'placeholder']);
|
||||
$disabled = (bool) $attributes->get('disabled');
|
||||
$described = $messages !== [] || filled($hint);
|
||||
$fieldIconSize = ['sm' => 20, 'xs' => 16][$size] ?? 24;
|
||||
@endphp
|
||||
|
||||
<div
|
||||
@@ -159,7 +160,7 @@
|
||||
x-bind:aria-expanded="open.toString()"
|
||||
@if ($messages !== []) aria-invalid="true" @endif
|
||||
@if ($described) aria-describedby="{{ $id }}-support" @endif
|
||||
class="field-control"
|
||||
data-md-field-control
|
||||
x-on:click="show()"
|
||||
x-on:keydown.enter.prevent="show()"
|
||||
x-on:keydown.space.prevent="show()"
|
||||
@@ -171,12 +172,13 @@
|
||||
<button
|
||||
type="button"
|
||||
x-on:click="value = null; $refs.input.focus()"
|
||||
class="field-trailing field-clear field-button"
|
||||
data-md-field-trailing
|
||||
data-md-field-button
|
||||
data-md-field-clear
|
||||
aria-label="{{ __('Clear') }}"
|
||||
data-field-clear
|
||||
@disabled($disabled)
|
||||
>
|
||||
<x-livewire-material::icon name="close" class="size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="close" :size="$fieldIconSize" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@@ -184,12 +186,13 @@
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
x-on:click="show()"
|
||||
class="field-trailing field-button"
|
||||
data-md-field-trailing
|
||||
data-md-field-button
|
||||
aria-label="{{ __('Choose time') }}"
|
||||
data-timepicker-open
|
||||
@disabled($disabled)
|
||||
>
|
||||
<x-livewire-material::icon name="schedule" class="size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="schedule" :size="$fieldIconSize" />
|
||||
</button>
|
||||
</x-slot:trailing>
|
||||
</x-livewire-material::field>
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
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).
|
||||
(resources/css/components/toggle.css).
|
||||
|
||||
Without a label the row is only the 52×32 track, so the track carries `touch-target` and catches
|
||||
presses over M3's 48px minimum. --}}
|
||||
Without a label the row is only the 52×32 track, so the track carries `md-touch-target` and
|
||||
catches presses over M3's 48px minimum. `class` and `style` land on the root, `data-md-toggle`. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
@@ -23,35 +23,30 @@
|
||||
$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 @class(['touch-target' => blank($label) && blank($hint)])>
|
||||
<div data-md-toggle {{ $attributes->only(['class', 'style', 'wire:key']) }}>
|
||||
<label for="{{ $id }}" data-md-selection-row @if ($right) data-md-right @endif>
|
||||
<span data-md-switch @if ($icons) data-md-icons="{{ $icons }}" @endif @if (blank($label) && blank($hint)) class="md-touch-target" @endif>
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'wire:key']) }}
|
||||
{{ $attributes->except(['class', 'style', 'id', 'wire:key']) }}
|
||||
id="{{ $id }}"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
/>
|
||||
<span data-handle>
|
||||
<span data-md-switch-handle>
|
||||
@if ($icons)
|
||||
<x-livewire-material::icon name="check" class="size-4" optical="20" data-on />
|
||||
<x-livewire-material::icon name="close" class="size-4" optical="20" data-off />
|
||||
<x-livewire-material::icon name="check" size="16" data-md-switch-on />
|
||||
<x-livewire-material::icon name="close" size="16" data-md-switch-off />
|
||||
@endif
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@if (filled($label) || filled($hint))
|
||||
<span class="min-w-0">
|
||||
<span data-md-selection-text>
|
||||
@if (filled($label))
|
||||
<span class="block type-body-lg text-on-surface" data-selection-label>{{ $label }}</span>
|
||||
<span data-md-selection-label>{{ $label }}</span>
|
||||
@endif
|
||||
@if (filled($hint))
|
||||
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $hint }}</span>
|
||||
<span data-md-selection-hint>{{ $hint }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
|
||||
Reference in New Issue
Block a user