Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/renderer-parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@
"artisanpack/accordion-title",
"artisanpack/accordion-body",
"artisanpack/tabs",
"artisanpack/tab-section"
"artisanpack/tab-section",
"artisanpack/grid",
"artisanpack/grid-item"
]
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
@php
use ArtisanPackUI\VisualEditor\Responsive\BreakpointRegistry;
use ArtisanPackUI\VisualEditorRendererBlade\Support\BlockSupports;

$validInnerLayouts = [ 'normal', 'equal', 'center', 'bottom', 'last-bottom' ];

$clampSpan = static function ( $value, int $fallback = 1 ): int {
$int = is_numeric( $value ) ? (int) $value : $fallback;
if ( $int < 1 ) {
return 1;
}
if ( $int > 12 ) {
return 12;
}
return $int;
};

$innerLayoutRaw = is_string( $attributes['innerLayout'] ?? null )
? $attributes['innerLayout']
: 'normal';
$innerLayout = in_array( $innerLayoutRaw, $validInnerLayouts, true )
? $innerLayoutRaw
: 'normal';

$baseColumnSpan = $clampSpan( $attributes['gridColumnSpan'] ?? null, 1 );
$baseRowSpan = $clampSpan( $attributes['gridRowSpan'] ?? null, 1 );

$spanClasses = [
'ap-grid-item-span-' . $baseColumnSpan . '-base-columns',
'ap-grid-item-span-' . $baseRowSpan . '-base-row',
];

$registry = app( BreakpointRegistry::class );

$collectOverrides = static function ( $overrides, string $suffix, int $base, callable $clamp ) use ( $registry ): array {
$classes = [];

if ( ! is_array( $overrides ) ) {
return $classes;
}

foreach ( $overrides as $bp => $value ) {
if ( BreakpointRegistry::BASE_KEY === $bp ) {
continue;
}
if ( ! is_numeric( $value ) ) {
continue;
}
if ( null === $registry->get( (string) $bp ) ) {
continue;
}

$classes[] = 'ap-grid-item-span-' . $clamp( $value, $base ) . '-' . $bp . '-' . $suffix;
}

return $classes;
};

$spanClasses = array_merge(
$spanClasses,
$collectOverrides( $attributes['responsive']['gridColumnSpan'] ?? null, 'columns', $baseColumnSpan, $clampSpan ),
$collectOverrides( $attributes['responsive']['gridRowSpan'] ?? null, 'row', $baseRowSpan, $clampSpan ),
);

$baseClasses = array_merge(
[
'ap-grid-item',
'ap-grid-item-layout-' . $innerLayout,
],
$spanClasses,
);
@endphp
<div{!! BlockSupports::wrapperAttrs( $attributes, $baseClasses ) !!}>
{!! $innerBlocksHtml !!}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@php
use ArtisanPackUI\VisualEditor\Responsive\BreakpointRegistry;
use ArtisanPackUI\VisualEditorRendererBlade\Support\BlockSupports;

$clampColumns = static function ( $value, int $fallback ): int {
$int = is_numeric( $value ) ? (int) $value : $fallback;
if ( $int < 1 ) {
return 1;
}
if ( $int > 12 ) {
return 12;
}
return $int;
};

$baseColumns = $clampColumns( $attributes['numColumns'] ?? null, 4 );

// Per-breakpoint overrides. Mobile-first CSS cascade handles the rest:
// emit `ap-grid-has-N-{bp}-columns` for every breakpoint that carries
// its own value. Ascending min-width ordering in grid.css means a
// later breakpoint's class wins for the wider viewport.
$breakpointClasses = [ 'ap-grid-has-' . $baseColumns . '-base-columns' ];

$responsiveColumns = $attributes['responsive']['numColumns'] ?? [];

if ( is_array( $responsiveColumns ) ) {
$registry = app( BreakpointRegistry::class );

foreach ( $responsiveColumns as $bp => $value ) {
if ( BreakpointRegistry::BASE_KEY === $bp ) {
continue;
}
if ( ! is_numeric( $value ) ) {
continue;
}
if ( null === $registry->get( (string) $bp ) ) {
continue;
}

$breakpointClasses[] = 'ap-grid-has-' . $clampColumns( $value, $baseColumns ) . '-' . $bp . '-columns';
}
}

