Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -5,6 +5,7 @@
@if( $emitInteractive )
<link rel="stylesheet" href="{{ $accordionStyleHref }}" data-ve-accordion>
<link rel="stylesheet" href="{{ $tabsStyleHref }}" data-ve-tabs>
<link rel="stylesheet" href="{{ $gridStyleHref }}" data-ve-grid>
<script src="{{ $interactivityScriptSrc }}" defer data-ve-interactivity></script>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
@endif
@if( '' !== $themeTokensCss )
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