A plain tooltip vanished the instant the pointer left, where M3 keeps one for 1.5s and shows only one at a time — these are popover="manual", so nothing was closing the last one. A rich tooltip's bubble was never associated with its trigger at all, so the explanation it exists to give was never announced; the script now writes aria-describedby (and aria-haspopup/aria-expanded when it is persistent) onto the focusable control in the trigger slot, and again after a morph. Plan step 18, actions.md ACT-22, ACT-25. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
75 lines
3.8 KiB
PHP
75 lines
3.8 KiB
PHP
{{-- An M3 rich tooltip: a few lines of context for a control, with an optional subhead and actions.
|
|
|
|
<x-rich-tooltip title="Expiry" text="Recipients lose access after this time. Admins can change the longest allowed.">
|
|
<x-button icon="help" aria-label="About expiry" />
|
|
<x-slot:actions><x-button label="Learn more" link="/help/expiry" /></x-slot:actions>
|
|
</x-rich-tooltip>
|
|
|
|
It wraps its trigger. By default it shows on hover and keyboard focus like a plain tooltip;
|
|
`persistent` makes it open on press instead and stay until a press elsewhere or Escape — the
|
|
form M3 asks for when it has actions. The bubble is a popover in surface-container with a
|
|
medium corner and elevation 2, 312px at most, placed by anchor positioning on `side`.
|
|
|
|
The bubble's id and anchor name are new with every render; its `wire:key` (see `<x-menu>`)
|
|
lets a Livewire morph patch it in place, so an open bubble stays open — through its own
|
|
action's `wire:click` too — and resources/js/rich-tooltip.js keeps showing and hiding the
|
|
element on the page rather than one the morph took away.
|
|
|
|
resources/js/rich-tooltip.js points the trigger at the bubble with `aria-describedby`, and a
|
|
`persistent` one also carries `aria-haspopup="dialog"` and `aria-expanded`: the explanation is
|
|
the whole point of a rich tooltip, and without the association a screen reader reads only the
|
|
trigger's own label. The trigger arrives in a slot, so only script can find the element in it
|
|
that takes the focus.
|
|
|
|
RichTooltipTokens (androidx Compose Material 3, Apache-2.0): title-small subhead and body-medium
|
|
text in on-surface-variant, label-large actions in primary. --}}
|
|
|
|
@props([
|
|
'title' => null,
|
|
'text' => null,
|
|
'side' => 'bottom',
|
|
'persistent' => false,
|
|
])
|
|
|
|
@php
|
|
$side = in_array($side, ['top', 'bottom', 'left', 'right'], true) ? $side : 'bottom';
|
|
$key = \Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10));
|
|
$anchor = "--material-rich-tooltip-{$key}";
|
|
@endphp
|
|
|
|
<span
|
|
{{ $attributes->class('inline-flex') }}
|
|
style="anchor-name: {{ $anchor }}"
|
|
x-data="materialRichTooltip({{ $persistent ? 'true' : 'false' }})"
|
|
>
|
|
{{ $trigger ?? $slot }}
|
|
|
|
<span
|
|
x-ref="bubble"
|
|
{{ new \Illuminate\View\ComponentAttributeBag(['wire:key' => 'material-rich-tooltip']) }}
|
|
id="material-rich-tooltip-{{ $key }}"
|
|
popover="{{ $persistent ? 'auto' : 'manual' }}"
|
|
role="{{ $persistent ? 'dialog' : 'tooltip' }}"
|
|
@if ($title) aria-label="{{ $title }}" @endif
|
|
style="position-anchor: {{ $anchor }}"
|
|
@class([
|
|
'm-0 w-max max-w-78 overflow-visible border-0 rounded-corner-md bg-surface-container px-4 pt-3 pb-2 text-start whitespace-normal shadow-elevation-2 [inset:auto]',
|
|
'opacity-0 transition-[opacity,display,overlay] transition-discrete duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast open:opacity-100 starting:open:opacity-0',
|
|
'my-1 [position-area:bottom_span-right] [position-try-fallbacks:flip-block,flip-inline]' => $side === 'bottom',
|
|
'my-1 [position-area:top_span-right] [position-try-fallbacks:flip-block,flip-inline]' => $side === 'top',
|
|
'mx-1 [position-area:left_span-bottom] [position-try-fallbacks:flip-inline,flip-block]' => $side === 'left',
|
|
'mx-1 [position-area:right_span-bottom] [position-try-fallbacks:flip-inline,flip-block]' => $side === 'right',
|
|
])
|
|
>
|
|
@if ($title)
|
|
<span class="mb-1 block type-title-sm text-on-surface-variant">{{ $title }}</span>
|
|
@endif
|
|
|
|
<span class="block pb-2 type-body-md text-on-surface-variant">{{ $text }}</span>
|
|
|
|
@isset($actions)
|
|
<span class="-ms-3 flex flex-wrap gap-2">{{ $actions }}</span>
|
|
@endisset
|
|
</span>
|
|
</span>
|