Draw the menu item without Tailwind
<x-menu-item> renders data-md-menu-item, data-md-description and its aria-* state, with data-md-keep-open renamed from data-keep-open; menu-item.css draws SegmentedMenuTokens' 48px row, 16px sides, 12px gaps, 4/12px corners, the selected/current colours and the shared --md-menu-item-ink icon-class outranks except when disabled (ACT-11/19/27/28); a submenu's popover shares its container colour with the menu it opens from and menu.js follows the rename (plan step 36, actions). 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
2f8fb2183f
commit
c040925cd4
@@ -21,6 +21,7 @@ dataset('action components', [
|
||||
'fab',
|
||||
'menu-separator',
|
||||
'menu-group',
|
||||
'menu-item',
|
||||
]);
|
||||
|
||||
it('draws the component from a stylesheet shaped like every package stylesheet', function (string $name) {
|
||||
|
||||
@@ -76,7 +76,7 @@ it('keeps two trailing icon buttons below medium and four from it, the rest in a
|
||||
->and(preg_match_all('/role="menu"\s+data-menu\s+aria-label="More options"/', $html))->toBe(2)
|
||||
// Below medium: all but the first.
|
||||
->and($compact)
|
||||
->toMatch('/<a role="menuitem" tabindex="-1" href="\/shares\/1\/share"/')
|
||||
->toMatch('/<a data-md-menu-item[^>]*role="menuitem" tabindex="-1" href="\/shares\/1\/share"/')
|
||||
->toMatch('/role="menuitemcheckbox" aria-checked="true"[^>]*wire:click="star"/')
|
||||
->toMatch('/role="menuitem"[^>]*wire:click="archive"/')
|
||||
->toMatch('/role="menuitem" aria-disabled="true"/')
|
||||
|
||||
@@ -41,6 +41,7 @@ it('draws an item as a menuitem button', function () {
|
||||
$html = (string) $this->blade('<x-menu-item label="Download" icon="download" shortcut="⌘D" description="As a ZIP" wire:click="download" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('data-md-menu-item')
|
||||
->toContain('role="menuitem"')
|
||||
->toContain('tabindex="-1"')
|
||||
->toContain('type="button"')
|
||||
@@ -48,7 +49,8 @@ it('draws an item as a menuitem button', function () {
|
||||
->toContain('Download')
|
||||
->toContain('⌘D')
|
||||
->toContain('As a ZIP')
|
||||
->toContain('size-5 text-on-surface-variant');
|
||||
->toContain('data-md-menu-item-description')
|
||||
->toContain('data-md-menu-item-shortcut');
|
||||
});
|
||||
|
||||
it('makes a selectable item a menuitemcheckbox, ticked at its end', function () {
|
||||
@@ -58,9 +60,12 @@ it('makes a selectable item a menuitemcheckbox, ticked at its end', function ()
|
||||
expect((string) $this->blade('<x-menu-item label="Newest" :selected="true" keep-open />'))
|
||||
->toContain('role="menuitemcheckbox"')
|
||||
->toContain('aria-checked="true"')
|
||||
->toContain('bg-tertiary-container')
|
||||
->toContain('data-keep-open')
|
||||
->toContain('data-md-keep-open')
|
||||
->toContain($check)
|
||||
->and(ComponentStylesheet::read('menu-item')->declarations("[data-md-menu-item][aria-checked='true']"))->toMatchArray([
|
||||
'background-color' => 'var(--md-sys-color-tertiary-container)',
|
||||
'color' => 'var(--md-sys-color-on-tertiary-container)',
|
||||
])
|
||||
->and((string) $this->blade('<x-menu-item label="Largest" :selected="false" />'))
|
||||
->toContain('aria-checked="false"')
|
||||
->not->toContain($check)
|
||||
@@ -69,7 +74,11 @@ it('makes a selectable item a menuitemcheckbox, ticked at its end', function ()
|
||||
});
|
||||
|
||||
it('gives an item M3\'s 48px row and 16px sides, and the separator its 8px', function () {
|
||||
expect((string) $this->blade('<x-menu-item label="Copy" />'))->toContain('min-h-12')->toContain('px-4')
|
||||
expect((string) $this->blade('<x-menu-item label="Copy" />'))->toContain('data-md-menu-item')
|
||||
->and(ComponentStylesheet::read('menu-item')->declarations('[data-md-menu-item]'))->toMatchArray([
|
||||
'min-block-size' => 'var(--md-sys-measurement-space600)',
|
||||
'padding-inline' => 'var(--md-sys-measurement-space200)',
|
||||
])
|
||||
->and(trim((string) $this->blade('<x-menu-separator />')))->toBe('<hr role="separator" data-md-menu-separator />')
|
||||
->and(ComponentStylesheet::read('menu-separator')->declarations('[data-md-menu-separator]'))->toBe(['block-size' => '1px', 'margin-block' => 'var(--md-sys-measurement-space100)', 'margin-inline' => 'var(--md-sys-measurement-space200)', 'border-width' => '0', 'background-color' => 'var(--md-sys-color-outline-variant)'])
|
||||
->and((string) $this->blade('<x-menu-group label="Sort by" />'))->toContain('data-md-menu-group-label')
|
||||
@@ -86,11 +95,15 @@ it('marks the page an item leads to, and carries a badge', function () {
|
||||
->toContain('role="menuitem"')
|
||||
->toContain('aria-current="page"')
|
||||
->not->toContain('aria-checked')
|
||||
->toContain('bg-secondary-container text-on-secondary-container')
|
||||
->toContain('data-md-badge')
|
||||
->toContain('>3<')
|
||||
->and(ComponentStylesheet::read('menu-item')->declarations("[data-md-menu-item][aria-current='page']:not([aria-checked='true'])"))->toMatchArray([
|
||||
'background-color' => 'var(--md-sys-color-secondary-container)',
|
||||
'color' => 'var(--md-sys-color-on-secondary-container)',
|
||||
])
|
||||
->and((string) $this->blade('<x-menu-item label="Users" link="/admin/users" />'))
|
||||
->not->toContain('aria-current')
|
||||
->not->toContain('secondary-container');
|
||||
->not->toContain('data-md-badge');
|
||||
});
|
||||
|
||||
it('links an item, and keeps a disabled one out of reach', function () {
|
||||
@@ -100,7 +113,9 @@ it('links an item, and keeps a disabled one out of reach', function () {
|
||||
|
||||
$this->blade('<x-menu-item label="Reset" disabled />')
|
||||
->assertSee('aria-disabled="true"', false)
|
||||
->assertSee('pointer-events-none', false);
|
||||
->assertSee('data-md-menu-item', false);
|
||||
|
||||
expect(ComponentStylesheet::read('menu-item')->declarations("[data-md-menu-item][aria-disabled='true']"))->toMatchArray(['pointer-events' => 'none']);
|
||||
});
|
||||
|
||||
it('separates and labels groups', function () {
|
||||
@@ -136,15 +151,19 @@ it('adds icon-class to the leading icon, over its own colour but not over disabl
|
||||
$leading = fn (string $html): string => preg_match('/<svg[^>]*class="([^"]*)"/', $html, $icon) ? $icon[1] : '';
|
||||
|
||||
expect($leading((string) $this->blade('<x-menu-item label="Running plan" icon="directions_run" icon-class="text-sport-run" icon-right="chevron_right" />')))
|
||||
->toBe('size-5 [:where(&)]:text-on-surface-variant text-sport-run')
|
||||
->toBe('text-sport-run')
|
||||
->and($leading((string) $this->blade('<x-menu-item label="Running plan" icon="directions_run" icon-class="text-sport-run" :selected="true" />')))
|
||||
->toBe('size-5 [:where(&)]:text-on-tertiary-container text-sport-run')
|
||||
->toBe('text-sport-run')
|
||||
->and($leading((string) $this->blade('<x-menu-item label="Running plan" icon="directions_run" icon-class="text-sport-run" disabled />')))
|
||||
->toBe('size-5 text-sport-run text-on-surface/38!')
|
||||
->toBe('text-sport-run')
|
||||
->and($leading((string) $this->blade('<x-menu-item label="Running plan" icon="directions_run" />')))
|
||||
->toBe('size-5 text-on-surface-variant')
|
||||
->toBe('')
|
||||
->and((string) $this->blade('<x-menu-item label="Next" icon-right="chevron_right" icon-class="text-sport-run" />'))
|
||||
->not->toContain('text-sport-run');
|
||||
->not->toContain('text-sport-run')
|
||||
// icon-class is a caller class, unlayered, so it always outranks the item's own layered
|
||||
// ink — except for a disabled item, whose icon rule is !important.
|
||||
->and(ComponentStylesheet::read('menu-item')->declarations('[data-md-menu-item] [data-md-icon]'))->toBe(['color' => 'var(--md-menu-item-ink)'])
|
||||
->and(ComponentStylesheet::read('menu-item')->declarations("[data-md-menu-item][aria-disabled='true'] [data-md-icon]"))->toBe(['color' => 'color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent) !important']);
|
||||
});
|
||||
|
||||
it('opens a submenu beside the item that holds it', function () {
|
||||
@@ -166,13 +185,15 @@ it('opens a submenu beside the item that holds it', function () {
|
||||
->toContain("aria-controls=\"material-submenu-{$anchor[2]}\"")
|
||||
->toContain("id=\"material-submenu-{$anchor[2]}\"")
|
||||
->toContain("position-anchor: {$anchor[1]}")
|
||||
->toContain('data-submenu')
|
||||
->toContain('data-md-submenu')
|
||||
->toContain('aria-label="Send to"')
|
||||
->toContain('[position-area:inline-end_span-block-end]')
|
||||
->toContain('[position-try-fallbacks:flip-inline]')
|
||||
// Opening a submenu chooses nothing, so the menu around it stays where it was.
|
||||
->toContain('data-keep-open')
|
||||
->toContain('A person');
|
||||
->toContain('data-md-keep-open')
|
||||
->toContain('A person')
|
||||
->and(ComponentStylesheet::read('menu-item')->declarations('[data-md-submenu]'))->toMatchArray([
|
||||
'position-area' => 'inline-end span-block-end',
|
||||
'position-try-fallbacks' => 'flip-inline',
|
||||
]);
|
||||
});
|
||||
|
||||
it('marks a submenu item with a chevron instead of a tick', function () {
|
||||
@@ -181,7 +202,8 @@ it('marks a submenu item with a chevron instead of a tick', function () {
|
||||
|
||||
expect((string) $this->blade('<x-menu-item label="Export as" submenu><x-menu-item label="ZIP" /></x-menu-item>'))
|
||||
->toContain($chevron)
|
||||
->toContain('rtl:-scale-x-100')
|
||||
->toContain('data-md-submenu-chevron')
|
||||
->toContain('data-md-mirror-rtl')
|
||||
->and((string) $this->blade('<x-menu-item label="Export as" icon-right="download" submenu><x-menu-item label="ZIP" /></x-menu-item>'))
|
||||
->not->toContain($chevron);
|
||||
});
|
||||
@@ -322,8 +344,8 @@ it('marks a submenu\'s chevron, which turns to point down at the list a sheet op
|
||||
[$popover, $sheet] = explode('<template x-teleport="body">', $html);
|
||||
|
||||
// One markup for both: menu.js tells the sheet's copy apart by the list around it.
|
||||
expect($popover)->toContain('data-submenu-chevron')->toContain('x-data="materialSubmenu"')->toContain('data-submenu')
|
||||
->and($sheet)->toContain('data-submenu-chevron')->toContain('x-data="materialSubmenu"')->toContain('aria-haspopup="menu"')->toContain('data-submenu')
|
||||
expect($popover)->toContain('data-md-submenu-chevron')->toContain('x-data="materialSubmenu"')->toContain('data-md-submenu')
|
||||
->and($sheet)->toContain('data-md-submenu-chevron')->toContain('x-data="materialSubmenu"')->toContain('aria-haspopup="menu"')->toContain('data-md-submenu')
|
||||
->and((string) $this->blade('<x-menu-item label="Export as" icon-right="download" submenu><x-menu-item label="ZIP" /></x-menu-item>'))
|
||||
->not->toContain('data-submenu-chevron');
|
||||
->not->toContain('data-md-submenu-chevron');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user