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>
185 lines
7.9 KiB
CSS
185 lines
7.9 KiB
CSS
/*
|
|
* <x-menu-item>: one row in an `<x-menu>` — an action, a link, or a choice — and, with `submenu`,
|
|
* the trigger and popover of a nested menu beside it.
|
|
*
|
|
* SegmentedMenuTokens (androidx Compose Material 3, Apache-2.0): 48px row (M3's published "List
|
|
* item height" wins over the token's own 44dp `Item`, docs/audits/m3-alignment/actions.md §
|
|
* ACT-28), 16px either side (`ItemLeadingSpace`/`ItemTrailingSpace`, § ACT-11), 12px between the
|
|
* icon, the label and the trailing content (`ItemBetweenSpace`, off the 8dp grid), 4px corners
|
|
* (`ItemShape`) that open to 12px at either end of the list (`ItemFirstChildShape`/
|
|
* `ItemLastChildShape` — the library's own list ends, 12px, win over `GroupShape`'s 8dp so a
|
|
* cluster's ends and a list's ends match, menu-group.css). A selected row
|
|
* (`role="menuitemcheckbox"`, the tick beside the colour and shape § ACT-27) takes
|
|
* `ItemSelectedShape`'s 12px corner in tertiary-container; `current` — a menu of places rather
|
|
* than choices — takes the same 12px corner in secondary-container, the navigation-indicator
|
|
* role. `description` (`ItemSupportingTextFont`) grows the row by 8px top and bottom; `shortcut`
|
|
* (`ItemTrailingSupportingTextFont`) sits at the end. Every quiet part of the row — the icon, the
|
|
* description, the trailing text — inks together as `--md-menu-item-ink`; corner and colour are
|
|
* two springs, not one (§ ACT-19), and `icon-class` paints only the leading icon, over that ink
|
|
* but never over disabled. The row renders the foundation's `md-state-layer` and `md-focus-ring`
|
|
* (foundation/interaction.css) — no `md-touch-target`, since the row is already 48px tall — and
|
|
* keeps only one refinement: the ring reads inward (-3px), because a row sits edge to edge in the
|
|
* list and an outward one would run past it. A disabled item stays focusable and keeps its ring,
|
|
* because M3 keeps it reachable, but the class already withholds the layer from `aria-disabled`.
|
|
*
|
|
* `submenu` turns the row into the WAI-ARIA menu button for a second `role="menu"` popover beside
|
|
* it, anchored to its end and flipping to the start where the window has no room
|
|
* (resources/js/menu.js). The popover shares its container colour with the menu it opens from
|
|
* through `--material-menu-surface`/`--material-menu-ink` (menu.css defines them on
|
|
* `[data-md-menu-popover]`; the fallbacks here are the standard menu's, for a submenu inside some
|
|
* other list, e.g. a FAB menu's). Inside an `<x-menu sheet-at-compact>` sheet the same markup
|
|
* opens in place instead — menu.css restyles `[data-md-submenu]` and `[data-md-submenu-chevron]`
|
|
* there.
|
|
*/
|
|
|
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
|
|
|
@import './icon.css';
|
|
@import './badge.css';
|
|
|
|
@layer material.components {
|
|
[data-md-menu-item] {
|
|
--md-menu-item-ink: var(--md-sys-color-on-surface-variant);
|
|
|
|
display: flex;
|
|
inline-size: 100%;
|
|
min-block-size: var(--md-sys-measurement-space600);
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding-inline: var(--md-sys-measurement-space200);
|
|
border-radius: var(--md-sys-shape-corner-xs);
|
|
background-color: transparent;
|
|
color: var(--md-sys-color-on-surface);
|
|
text-align: start;
|
|
cursor: pointer;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
transition-property: border-radius, background-color, color;
|
|
transition-duration:
|
|
var(--md-sys-motion-spatial-fast-duration),
|
|
var(--md-sys-motion-effects-fast-duration), var(--md-sys-motion-effects-fast-duration);
|
|
transition-timing-function:
|
|
var(--md-sys-motion-spatial-fast),
|
|
var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast);
|
|
|
|
/* The row sits edge to edge in the list, so the ring reads inward, not past the item. */
|
|
&:focus-visible {
|
|
outline-offset: -3px;
|
|
}
|
|
}
|
|
|
|
[data-md-menu-item]:first-child {
|
|
border-start-start-radius: var(--md-sys-shape-corner-md);
|
|
border-start-end-radius: var(--md-sys-shape-corner-md);
|
|
}
|
|
|
|
[data-md-menu-item]:last-child {
|
|
border-end-start-radius: var(--md-sys-shape-corner-md);
|
|
border-end-end-radius: var(--md-sys-shape-corner-md);
|
|
}
|
|
|
|
[data-md-menu-item][data-md-description] {
|
|
padding-block: var(--md-sys-measurement-space100);
|
|
}
|
|
|
|
[data-md-menu-item][aria-checked='true'] {
|
|
--md-menu-item-ink: var(--md-sys-color-on-tertiary-container);
|
|
|
|
border-radius: var(--md-sys-shape-corner-md);
|
|
background-color: var(--md-sys-color-tertiary-container);
|
|
color: var(--md-sys-color-on-tertiary-container);
|
|
}
|
|
|
|
[data-md-menu-item][aria-current='page']:not([aria-checked='true']) {
|
|
--md-menu-item-ink: var(--md-sys-color-on-secondary-container);
|
|
|
|
border-radius: var(--md-sys-shape-corner-md);
|
|
background-color: var(--md-sys-color-secondary-container);
|
|
color: var(--md-sys-color-on-secondary-container);
|
|
}
|
|
|
|
[data-md-menu-item][aria-disabled='true'] {
|
|
--md-menu-item-ink: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
|
|
|
pointer-events: none;
|
|
color: var(--md-menu-item-ink);
|
|
}
|
|
|
|
[data-md-menu-item] [data-md-icon] {
|
|
color: var(--md-menu-item-ink);
|
|
}
|
|
|
|
[data-md-menu-item][aria-disabled='true'] [data-md-icon] {
|
|
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent) !important;
|
|
}
|
|
|
|
[data-md-menu-item-text] {
|
|
min-inline-size: 0;
|
|
flex: 1;
|
|
}
|
|
|
|
[data-md-menu-item-label] {
|
|
display: block;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
font: var(--md-sys-typescale-body-lg);
|
|
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
|
font-variation-settings: normal;
|
|
}
|
|
|
|
[data-md-menu-item-description] {
|
|
display: block;
|
|
color: var(--md-menu-item-ink);
|
|
font: var(--md-sys-typescale-body-md);
|
|
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
|
font-variation-settings: normal;
|
|
}
|
|
|
|
[data-md-menu-item-shortcut] {
|
|
flex-shrink: 0;
|
|
color: var(--md-menu-item-ink);
|
|
font: var(--md-sys-typescale-label-sm);
|
|
letter-spacing: var(--md-sys-typescale-label-sm-tracking);
|
|
font-variation-settings: normal;
|
|
}
|
|
|
|
/* The submenu popover: a menu of its own, sharing the container colour of the list it opens
|
|
from. The fallbacks are the standard menu's, for a submenu inside some other list. */
|
|
[data-md-submenu] {
|
|
inset: auto;
|
|
margin: 0;
|
|
margin-inline: var(--md-sys-measurement-space50);
|
|
min-inline-size: 112px;
|
|
max-inline-size: 280px;
|
|
max-block-size: min(288px, calc(100dvh - 32px));
|
|
overflow-y: auto;
|
|
border-width: 0;
|
|
border-radius: var(--md-sys-shape-corner-lg);
|
|
padding: var(--md-sys-measurement-space50);
|
|
background-color: var(--material-menu-surface, var(--md-sys-color-surface-container-low));
|
|
color: var(--material-menu-ink, var(--md-sys-color-on-surface));
|
|
box-shadow: var(--md-sys-elevation-2);
|
|
transform-origin: top;
|
|
position-area: inline-end span-block-end;
|
|
position-try-fallbacks: flip-inline;
|
|
opacity: 0;
|
|
scale: 0.95;
|
|
transition-property: opacity, scale;
|
|
transition-duration: var(--md-sys-motion-effects-fast-duration), var(--md-sys-motion-spatial-fast-duration);
|
|
transition-timing-function: var(--md-sys-motion-effects-fast), var(--md-sys-motion-spatial-fast);
|
|
|
|
&:popover-open:not([data-md-popover-closing]) {
|
|
opacity: 1;
|
|
scale: 1;
|
|
}
|
|
|
|
@starting-style {
|
|
&:popover-open {
|
|
opacity: 0;
|
|
scale: 0.95;
|
|
}
|
|
}
|
|
}
|
|
}
|