$baseClasses = array_merge( [ 'ap-grid' ], $breakpointClasses );
@endphp
<div{!! BlockSupports::wrapperAttrs( $attributes, $baseClasses ) !!}>
{!! $innerBlocksHtml !!}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
<link rel="stylesheet" href="{{ $tabsStyleHref }}" data-ve-tabs>
<script src="{{ $interactivityScriptSrc }}" defer data-ve-interactivity></script>
@endif
{{-- Grid family CSS is layout-only (no interactivity script), so it
loads independently of $emitInteractive. Hosts that opt out of
accordion/tabs interactivity still want grid blocks to render. --}}
<link rel="stylesheet" href="{{ $gridStyleHref }}" data-ve-grid>
@if( '' !== $themeTokensCss )
<style data-ve-theme-tokens>{!! $themeTokensCss !!}</style>
@endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class BlocksStylesComponent extends Component

public string $tabsStyleHref;

public string $gridStyleHref;

public string $interactivityScriptSrc;

public bool $emitBlockLibrary;
Expand Down Expand Up @@ -93,6 +95,7 @@ public function __construct(
$this->themeHref = $base . '/theme.css';
$this->accordionStyleHref = $base . '/frontend/accordion.css';
$this->tabsStyleHref = $base . '/frontend/tabs.css';
$this->gridStyleHref = $base . '/frontend/grid.css';
$this->interactivityScriptSrc = $base . '/frontend/interactivity.js';
$this->emitBlockLibrary = $bundle;
$this->emitInteractive = $interactive;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare( strict_types=1 );

use ArtisanPackUI\VisualEditorRendererBlade\Services\ResponsiveCssAccumulator;
use Illuminate\Support\Facades\Blade;

beforeEach( function (): void {
app( ResponsiveCssAccumulator::class )->reset();
} );

it( 'emits per-breakpoint grid column classes on the wrapper from attributes.responsive.numColumns', function () {
$tree = [
[
'clientId' => 'grid-1',
'name' => 'artisanpack/grid',
'attributes' => [
'numColumns' => 1,
'responsive' => [
'numColumns' => [
'md' => 2,
'lg' => 4,
],
],
],
'innerBlocks' => [],
],
];

$rendered = $this->stripGlobalStyles( Blade::render( '<x-ve-blocks :tree="$tree" />', [ 'tree' => $tree ] ) );

expect( $rendered )->toContain( 'ap-grid-has-1-base-columns' )
->and( $rendered )->toContain( 'ap-grid-has-2-md-columns' )
->and( $rendered )->toContain( 'ap-grid-has-4-lg-columns' )
// Static-class approach: no accumulated <style data-ve-responsive>
// block is emitted just for the grid family.
->and( $rendered )->not->toContain( '<style data-ve-responsive>' );
} );

it( 'emits per-breakpoint span classes on grid-item from responsive.gridColumnSpan and gridRowSpan', function () {
$tree = [
[
'clientId' => 'item-1',
'name' => 'artisanpack/grid-item',
'attributes' => [
'gridColumnSpan' => 1,
'gridRowSpan' => 1,
'responsive' => [
'gridColumnSpan' => [ 'md' => 2, 'lg' => 3 ],
'gridRowSpan' => [ 'md' => 2 ],
],
],
'innerBlocks' => [],
],
];

$rendered = $this->stripGlobalStyles( Blade::render( '<x-ve-blocks :tree="$tree" />', [ 'tree' => $tree ] ) );

expect( $rendered )->toContain( 'ap-grid-item-span-1-base-columns' )
->and( $rendered )->toContain( 'ap-grid-item-span-1-base-row' )
->and( $rendered )->toContain( 'ap-grid-item-span-2-md-columns' )
->and( $rendered )->toContain( 'ap-grid-item-span-3-lg-columns' )
->and( $rendered )->toContain( 'ap-grid-item-span-2-md-row' );
} );

it( 'skips overrides at unknown breakpoints', function () {
$tree = [
[
'clientId' => 'grid-1',
'name' => 'artisanpack/grid',
'attributes' => [
'numColumns' => 2,
'responsive' => [
'numColumns' => [ 'orphan' => 6 ],
],
],
'innerBlocks' => [],
],
];

$rendered = $this->stripGlobalStyles( Blade::render( '<x-ve-blocks :tree="$tree" />', [ 'tree' => $tree ] ) );

expect( $rendered )->toContain( 'ap-grid-has-2-base-columns' )
->and( $rendered )->not->toContain( '-orphan-' );
} );
125 changes: 125 additions & 0 deletions packages/visual-editor-renderer-blade/tests/Unit/BlockRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,131 @@ public function render( array $attrs )
->and( $html )->toContain( 'Naked' );
} );

