Draw the chips without Tailwind
<x-chip> renders data-md-chip with its type and its states and parts as data-md-* attributes (elevated, disabled, selected, actionable, the icon, check slot, avatar, action and remove button), and every colour, size, target and transition moves into components/chip.css on tokens (plan step 36). Icons take size 18; the filter checkbox is md-visually-hidden. chips.js follows the renamed chip hooks. A disabled filter chip drawn as a label no longer shows a hover state layer, which M3 never gives a disabled control. Browser tests cover the 32px chip and the remove button's 48px target. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
b42069503d
commit
d8072d878a
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user