Plan step 22, actions.md § Missing (Menus as a filtering surface): M3's menus page describes a menu that embeds a text field and filters its options as you type, and nothing in the library did that. `<x-menu filter>` renders the field, sticky above the list, and hides the rows the query leaves out — client-side over the items already rendered, so nothing is fetched and a `wire:click` stays where it was. The field keeps the focus and the arrow keys move a highlight it names through `aria-activedescendant`, the APG combobox keyboard `<x-choices searchable>` already uses; Enter chooses the highlighted row, and a query that leaves nothing says so. The list becomes a `role="menu"` inside the popover, because a text field is not something a menu may contain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
260 lines
9.3 KiB
CSS
260 lines
9.3 KiB
CSS
/*
|
|
* The menus: the exposed dropdown a field drops open, and what `<x-menu>` cannot say in utilities.
|
|
*
|
|
* Two lists wear the dropdown, so a form reads as one family:
|
|
*
|
|
* - `<x-select>`'s: the native <select>, opted into the browser's customizable select
|
|
* (`appearance: base-select`). `::picker(select)` is the menu and each <option> a row, while the
|
|
* keyboard, type-to-find, the screen reader and the form value stay the browser's own.
|
|
* - `<x-choices searchable>`'s listbox, which Alpine draws, as `.field-menu` and `.field-option`.
|
|
*
|
|
* M3's menu as its tokens have it: surface-container at elevation 2, extra-small corner, 48px rows in
|
|
* body-large, the chosen row in secondary-container with a tick at its end (so it is not told by colour
|
|
* alone), and the state layer on hover (only where a pointer can hover) and focus.
|
|
*
|
|
* Unlayered, as the rest of the package's component CSS, so a utility on the call site cannot half-undo
|
|
* a row.
|
|
*
|
|
* Where the browser has no customizable select, the select keeps its native list, which
|
|
* `color-scheme` themes. Every rule for the select's own rows is inside `@supports`: a browser that
|
|
* paints option colours into its native list would otherwise show a half-painted one.
|
|
*/
|
|
|
|
/*
|
|
* `<x-menu>` and the submenus inside it.
|
|
*
|
|
* A submenu is a popover of its own nested in the menu's, so it cannot take the container colour
|
|
* from a class the item knows nothing about: the menu declares the pair as custom properties,
|
|
* which a nested popover inherits down the DOM whatever the top layer does with its painting. The
|
|
* fallbacks are the standard menu's, for a submenu in some other list (a FAB menu's).
|
|
*/
|
|
[data-menu] {
|
|
--material-menu-surface: var(--md-sys-color-surface-container-low);
|
|
--material-menu-ink: var(--md-sys-color-on-surface);
|
|
}
|
|
|
|
[data-menu][data-vibrant] {
|
|
--material-menu-surface: var(--md-sys-color-tertiary-container);
|
|
--material-menu-ink: var(--md-sys-color-on-tertiary-container);
|
|
}
|
|
|
|
[data-submenu] {
|
|
background: var(--material-menu-surface, var(--md-sys-color-surface-container-low));
|
|
color: var(--material-menu-ink, var(--md-sys-color-on-surface));
|
|
}
|
|
|
|
/*
|
|
* `<x-menu filter>`: M3's menu as a filtering surface. The field stays put while the list scrolls
|
|
* under it and takes the menu's own container, so a vibrant menu's field is vibrant too. It is a
|
|
* plain <input> with no field chrome — M3's menus embed a text field, not a text field component.
|
|
*/
|
|
[data-menu-filter] {
|
|
position: sticky;
|
|
inset-block-start: 0;
|
|
z-index: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
min-block-size: 3rem;
|
|
padding-inline: 1rem;
|
|
border-block-end: 1px solid var(--md-sys-color-outline-variant);
|
|
background: var(--material-menu-surface, var(--md-sys-color-surface-container-low));
|
|
}
|
|
|
|
[data-menu-filter] input {
|
|
flex: 1;
|
|
min-inline-size: 0;
|
|
border: 0;
|
|
background: transparent;
|
|
padding: 0;
|
|
color: inherit;
|
|
font: var(--md-sys-typescale-body-lg);
|
|
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
|
outline: none;
|
|
}
|
|
|
|
[data-menu-filter] input::placeholder {
|
|
color: color-mix(in srgb, var(--material-menu-ink, var(--md-sys-color-on-surface)) 70%, transparent);
|
|
}
|
|
|
|
/* The filter hides what the query leaves out with the `hidden` attribute; a row's own
|
|
`display: flex` utility would otherwise beat the user agent's `[hidden] { display: none }`. */
|
|
[data-menu] [hidden] {
|
|
display: none;
|
|
}
|
|
|
|
/* `data-active` is the row the arrow keys are on while the focus stays in the field — the state
|
|
layer the roving focus would have drawn, without taking the focus off the field. A chosen or
|
|
current row keeps its own colour. */
|
|
[data-menu] [role^="menuitem"][data-active]:not([aria-checked="true"], [aria-current="page"]) {
|
|
background: color-mix(in srgb, var(--material-menu-ink, var(--md-sys-color-on-surface)) 8%, transparent);
|
|
}
|
|
|
|
.field-menu {
|
|
max-block-size: 18rem;
|
|
overflow-y: auto;
|
|
padding-block: 0.5rem;
|
|
border-radius: var(--md-sys-shape-corner-xs);
|
|
background: var(--md-sys-color-surface-container);
|
|
box-shadow: var(--md-sys-elevation-2);
|
|
}
|
|
|
|
/* As a popover (`<x-choices searchable>`), it hangs under its field, as wide, and goes above only
|
|
when there is no room below. */
|
|
.field-menu[popover] {
|
|
inset: auto;
|
|
top: anchor(bottom);
|
|
left: anchor(left);
|
|
width: anchor-size(width);
|
|
margin: 0.25rem 0;
|
|
border: 0;
|
|
padding-inline: 0;
|
|
color: var(--md-sys-color-on-surface);
|
|
position-try-fallbacks: flip-block;
|
|
}
|
|
|
|
.field-option[aria-disabled="true"] {
|
|
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
|
cursor: default;
|
|
}
|
|
|
|
.field-option {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
min-block-size: 3rem;
|
|
padding-inline: 1rem;
|
|
color: var(--md-sys-color-on-surface);
|
|
font: var(--md-sys-typescale-body-lg);
|
|
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* `data-active` is the row the arrow keys or the pointer are on. */
|
|
.field-option[data-active] {
|
|
background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
|
|
}
|
|
|
|
.field-option[aria-selected="true"] {
|
|
background: var(--md-sys-color-secondary-container);
|
|
color: var(--md-sys-color-on-secondary-container);
|
|
}
|
|
|
|
.field-option[aria-selected="true"][data-active] {
|
|
background: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 8%, var(--md-sys-color-secondary-container));
|
|
}
|
|
|
|
.field-check {
|
|
margin-inline-start: auto;
|
|
}
|
|
|
|
.field-option:not([aria-selected="true"]) .field-check {
|
|
visibility: hidden;
|
|
}
|
|
|
|
@supports (appearance: base-select) {
|
|
select.field-control,
|
|
select.field-control::picker(select) {
|
|
appearance: base-select;
|
|
}
|
|
|
|
/* The menu. The browser anchors it to the select; the select covers its whole
|
|
field (field.css), so the menu is at least the field's width. It hangs under
|
|
the field and goes above only when it does not fit there — the browser's own
|
|
order tries the roomier side first, which opened a menu upwards with room to
|
|
spare under it. It opens with a fade and a grow on the spatial spring and
|
|
closes with a fade, as `<x-menu>` does. */
|
|
select.field-control::picker(select) {
|
|
position-try-order: normal;
|
|
max-block-size: 18rem;
|
|
max-inline-size: calc(100vw - 2rem);
|
|
margin-block: 0.25rem;
|
|
padding-block: 0.5rem;
|
|
border: 0;
|
|
border-radius: var(--md-sys-shape-corner-xs);
|
|
background: var(--md-sys-color-surface-container);
|
|
color: var(--md-sys-color-on-surface);
|
|
box-shadow: var(--md-sys-elevation-2);
|
|
opacity: 0;
|
|
transform-origin: top;
|
|
transition-property: opacity, display, overlay;
|
|
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
|
transition-timing-function: var(--md-sys-motion-effects-fast);
|
|
transition-behavior: allow-discrete;
|
|
}
|
|
|
|
select.field-control:open::picker(select) {
|
|
opacity: 1;
|
|
transition-property: opacity, scale, display, overlay;
|
|
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
|
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
|
}
|
|
|
|
@starting-style {
|
|
select.field-control:open::picker(select) {
|
|
opacity: 0;
|
|
scale: 0.95;
|
|
}
|
|
}
|
|
|
|
select.field-control option {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
min-block-size: 3rem;
|
|
padding-block: 0;
|
|
padding-inline: 1rem;
|
|
background: transparent;
|
|
color: var(--md-sys-color-on-surface);
|
|
font: var(--md-sys-typescale-body-lg);
|
|
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
|
outline: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
select.field-control option:hover {
|
|
background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
|
|
}
|
|
}
|
|
|
|
select.field-control option:focus-visible {
|
|
background: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
|
}
|
|
|
|
select.field-control option:checked {
|
|
background: var(--md-sys-color-secondary-container);
|
|
color: var(--md-sys-color-on-secondary-container);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
select.field-control option:checked:hover {
|
|
background: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 8%, var(--md-sys-color-secondary-container));
|
|
}
|
|
}
|
|
|
|
select.field-control option:checked:focus-visible {
|
|
background: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 10%, var(--md-sys-color-secondary-container));
|
|
}
|
|
|
|
select.field-control option:disabled {
|
|
background: transparent;
|
|
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
|
cursor: default;
|
|
}
|
|
|
|
/* The tick, at the row's end: the browser draws it before the label, and hides
|
|
it on every row but the chosen one. The Material Symbol, as a mask, so it
|
|
takes the row's ink. */
|
|
select.field-control option::checkmark {
|
|
content: "";
|
|
order: 1;
|
|
flex: none;
|
|
inline-size: 1.5rem;
|
|
block-size: 1.5rem;
|
|
margin-inline-start: auto;
|
|
background-color: currentColor;
|
|
mask: url("../../svg/symbols/outlined/check.svg") center / contain no-repeat;
|
|
}
|
|
}
|