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>
155 lines
6.3 KiB
CSS
155 lines
6.3 KiB
CSS
/*
|
|
* <x-fab-menu>: an M3 Expressive FAB that opens into a short list of related actions.
|
|
*
|
|
* FabMenuBaselineTokens (androidx Compose Material 3, Apache-2.0): the trigger is a 56px FAB,
|
|
* `data-md-color`'s container, large corner, elevation 3, rising to elevation 4 under a hovering
|
|
* pointer; while its list is open (`aria-expanded`) it turns fully round and paints the colour
|
|
* itself, on the default spatial and effects springs (ACT-19 splits the two). It renders the
|
|
* foundation's `md-state-layer` and `md-focus-ring` (foundation/interaction.css); its 56px already
|
|
* meets M3's target, so it needs no `md-touch-target`. The list sits
|
|
* `data-md-position`'s corner of the trigger, 8px padding — the room the close button keeps below
|
|
* the first item — and scrolls past `calc(100dvh - 128px)` on a short window, behind the close
|
|
* button, which stays fixed (ACT-32).
|
|
*
|
|
* The items (fab-menu-item.css) rise into place as the list opens and sink back as it closes: the
|
|
* entry is `@starting-style` on the item itself; the exit is the popover's exit copy
|
|
* (`data-md-popover-exit`, resources/js/popover-exit.js), which stays on screen after the popover
|
|
* has closed, so the items have somewhere to animate to (ACT-33) in every engine, Firefox included.
|
|
* `[data-md-fab-menu-popover]:not(:popover-open) > *` and the copy's `[data-md-popover-closing] > *`
|
|
* are the closed values; inside the layer they beat fab-menu-item.css's resting `translate: 0 0`
|
|
* and `opacity: 1` on specificity — two attributes, or an attribute and a pseudo-class, against the
|
|
* item's one attribute — whichever order the two files are bundled in.
|
|
*/
|
|
|
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
|
|
|
@import './icon.css';
|
|
|
|
@layer material.components {
|
|
[data-md-fab-menu] {
|
|
position: relative;
|
|
display: inline-flex;
|
|
}
|
|
|
|
[data-md-fab-menu-anchor] {
|
|
display: inline-flex;
|
|
}
|
|
|
|
[data-md-fab-menu-trigger] {
|
|
--md-fab-menu-container: var(--md-sys-color-primary-container);
|
|
--md-fab-menu-on-container: var(--md-sys-color-on-primary-container);
|
|
--md-fab-menu-color: var(--md-sys-color-primary);
|
|
--md-fab-menu-on-color: var(--md-sys-color-on-primary);
|
|
|
|
display: inline-flex;
|
|
flex-shrink: 0;
|
|
align-items: center;
|
|
justify-content: center;
|
|
inline-size: var(--md-sys-measurement-space700);
|
|
block-size: var(--md-sys-measurement-space700);
|
|
border-radius: var(--md-sys-shape-corner-lg);
|
|
background-color: var(--md-fab-menu-container);
|
|
color: var(--md-fab-menu-on-container);
|
|
box-shadow: var(--md-sys-elevation-3);
|
|
cursor: pointer;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
transition-property: border-radius, background-color, color, box-shadow;
|
|
transition-duration:
|
|
var(--md-sys-motion-spatial-default-duration), var(--md-sys-motion-effects-default-duration),
|
|
var(--md-sys-motion-effects-default-duration), var(--md-sys-motion-effects-default-duration);
|
|
transition-timing-function:
|
|
var(--md-sys-motion-spatial-default), var(--md-sys-motion-effects-default),
|
|
var(--md-sys-motion-effects-default), var(--md-sys-motion-effects-default);
|
|
|
|
@media (hover: hover) {
|
|
&:hover {
|
|
box-shadow: var(--md-sys-elevation-4);
|
|
}
|
|
}
|
|
|
|
&[aria-expanded='true'] {
|
|
border-radius: var(--md-sys-shape-corner-full);
|
|
background-color: var(--md-fab-menu-color);
|
|
color: var(--md-fab-menu-on-color);
|
|
}
|
|
}
|
|
|
|
[data-md-fab-menu-trigger][data-md-color='secondary'] {
|
|
--md-fab-menu-container: var(--md-sys-color-secondary-container);
|
|
--md-fab-menu-on-container: var(--md-sys-color-on-secondary-container);
|
|
--md-fab-menu-color: var(--md-sys-color-secondary);
|
|
--md-fab-menu-on-color: var(--md-sys-color-on-secondary);
|
|
}
|
|
|
|
[data-md-fab-menu-trigger][data-md-color='tertiary'] {
|
|
--md-fab-menu-container: var(--md-sys-color-tertiary-container);
|
|
--md-fab-menu-on-container: var(--md-sys-color-on-tertiary-container);
|
|
--md-fab-menu-color: var(--md-sys-color-tertiary);
|
|
--md-fab-menu-on-color: var(--md-sys-color-on-tertiary);
|
|
}
|
|
|
|
[data-md-fab-menu-close-icon] {
|
|
display: none;
|
|
}
|
|
|
|
[data-md-fab-menu-trigger][aria-expanded='true'] [data-md-fab-menu-icon] {
|
|
display: none;
|
|
}
|
|
|
|
[data-md-fab-menu-trigger][aria-expanded='true'] [data-md-fab-menu-close-icon] {
|
|
display: inline-flex;
|
|
}
|
|
|
|
[data-md-fab-menu-popover] {
|
|
inset: auto;
|
|
margin: 0;
|
|
display: none;
|
|
flex-direction: column;
|
|
gap: var(--md-sys-measurement-space50);
|
|
max-block-size: calc(100dvh - 128px);
|
|
overflow-y: auto;
|
|
border-width: 0;
|
|
background-color: transparent;
|
|
padding: var(--md-sys-measurement-space100);
|
|
|
|
&:popover-open {
|
|
display: flex;
|
|
}
|
|
}
|
|
|
|
[data-md-fab-menu-popover][data-md-position='top-end'] {
|
|
margin-inline: calc(-1 * var(--md-sys-measurement-space100));
|
|
margin-block-start: calc(-1 * var(--md-sys-measurement-space100));
|
|
align-items: flex-end;
|
|
position-area: top span-left;
|
|
}
|
|
|
|
[data-md-fab-menu-popover][data-md-position='top-start'] {
|
|
margin-inline: calc(-1 * var(--md-sys-measurement-space100));
|
|
margin-block-start: calc(-1 * var(--md-sys-measurement-space100));
|
|
align-items: flex-start;
|
|
position-area: top span-right;
|
|
}
|
|
|
|
[data-md-fab-menu-popover][data-md-position='bottom-end'] {
|
|
margin-inline: calc(-1 * var(--md-sys-measurement-space100));
|
|
margin-block-end: calc(-1 * var(--md-sys-measurement-space100));
|
|
align-items: flex-end;
|
|
position-area: bottom span-left;
|
|
}
|
|
|
|
[data-md-fab-menu-popover][data-md-position='bottom-start'] {
|
|
margin-inline: calc(-1 * var(--md-sys-measurement-space100));
|
|
margin-block-end: calc(-1 * var(--md-sys-measurement-space100));
|
|
align-items: flex-start;
|
|
position-area: bottom span-right;
|
|
}
|
|
|
|
[data-md-fab-menu-popover]:not(:popover-open) > *,
|
|
[data-md-fab-menu-popover][data-md-popover-closing] > * {
|
|
translate: 0 var(--md-sys-measurement-space100);
|
|
opacity: 0;
|
|
}
|
|
}
|