it( 'renders an artisanpack/grid tree with per-breakpoint column + span classes', function () {
$tree = [
makeBlock( 'artisanpack/grid', [
'numColumns' => 4,
], [
makeBlock( 'artisanpack/grid-item', [
'innerLayout' => 'center',
'gridColumnSpan' => 2,
'gridRowSpan' => 1,
], [
makeBlock( 'core/paragraph', [ 'content' => 'Cell content' ], [], 'p-1' ),
], 'item-1' ),
] ),
];

$html = makeRenderer()->render( $tree );

expect( $html )->toContain( 'ap-grid' )
->and( $html )->toContain( 'ap-grid-has-4-base-columns' )
->and( $html )->toContain( 'ap-grid-item' )
->and( $html )->toContain( 'ap-grid-item-layout-center' )
->and( $html )->toContain( 'ap-grid-item-span-2-base-columns' )
->and( $html )->toContain( 'ap-grid-item-span-1-base-row' )
->and( $html )->toContain( 'Cell content' );
} );

it( 'emits an `ap-grid-has-N-{bp}-columns` class for every responsive.numColumns override', function () {
$tree = [
makeBlock( 'artisanpack/grid', [
'numColumns' => 1,
'responsive' => [
'numColumns' => [
'md' => 2,
'lg' => 4,
],
],
] ),
];

$html = makeRenderer()->render( $tree );

expect( $html )->toContain( 'ap-grid-has-1-base-columns' )
->and( $html )->toContain( 'ap-grid-has-2-md-columns' )
->and( $html )->toContain( 'ap-grid-has-4-lg-columns' );
} );

it( 'clamps grid column counts outside 1-12 to safe defaults', function () {
$tooHigh = [
makeBlock( 'artisanpack/grid', [ 'numColumns' => 99 ] ),
];
$tooLow = [
makeBlock( 'artisanpack/grid', [ 'numColumns' => 0 ] ),
];

expect( makeRenderer()->render( $tooHigh ) )->toContain( 'ap-grid-has-12-base-columns' );
expect( makeRenderer()->render( $tooLow ) )->toContain( 'ap-grid-has-1-base-columns' );
} );

it( 'emits per-breakpoint grid-item span classes for both columns and rows', function () {
$tree = [
makeBlock( 'artisanpack/grid-item', [
'gridColumnSpan' => 1,
'gridRowSpan' => 1,
'responsive' => [
'gridColumnSpan' => [ 'md' => 2, 'lg' => 3 ],
'gridRowSpan' => [ 'md' => 2 ],
],
] ),
];

$html = makeRenderer()->render( $tree );

expect( $html )->toContain( 'ap-grid-item-span-1-base-columns' )
->and( $html )->toContain( 'ap-grid-item-span-1-base-row' )
->and( $html )->toContain( 'ap-grid-item-span-2-md-columns' )
->and( $html )->toContain( 'ap-grid-item-span-3-lg-columns' )
->and( $html )->toContain( 'ap-grid-item-span-2-md-row' );
} );

it( 'skips numColumns / span overrides at unknown breakpoints', function () {
$tree = [
makeBlock( 'artisanpack/grid', [
'numColumns' => 2,
'responsive' => [ 'numColumns' => [ 'orphan' => 6 ] ],
] ),
];

$html = makeRenderer()->render( $tree );

expect( $html )->toContain( 'ap-grid-has-2-base-columns' )
->and( $html )->not->toContain( 'orphan-columns' )
->and( $html )->not->toContain( '-orphan-' );
} );

it( 'falls back to a safe default when grid-item innerLayout is invalid', function () {
$tree = [
makeBlock( 'artisanpack/grid-item', [
'innerLayout' => 'evil"><script>',
] ),
];

$html = makeRenderer()->render( $tree );

expect( $html )->toContain( 'ap-grid-item-layout-normal' )
->and( $html )->not->toContain( '<script>' )
->and( $html )->not->toContain( 'ap-grid-item-layout-evil' );
} );

it( 'compiles spacing.blockGap with horizontal/vertical sides into row-gap + column-gap', function () {
$tree = [
makeBlock( 'artisanpack/grid', [
'style' => [
'spacing' => [
'blockGap' => [ 'top' => '2rem', 'left' => '1rem' ],
],
],
] ),
];

$html = makeRenderer()->render( $tree );

expect( $html )->toContain( 'row-gap: 2rem' )
->and( $html )->toContain( 'column-gap: 1rem' );
} );

describe( 'Keystone #50 — block-supports compilation on the rendered HTML', function (): void {
it( 'renders a custom background color on core/group as both class marker and inline style', function (): void {
$tree = [ makeBlock( 'core/group', [
Expand Down
Loading