<x-menu> named its CSS anchor on the <span> around the trigger slot. A trigger taken out of the flow, such as <x-button fab> fixed to the bottom corner of a phone's window, left that span behind as an empty box where the page put it, and the menu opened there. menu.js now moves the name onto the menu button, beside any name the button already carries for its tooltip, and moves it again whenever a Livewire morph puts the server's attributes and a fresh name back. The wrapper keeps the name until Alpine starts, or when there is no menu button. A menu that fits neither below nor above its start edge now also tries the opposite side and end together, so a default-position menu on a FAB in the bottom-right corner opens above it, end-aligned. Before, it fell back to its base position and overflowed the window. This also applies to <x-fab-menu>, <x-account-menu>, <x-split-button> and the <x-section-nav> picker, which share menu.js. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
75 lines
3.7 KiB
PHP
75 lines
3.7 KiB
PHP
{{-- An M3 Expressive menu: a list of actions that opens from a trigger.
|
|
|
|
<x-menu label="Share actions">
|
|
<x-slot:trigger>
|
|
<x-button icon="more_vert" tooltip="More" />
|
|
</x-slot:trigger>
|
|
|
|
<x-menu-item label="Copy link" icon="content_copy" wire:click="copy" />
|
|
<x-menu-separator />
|
|
<x-menu-item label="Delete" icon="delete" wire:click="delete" />
|
|
</x-menu>
|
|
|
|
The trigger's first button or link becomes the menu button (aria-haspopup, aria-expanded,
|
|
aria-controls). The list is a `popover="auto"` in the top layer, placed by CSS anchor
|
|
positioning at `position` (`bottom-start`, `bottom-end`, `top-start`, `top-end`) and flipping
|
|
when there is no room — to the other side, the other end, or both, so a menu on a FAB in a
|
|
corner of the window opens back across it; a click outside or Escape closes it. The keyboard
|
|
is WAI-ARIA's menu button: Enter, Space or ArrowDown open on the first item, ArrowUp on the
|
|
last; arrows, Home, End and typing a letter move between items; Tab closes; activating an item
|
|
closes the menu unless the item says `keep-open`, and Escape returns focus to the trigger.
|
|
|
|
The anchor name is rendered on the wrapper around the trigger slot, the only element the
|
|
server can name, and resources/js/menu.js moves it onto the menu button itself: a trigger
|
|
that is `position: fixed` (`<x-button fab>` on a phone) leaves the wrapper behind as an empty
|
|
box where the page put it, and the menu opened there.
|
|
|
|
The container is Expressive's standard menu (surface-container-low, 16px corner, elevation
|
|
2), or `vibrant` in tertiary-container — StandardMenuTokens and VibrantMenuTokens from
|
|
androidx Compose Material 3 (Apache-2.0). --}}
|
|
|
|
@props([
|
|
'label' => null,
|
|
'position' => 'bottom-start',
|
|
'vibrant' => false,
|
|
])
|
|
|
|
@php
|
|
$position = in_array($position, ['bottom-start', 'bottom-end', 'top-start', 'top-end'], true) ? $position : 'bottom-start';
|
|
$key = \Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10));
|
|
$anchor = "--material-menu-{$key}";
|
|
@endphp
|
|
|
|
<div x-data="materialMenu" {{ $attributes->class('relative inline-flex') }}>
|
|
<span x-ref="trigger" class="inline-flex" style="anchor-name: {{ $anchor }}"
|
|
x-on:click="toggle('first')"
|
|
x-on:keydown.down.prevent="open('first')"
|
|
x-on:keydown.up.prevent="open('last')"
|
|
>{{ $trigger }}</span>
|
|
|
|
<div
|
|
x-ref="menu"
|
|
id="material-menu-{{ $key }}"
|
|
popover="auto"
|
|
role="menu"
|
|
@if ($label) aria-label="{{ $label }}" @endif
|
|
tabindex="-1"
|
|
style="position-anchor: {{ $anchor }}"
|
|
x-on:keydown="navigate($event)"
|
|
x-on:click="activate($event)"
|
|
@class([
|
|
'm-0 min-w-28 max-w-70 overflow-visible border-0 p-1 rounded-corner-lg shadow-elevation-2 [inset:auto]',
|
|
'my-1 [position-try-fallbacks:flip-block,flip-inline,flip-block_flip-inline]',
|
|
'opacity-0 transition-[opacity,translate,display,overlay] transition-discrete duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast open:opacity-100 starting:open:opacity-0',
|
|
'bg-surface-container-low text-on-surface' => ! $vibrant,
|
|
'bg-tertiary-container text-on-tertiary-container' => $vibrant,
|
|
'[position-area:bottom_span-right]' => $position === 'bottom-start',
|
|
'[position-area:bottom_span-left]' => $position === 'bottom-end',
|
|
'[position-area:top_span-right]' => $position === 'top-start',
|
|
'[position-area:top_span-left]' => $position === 'top-end',
|
|
])
|
|
>
|
|
{{ $slot }}
|
|
</div>
|
|
</div>
|