Files
livewire-material/resources/views/components/chip-set.blade.php
T
Andreas Reinhold / reiniandClaude Opus 5 55cf8ceed7 Draw the chip set without Tailwind
<x-chip-set> renders data-md-chip-set (data-md-scroll), its row, label,
hint, errors and scroll buttons as data-md-* attributes, and its row,
fade mask, scroll padding and pointer-fine buttons move into
components/chip-set.css on tokens (plan step 36). chips.js marks the
row data-md-scroll-start and data-md-scroll-end. Browser tests cover the
scroll buttons in LTR and RTL and the arrow keys with a roving tab stop
that scrolls the focused chip clear of the button.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:54:41 +02:00

96 lines
4.1 KiB
PHP

{{-- A set of M3 chips: a row that wraps, 8px apart, named for screen readers as a group.
<x-chip-set label="File types" hint="Show only these" error-field="kinds">
<x-chip type="filter" label="Photos" value="photos" wire:model.live="kinds" />
<x-chip type="filter" label="Documents" value="documents" wire:model.live="kinds" />
</x-chip-set>
`label` is shown above the row and names the group; without it, pass `aria-label`. `hint`
sits under it, and a validation message for `error-field` (the property the chips bind, and
its items: `kinds` and `kinds.*`) replaces the hint.
`scroll` keeps the chips on one line that scrolls sideways, as M3 lays chips out on a narrow
screen. M3's chips accessibility page asks that a row which overflows say so, so the edge it
can still scroll towards fades (resources/js/chips.js) and, where the pointer is fine and
there is no swipe to reach for, a button sits over each fading edge and scrolls the row by
most of its width. The buttons are pointer affordances only — `tabindex="-1"` and
`aria-hidden`, because the arrow keys already walk every chip and scroll each one clear of
the fade. Removing a focused input chip moves focus within the set.
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.
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,
'hint' => null,
'errorField' => null,
'scroll' => false,
])
@php
$key = \Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10));
$messages = filled($errorField) && isset($errors)
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($errorField), $errors->get($errorField.'.*')])))
: [];
$describedBy = $messages !== [] || filled($hint) ? "material-chip-set-{$key}-hint" : null;
@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 }}
>
@if (filled($label))
<p id="material-chip-set-{{ $key }}-label" data-md-chip-set-label>{{ $label }}</p>
@endif
@if ($scroll)
<div x-data="materialChipSet" data-md-chip-set-scroller>
<div
data-md-chip-set-row
x-ref="row"
x-on:keydown="key($event)"
x-on:focusin="rove($event.target)"
wire:ignore.self
>
{{ $slot }}
</div>
{{-- After the row, so each button can ask whether the row can still scroll its way. --}}
@foreach (['start' => 'chevron_left', 'end' => 'chevron_right'] as $edge => $glyph)
<button
type="button"
tabindex="-1"
aria-hidden="true"
data-md-chip-scroll="{{ $edge }}"
x-on:click="nudge('{{ $edge }}')"
>
<x-livewire-material::icon :name="$glyph" size="18" mirror-rtl />
</button>
@endforeach
</div>
@else
<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 }}" data-md-chip-set-errors>
@foreach ($messages as $message)
<p>{{ $message }}</p>
@endforeach
</div>
@elseif (filled($hint))
<p id="{{ $describedBy }}" data-md-chip-set-hint>{{ $hint }}</p>
@endif
</div>