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>
80 lines
3.7 KiB
PHP
80 lines
3.7 KiB
PHP
{{-- An M3 Expressive FAB menu: a FAB that opens into a short list of related actions.
|
|
|
|
<div style="position: fixed; inset-inline-end: 16px; bottom: 16px">
|
|
<x-fab-menu label="New">
|
|
<x-fab-menu-item label="Upload files" icon="upload_file" wire:click="uploadFiles" />
|
|
<x-fab-menu-item label="Upload a folder" icon="drive_folder_upload" wire:click="uploadFolder" />
|
|
</x-fab-menu>
|
|
</div>
|
|
|
|
Two to six items. The FAB (`icon`, `add` by default, in `color`'s container) turns into a
|
|
round close button in the colour itself while the list is open above it, end-aligned; the
|
|
list is a `popover="auto"` menu with the menu keyboard of `<x-menu>`. `label` names the FAB
|
|
and the menu for screen readers, and defaults to "Toggle menu" (ACT-21) — the icons inside
|
|
the button are aria-hidden, so without it the trigger has no name at all. Give the items the
|
|
same `color`. Like `<x-menu>`'s, the list is keyed for Livewire, so it stays open through a
|
|
render of the component around it.
|
|
|
|
It does not place itself. M3's margin from the edge of the window is 16px, 24px from `large`
|
|
(1200px) up — grow it in the caller's own stylesheet:
|
|
|
|
@media (width >= 1200px) { .my-fab-menu { inset-inline-end: 24px; bottom: 24px } }
|
|
|
|
A window too short for the list scrolls it, behind the close button, which stays fixed
|
|
(ACT-32); the items rise into place as it opens and sink back as it closes (ACT-33).
|
|
|
|
FabMenuBaselineTokens (androidx Compose Material 3, Apache-2.0): 56px close button, full
|
|
corner when open, 20px close icon, 8px to the first item, 56px items 4px apart.
|
|
|
|
Drawn by resources/css/components/fab-menu.css from `data-md-fab-menu`, `data-md-color` and
|
|
`data-md-position`. --}}
|
|
|
|
@props([
|
|
'icon' => 'add',
|
|
'label' => null,
|
|
'color' => 'primary',
|
|
'position' => 'top-end',
|
|
])
|
|
|
|
@php
|
|
// The trigger's only content is two icons, which are aria-hidden, so without a name it is an
|
|
// unnamed button. M3 asks the FAB's label to say what menu it opens; "Toggle menu" is its own
|
|
// fallback wording for the close button.
|
|
$label = filled($label) ? $label : __('Toggle menu');
|
|
$color = in_array($color, ['primary', 'secondary', 'tertiary'], true) ? $color : 'primary';
|
|
$position = in_array($position, ['top-start', 'top-end', 'bottom-start', 'bottom-end'], true) ? $position : 'top-end';
|
|
$key = \Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10));
|
|
$anchor = "--material-fab-menu-{$key}";
|
|
@endphp
|
|
|
|
<div x-data="materialMenu" data-md-fab-menu {{ $attributes }}>
|
|
<span x-ref="trigger" data-md-fab-menu-anchor style="anchor-name: {{ $anchor }}"
|
|
x-on:click="toggle('first')"
|
|
x-on:keydown.down.prevent="open('first')"
|
|
x-on:keydown.up.prevent="open('last')"
|
|
>
|
|
<button type="button" class="md-state-layer md-focus-ring" aria-label="{{ $label }}" data-md-fab-menu-trigger data-md-color="{{ $color }}">
|
|
<x-livewire-material::icon :name="$icon" filled data-md-fab-menu-icon />
|
|
<x-livewire-material::icon name="close" filled size="20" data-md-fab-menu-close-icon />
|
|
</button>
|
|
</span>
|
|
|
|
<div
|
|
x-ref="menu"
|
|
{{ new \Illuminate\View\ComponentAttributeBag(['wire:key' => 'material-fab-menu']) }}
|
|
id="material-fab-menu-{{ $key }}"
|
|
popover="auto"
|
|
role="menu"
|
|
data-md-fab-menu-popover
|
|
data-md-popover-exit
|
|
data-md-position="{{ $position }}"
|
|
aria-label="{{ $label }}"
|
|
tabindex="-1"
|
|
style="position-anchor: {{ $anchor }}"
|
|
x-on:keydown="navigate($event)"
|
|
x-on:click="activate($event)"
|
|
>
|
|
{{ $slot }}
|
|
</div>
|
|
</div>
|