blade("Body"); $css = ComponentStylesheet::read('card'); expect($html)->toContain("data-md-card=\"{$variant}\"") ->and($css->has("[data-md-card='{$variant}']"))->toBeTrue(); })->with(['filled', 'elevated', 'outlined']); it('is filled unless it knows the variant', function () { expect((string) $this->blade('Body'))->toContain('data-md-card="filled"'); }); it('lays out a header, menu, media, body and actions', function () { $html = (string) $this->blade(<<<'BLADE' Expires in 3 days. BLADE); expect($html) ->toContain('data-md-card-figure') ->toContain('') ->toContain('

holiday-photos.zip

') ->toContain('data-md-card-menu') ->toContain('248 MB') ->toContain('') ->toContain('role="separator"') ->toContain('data-md-card-content') ->toContain('Expires in 3 days.') ->toContain('data-md-card-actions') ->toContain('') ->and(strpos($html, 'Expires'))->toBeLessThan(strpos($html, 'Copy link')); }); it('passes row attributes through and renders the shared interaction classes for a row', function () { $html = (string) $this->blade('Open'); expect($html) ->toContain('data-md-list-row') ->toContain('data-md-list-open') ->toContain('class="md-state-layer md-focus-ring"') // Any card can be carried, so each draws the shared layer, which card.css keeps off one // that is neither a row nor dragged. ->and((string) $this->blade('Body')) ->toContain('class="md-state-layer"') ->not->toContain('md-focus-ring') ->and(ComponentStylesheet::read('card')->declarations('[data-md-card]:not([data-md-list-row], [data-md-dragged])::before')) ->toBe(['display' => 'none']); }); it('makes a directly actionable card the one tab stop, named by its title', function () { $html = (string) $this->blade('Open'); expect($html) ->toContain('data-md-list-row') ->toContain('data-md-list-actionable') ->toContain('tabindex="0"') ->toContain('role="button"') ->toContain('aria-label="holiday-photos.zip"') ->toContain('class="md-state-layer md-focus-ring"') ->and((string) $this->blade('Body')) ->toContain('role="link"') ->and(substr_count((string) $this->blade('Body'), 'role="link"'))->toBe(1) ->and((string) $this->blade('Body')) ->not->toContain('data-md-list-actionable') ->not->toContain('tabindex'); }); it('draws M3\'s per-state elevation and dragged state from the tokens, never a hand-made shadow', function () { $css = ComponentStylesheet::read('card'); expect((string) $this->blade('Body'))->toContain('data-md-dragged') ->and($css->declarations('[data-md-card][data-md-dragged]'))->toBe(['box-shadow' => 'var(--md-sys-elevation-3)']) ->and($css->declarations("[data-md-card='elevated'][data-md-dragged]"))->toBe(['box-shadow' => 'var(--md-sys-elevation-4)']) ->and($css->declarations("[data-md-card='elevated']"))->toHaveKey('box-shadow', 'var(--md-sys-elevation-1)') ->and($css->has('[data-md-card][data-md-list-row]:not([data-md-dragged]):hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):hover))', ['@media (hover: hover)']))->toBeTrue() ->and($css->declarations("[data-md-card='elevated'][data-md-list-row]:not([data-md-dragged]):hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):hover))", ['@media (hover: hover)']))->toBe(['box-shadow' => 'var(--md-sys-elevation-2)']) ->and($css->declarations('[data-md-card][data-md-list-row]:not([data-md-dragged]):hover:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):hover)::before', ['@media (hover: hover)']))->toBe(['opacity' => '0']); }); it('rings the row from the card itself when actionable, or from a has() when its opener is focused', function () { $css = ComponentStylesheet::read('card'); expect($css->declarations('[data-md-card][data-md-list-row]:has([data-md-list-open]:focus-visible)')) ->toBe(['outline' => '3px solid var(--md-sys-color-secondary)', 'outline-offset' => '2px']) ->and($css->declarations('[data-md-card][data-md-list-row] [data-md-list-open]:focus-visible')) ->toBe(['outline' => 'none']); });