Draw the menu item's state layer and focus ring from the classes
Plan step 36 ("Interaction is the shared classes", the user, 2026-09-14).
menu-item.css hand-rolled md-state-layer and md-focus-ring, including a
separate aria-disabled rule the class already carries; the row now renders
the classes and keeps only the inward -3px ring offset a row edge to edge
in the list needs. No md-touch-target: the row is already 48px tall.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
7803298fe7
commit
6c30f1e3ae
@@ -16,7 +16,11 @@
|
|||||||
* (`ItemTrailingSupportingTextFont`) sits at the end. Every quiet part of the row — the icon, the
|
* (`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
|
* 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
|
* two springs, not one (§ ACT-19), and `icon-class` paints only the leading icon, over that ink
|
||||||
* but never over disabled.
|
* 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
|
* `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
|
* it, anchored to its end and flipping to the start where the window has no room
|
||||||
@@ -37,8 +41,6 @@
|
|||||||
[data-md-menu-item] {
|
[data-md-menu-item] {
|
||||||
--md-menu-item-ink: var(--md-sys-color-on-surface-variant);
|
--md-menu-item-ink: var(--md-sys-color-on-surface-variant);
|
||||||
|
|
||||||
position: relative;
|
|
||||||
isolation: isolate;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
min-block-size: var(--md-sys-measurement-space600);
|
min-block-size: var(--md-sys-measurement-space600);
|
||||||
@@ -50,7 +52,6 @@
|
|||||||
color: var(--md-sys-color-on-surface);
|
color: var(--md-sys-color-on-surface);
|
||||||
text-align: start;
|
text-align: start;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition-property: border-radius, background-color, color;
|
transition-property: border-radius, background-color, color;
|
||||||
@@ -61,41 +62,10 @@
|
|||||||
var(--md-sys-motion-spatial-fast),
|
var(--md-sys-motion-spatial-fast),
|
||||||
var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast);
|
var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast);
|
||||||
|
|
||||||
&::before {
|
/* The row sits edge to edge in the list, so the ring reads inward, not past the item. */
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: -1;
|
|
||||||
border-radius: inherit;
|
|
||||||
background-color: currentColor;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
&:hover::before {
|
|
||||||
opacity: var(--md-sys-state-hover-state-layer-opacity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: 3px solid var(--md-sys-color-secondary);
|
|
||||||
outline-offset: -3px;
|
outline-offset: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible::before {
|
|
||||||
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active::before {
|
|
||||||
opacity: var(--md-sys-state-pressed-state-layer-opacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* A disabled item stays focusable and keeps its focus ring, but has no state layer. */
|
|
||||||
&[aria-disabled='true']::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-md-menu-item]:first-child {
|
[data-md-menu-item]:first-child {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@
|
|||||||
'wire:navigate' => $isLink && ! $external && ! $noWireNavigate && ! $attributes->has('wire:navigate') ? true : null,
|
'wire:navigate' => $isLink && ! $external && ! $noWireNavigate && ! $attributes->has('wire:navigate') ? true : null,
|
||||||
// Opening a submenu is not choosing anything: the outer menu stays where it was.
|
// Opening a submenu is not choosing anything: the outer menu stays where it was.
|
||||||
'data-md-keep-open' => $keepOpen || $submenu ? true : null,
|
'data-md-keep-open' => $keepOpen || $submenu ? true : null,
|
||||||
], fn ($value): bool => $value !== null));
|
], fn ($value): bool => $value !== null))->class(['md-state-layer', 'md-focus-ring']);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@if ($submenu)
|
@if ($submenu)
|
||||||
|
|||||||
@@ -130,8 +130,11 @@ it('links an item, and keeps a disabled one out of reach', function () {
|
|||||||
->assertSee('aria-disabled="true"', false)
|
->assertSee('aria-disabled="true"', false)
|
||||||
->assertSee('data-md-menu-item', false);
|
->assertSee('data-md-menu-item', false);
|
||||||
|
|
||||||
|
// A disabled item stays focusable and keeps its ring, but the foundation's md-state-layer class
|
||||||
|
// (which the item renders, not a copy of its own) already withholds the layer on aria-disabled.
|
||||||
expect(ComponentStylesheet::read('menu-item')->declarations("[data-md-menu-item][aria-disabled='true']"))->toMatchArray(['pointer-events' => 'none'])
|
expect(ComponentStylesheet::read('menu-item')->declarations("[data-md-menu-item][aria-disabled='true']"))->toMatchArray(['pointer-events' => 'none'])
|
||||||
->and(ComponentStylesheet::read('menu-item')->declarations("[data-md-menu-item][aria-disabled='true']::before"))->toBe(['display' => 'none']);
|
->and(ComponentStylesheet::read('menu-item')->has('[data-md-menu-item]::before'))->toBeFalse()
|
||||||
|
->and((string) $this->blade('<x-menu-item label="Reset" disabled />'))->toContain('class="md-state-layer md-focus-ring"');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('separates and labels groups', function () {
|
it('separates and labels groups', function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user