Menus, submenus, tooltips, rich tooltips and the FAB menu held their exit with `transition-behavior: allow-discrete` on `display` and `overlay`. Firefox transitions neither (MDN browser-compat-data, `display.is_transitionable`: Chrome 117, Safari 18, Firefox none), so every one of them vanished on its first frame there. A script cannot hold a popover open instead: `beforetoggle` is not cancellable on the way out, and the browser's own light dismiss (Escape, a press outside) never asks. resources/js/popover-exit.js: a popover marked `data-md-popover-exit` closes for real at once — focus, aria-expanded and toggle stay the browser's — and a copy taken in `beforetoggle`, while it is still drawn, stands in for the exit. The copy is decoration: a manual popover in the top layer (closing no other popover), inert, aria-hidden, without ids or nested popovers, `x-ignore`d so Alpine starts nothing in it, pinned to the popover's box with its resolved colours. It is shown with its transitions off, so `@starting-style` does not replay the entry, then marked `data-md-popover-closing`, which each stylesheet turns into its closed values (`:popover-open:not([data-md-popover-closing])`, and the FAB menu's items' sink), so it moves on the component's own tokens. It is removed once the longest of them has run, and opening the popover again takes it away. Under reduced motion every duration is zero and no copy is made. `display`, `overlay` and `allow-discrete` leave the transitions, so Chrome and Safari take the same path. Browser tests in Chrome, Firefox and Safari slow the motion tokens so a round trip still finds the exit on screen: a menu after Escape and after a press outside (the real menu closed and focus back on its button, the copy inert, fading, with no Alpine state, and gone after), a reopen part-way through, reduced motion, a submenu while its menu stays open, a tooltip, the FAB menu's items part-way down their sink, and a persistent rich tooltip. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
73 lines
3.3 KiB
PHP
73 lines
3.3 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` (ACT-22): 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. It stands for 1.5s after the pointer or focus leaves,
|
|
M3's transient tooltip timing (ACT-25), unless `persistent` keeps it open until dismissed.
|
|
|
|
RichTooltipTokens (androidx Compose Material 3, Apache-2.0): title-small subhead and body-medium
|
|
text in on-surface-variant, label-large actions in primary. Drawn by
|
|
resources/css/components/rich-tooltip.css from `data-md-rich-tooltip` and `data-md-side`. --}}
|
|
|
|
@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
|
|
data-md-rich-tooltip
|
|
{{ $attributes }}
|
|
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
|
|
data-md-rich-tooltip-bubble
|
|
data-md-popover-exit
|
|
data-md-side="{{ $side }}"
|
|
style="position-anchor: {{ $anchor }}"
|
|
>
|
|
@if ($title)
|
|
<span data-md-rich-tooltip-title>{{ $title }}</span>
|
|
@endif
|
|
|
|
<span data-md-rich-tooltip-text>{{ $text }}</span>
|
|
|
|
@isset($actions)
|
|
<span data-md-rich-tooltip-actions>{{ $actions }}</span>
|
|
@endisset
|
|
</span>
|
|
</span>
|