Plan step 36 review of 9ff5a7a8. The rewrite dropped the unlayered
selected rule into material.components beside the segmented fill and the
row tint, so on equal or greater weight a selected item in a segmented
list drew surface instead of secondary-container, and hovering, pressing
or focusing any selected row (or a table's selected row) swapped its
fill for a translucent tint. Each fill now names itself
--md-list-row-fill, and the row's layer is its ink mixed over that fill
at the state tokens, which is also C-08's second suggested fix.
A disabled item inks the whole row at the disabled-content token, as the
old root colour did, so the check, the leading slot and the end slot dim
with the text; the three-line padding is a literal 12px like alert.css;
list-rows.js named list.css.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
144 lines
9.0 KiB
PHP
144 lines
9.0 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\File;
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
|
|
|
it('draws a plain list, with dividers on request', function () {
|
|
expect((string) $this->blade('<x-list label="Files" dividers><x-list-item title="a.zip" /></x-list>'))
|
|
->toContain('role="list"')
|
|
->toContain('aria-label="Files"')
|
|
->toContain('data-md-list="plain"')
|
|
->toContain('data-md-dividers')
|
|
->toContain('role="listitem"')
|
|
->and((string) $this->blade('<x-list><x-list-item title="a.zip" /></x-list>'))->not->toContain('data-md-dividers')
|
|
->and((string) $this->blade('<x-list segmented dividers><x-list-item title="a.zip" /></x-list>'))->not->toContain('data-md-dividers');
|
|
});
|
|
|
|
it('draws M3 Expressive\'s segmented list', function () {
|
|
expect((string) $this->blade('<x-list segmented><x-list-item title="a.zip" /></x-list>'))
|
|
->toContain('data-md-list="segmented"');
|
|
});
|
|
|
|
it('grows an item from one to three lines, top-aligning the tallest', function () {
|
|
expect((string) $this->blade('<x-list-item title="a.zip" />'))->toContain('data-md-lines="1"')
|
|
->and((string) $this->blade('<x-list-item title="a.zip" description="248 MB" />'))->toContain('data-md-lines="2"')->toContain('248 MB')
|
|
->and((string) $this->blade('<x-list-item title="a.zip" overline="Protected" description="248 MB" />'))
|
|
->toContain('data-md-lines="3"')
|
|
->toContain('Protected');
|
|
});
|
|
|
|
it('leaves 16px between the item and what leads or trails it', function () {
|
|
$css = ComponentStylesheet::read('list-item');
|
|
|
|
expect($css->declarations('[data-md-list-item]'))
|
|
->toHaveKey('gap', 'var(--md-sys-measurement-space200)')
|
|
->toHaveKey('padding-inline', 'var(--md-sys-measurement-space200)');
|
|
});
|
|
|
|
it('draws the expressive icon size in a segmented list', function () {
|
|
expect((string) $this->blade('<x-list segmented><x-list-item title="a" icon="settings" icon-right="chevron_right" /></x-list>'))
|
|
->toContain('style="--md-icon-size: 20px"')
|
|
->and((string) $this->blade('<x-list><x-list-item title="a" icon="settings" /></x-list>'))
|
|
->not->toContain('--md-icon-size: 20px');
|
|
});
|
|
|
|
it('leads with an icon, an avatar or an image, and trails with text or an icon', function () {
|
|
expect((string) $this->blade('<x-list-item title="a" icon="folder_zip" trailing="3 days" icon-right="chevron_right" />'))
|
|
->toContain('3 days')
|
|
->and(substr_count((string) $this->blade('<x-list-item title="a" icon="folder_zip" icon-right="chevron_right" />'), '<svg'))->toBe(2)
|
|
->and((string) $this->blade('<x-list-item title="Anna" avatar="AM" />'))->toContain('data-md-list-item-avatar')->toContain('>AM</span>')
|
|
->and((string) $this->blade('<x-list-item title="Anna" avatar="/anna.jpg" />'))->toContain('<img src="/anna.jpg"')
|
|
->and((string) $this->blade('<x-list-item title="Photo" image="/p.jpg" />'))->toContain('data-md-list-item-image');
|
|
});
|
|
|
|
it('makes a linked item a row that opens from anywhere', function () {
|
|
expect((string) $this->blade('<x-list-item title="Settings" link="/settings" />'))
|
|
->toContain('data-md-list-row')
|
|
->toContain('href="/settings"')
|
|
->toContain('data-md-list-open')
|
|
->toContain('wire:navigate');
|
|
});
|
|
|
|
it('is a list box of options when it is one a person chooses from', function () {
|
|
expect((string) $this->blade('<x-list selectable label="Recipients"><x-list-item title="Anna" :selected="true" /><x-list-item title="Ben" /></x-list>'))
|
|
->toContain('role="listbox"')
|
|
->toMatch('/role="option"\s+aria-selected="true"/')
|
|
->toMatch('/role="option"\s+aria-selected="false"/')
|
|
->not->toContain('aria-multiselectable')
|
|
->and((string) $this->blade('<x-list selection="multi"><x-list-item title="Anna" /></x-list>'))
|
|
->toContain('role="listbox"')
|
|
->toContain('aria-multiselectable="true"')
|
|
->and((string) $this->blade('<x-list><x-list-item title="Anna" :selected="true" /></x-list>'))
|
|
->toContain('role="list"')
|
|
->toMatch('/role="listitem"\s+aria-current="true"/')
|
|
->not->toContain('aria-selected');
|
|
});
|
|
|
|
it('draws a second cue on a selected option, which icon-right replaces', function () {
|
|
expect(substr_count((string) $this->blade('<x-list selectable><x-list-item title="Anna" :selected="true" /></x-list>'), '<svg'))->toBe(1)
|
|
->and(substr_count((string) $this->blade('<x-list selectable><x-list-item title="Anna" :selected="true" icon-right="chevron_right" /></x-list>'), '<svg'))->toBe(1)
|
|
->and(substr_count((string) $this->blade('<x-list selectable><x-list-item title="Anna" /></x-list>'), '<svg'))->toBe(0)
|
|
->and(substr_count((string) $this->blade('<x-list><x-list-item title="Anna" :selected="true" /></x-list>'), '<svg'))->toBe(0);
|
|
});
|
|
|
|
it('leaves a disabled item out of the keyboard and announces it disabled', function () {
|
|
expect((string) $this->blade('<x-list-item title="Settings" link="/settings" icon="settings" disabled />'))
|
|
->toContain('aria-disabled="true"')
|
|
->not->toContain('href="/settings"')
|
|
->not->toContain('data-md-list-open')
|
|
->not->toContain('data-md-list-row');
|
|
});
|
|
|
|
it('marks a selected item and takes controls in its slots', function () {
|
|
expect((string) $this->blade('<x-list-item title="Anna" :selected="true"><x-slot:leading><input type="checkbox"></x-slot:leading><x-slot:end><button>⋮</button></x-slot:end></x-list-item>'))
|
|
->toContain('data-md-selected')
|
|
->toContain('data-md-list-item-leading')
|
|
->toContain('<input type="checkbox">')
|
|
->toContain('data-md-list-item-end')
|
|
->toContain('<button>⋮</button>');
|
|
});
|
|
|
|
it('leads with M3\'s video, in the size the item\'s height calls for', function () {
|
|
expect((string) $this->blade('<x-list-item title="Sunrise" video="/poster.jpg" />'))
|
|
->toContain('data-md-list-item-video')
|
|
->toContain('data-md-size="sm"')
|
|
->toContain('<img src="/poster.jpg" alt="" />')
|
|
->toContain('data-md-lines="2"')
|
|
->and((string) $this->blade('<x-list-item title="Sunrise" overline="Draft" description="7:48" video="/poster.jpg" />'))
|
|
->toContain('data-md-size="lg"')
|
|
->toContain('data-md-lines="3"')
|
|
->and((string) $this->blade('<x-list-item title="Sunrise"><x-slot:video><video src="/clip.mp4"></video></x-slot:video></x-list-item>'))
|
|
->toContain('<video src="/clip.mp4"></video>')
|
|
->toContain('data-md-size="sm"')
|
|
->and((string) $this->blade('<x-list-item title="Sunrise" />'))
|
|
->not->toContain('data-md-list-item-video')
|
|
->toContain('data-md-lines="1"');
|
|
});
|
|
|
|
it('lays a row\'s state layer over its fill, so a selected or segmented row keeps its colour under the tint', function () {
|
|
$css = ComponentStylesheet::read('list-item');
|
|
$hover = '[data-md-list-row]:not([data-md-card]):hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):hover))';
|
|
$press = '[data-md-list-row]:not([data-md-card]):active:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):active))';
|
|
|
|
expect($css->declarations($hover, ['@media (hover: hover)']))
|
|
->toBe(['background-color' => 'color-mix(in srgb, currentColor calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), var(--md-list-row-fill, transparent))'])
|
|
->and($css->declarations($press))
|
|
->toBe(['background-color' => 'color-mix(in srgb, currentColor calc(var(--md-sys-state-pressed-state-layer-opacity) * 100%), var(--md-list-row-fill, transparent))'])
|
|
->and($css->declarations('[data-md-list-row]:not([data-md-card]):has([data-md-list-open]:focus-visible)'))
|
|
->toHaveKey('background-color', 'color-mix(in srgb, currentColor calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), var(--md-list-row-fill, transparent))')
|
|
->and($css->declarations(':where([data-md-list-row])'))->toBe(['--md-list-row-fill' => 'transparent'])
|
|
->and($css->declarations("[data-md-list='segmented'] > [data-md-list-item]"))->toHaveKey('--md-list-row-fill', 'var(--md-sys-color-surface)')
|
|
->and($css->declarations('[data-md-list-item][data-md-selected]'))->toHaveKey('--md-list-row-fill', 'var(--md-sys-color-secondary-container)')
|
|
->and(File::get(ComponentStylesheet::path('table')))->toContain('--md-list-row-fill: var(--md-sys-color-secondary-container);');
|
|
|
|
// The segmented fill comes first: on the same weight, a selected tile's secondary-container wins.
|
|
expect(strpos($css->css, "[data-md-list='segmented'] > [data-md-list-item] {"))->toBeLessThan(strpos($css->css, '[data-md-list-item][data-md-selected] {'));
|
|
});
|
|
|
|
it('inks a disabled item as a whole at the disabled-content opacity', function () {
|
|
expect(ComponentStylesheet::read('list-item')->declarations("[data-md-list-item][aria-disabled='true']"))->toBe([
|
|
'pointer-events' => 'none',
|
|
'color' => 'color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent)',
|
|
]);
|
|
});
|