From f1bf3de6e709a4f2737f4643e462171b027649e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=BDoljom?= Date: Wed, 29 Oct 2025 18:04:22 +0100 Subject: [PATCH] Implement mj-group element Add group component to prevent column stacking on mobile devices for complex layouts. --- src/Elements/BodyComponents/MjGroup.php | 113 ++++++++++++ .../Elements/BodyComponents/MjGroupTest.php | 173 ++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 src/Elements/BodyComponents/MjGroup.php create mode 100644 tests/Unit/Elements/BodyComponents/MjGroupTest.php diff --git a/src/Elements/BodyComponents/MjGroup.php b/src/Elements/BodyComponents/MjGroup.php new file mode 100644 index 0000000..6aa2aca --- /dev/null +++ b/src/Elements/BodyComponents/MjGroup.php @@ -0,0 +1,113 @@ +> + */ + protected array $allowedAttributes = [ + 'width' => [ + 'unit' => 'px,%', + 'type' => 'string', + 'description' => 'group width', + 'default_value' => '100%', + ], + 'vertical-align' => [ + 'unit' => 'string', + 'type' => 'string', + 'description' => 'vertical alignment (top/middle/bottom)', + 'default_value' => 'top', + ], + 'background-color' => [ + 'unit' => 'color', + 'type' => 'color', + 'description' => 'background color for the group', + 'default_value' => '', + ], + 'direction' => [ + 'unit' => 'string', + 'type' => 'string', + 'description' => 'set the display order of direct children (ltr/rtl)', + 'default_value' => 'ltr', + ], + 'css-class' => [ + 'unit' => 'string', + 'type' => 'string', + 'description' => 'class name added to root HTML element', + 'default_value' => '', + ], + ]; + + protected array $defaultAttributes = [ + 'width' => '100%', + 'vertical-align' => 'top', + 'direction' => 'ltr', + ]; + + public function render(): string + { + $children = $this->getChildren() ?? []; + $content = $this->renderChildren($children, []); + + return $content; + } + + /** + * Gets the context for child elements. + * + * @return array + */ + public function getChildContext(): array + { + return [ + ...$this->context, + 'containerWidth' => $this->getAttribute('width'), + ]; + } + + /** + * @return array> + */ + public function getStyles(): array + { + return [ + 'td' => [ + 'background-color' => $this->getAttribute('background-color'), + 'direction' => $this->getAttribute('direction'), + 'font-size' => '0px', + 'text-align' => 'center', + 'vertical-align' => $this->getAttribute('vertical-align'), + ], + ]; + } +} diff --git a/tests/Unit/Elements/BodyComponents/MjGroupTest.php b/tests/Unit/Elements/BodyComponents/MjGroupTest.php new file mode 100644 index 0000000..bdccba4 --- /dev/null +++ b/tests/Unit/Elements/BodyComponents/MjGroupTest.php @@ -0,0 +1,173 @@ +element = new MjGroup(); +}); + +it('is not ending tag', function () { + expect($this->element->isEndingTag())->toBe(false); +}); + +it('returns the correct component name', function () { + expect($this->element->getTagName())->toBe('mj-group'); +}); + +it('returns the correct default attributes', function () { + $attributes = [ + 'width' => '100%', + 'vertical-align' => 'top', + 'direction' => 'ltr', + ]; + + foreach ($attributes as $key => $value) { + expect($this->element->getAttribute($key))->toBe($value); + } +}); + +it('will throw out of bounds exception if the allowed attribute is not existing', function () { + $this->element->getAllowedAttributeData('invalid-attribute'); +})->throws(OutOfBoundsException::class); + +it('will return allowed attribute data', function () { + $data = $this->element->getAllowedAttributeData('width'); + expect($data)->toBeArray(); + expect($data)->toHaveKey('type'); + expect($data)->toHaveKey('unit'); +}); + +it('will correctly render a simple group', function () { + $groupNode = new MjmlNode( + 'mj-group', + null, + null, + false, + null + ); + + $factory = new ElementFactory(); + $mjGroupElement = $factory->create($groupNode); + + expect($mjGroupElement)->toBeInstanceOf(MjGroup::class); +}); + +it('will correctly set background color attribute', function () { + $groupNode = new MjmlNode( + 'mj-group', + ['background-color' => '#f5f5f5'], + null, + false, + null + ); + + $factory = new ElementFactory(); + $mjGroupElement = $factory->create($groupNode); + + expect($mjGroupElement->getAttribute('background-color'))->toBe('#f5f5f5'); + + $styles = $mjGroupElement->getStyles(); + expect($styles['td']['background-color'])->toBe('#f5f5f5'); +}); + +it('will correctly render a group with custom width', function () { + $groupNode = new MjmlNode( + 'mj-group', + ['width' => '600px'], + null, + false, + null + ); + + $factory = new ElementFactory(); + $mjGroupElement = $factory->create($groupNode); + + expect($mjGroupElement)->toBeInstanceOf(MjGroup::class); + expect($mjGroupElement->getAttribute('width'))->toBe('600px'); +}); + +it('will correctly set vertical alignment attribute', function () { + $groupNode = new MjmlNode( + 'mj-group', + ['vertical-align' => 'middle'], + null, + false, + null + ); + + $factory = new ElementFactory(); + $mjGroupElement = $factory->create($groupNode); + + expect($mjGroupElement->getAttribute('vertical-align'))->toBe('middle'); + + $styles = $mjGroupElement->getStyles(); + expect($styles['td']['vertical-align'])->toBe('middle'); +}); + +it('will correctly set rtl direction attribute', function () { + $groupNode = new MjmlNode( + 'mj-group', + ['direction' => 'rtl'], + null, + false, + null + ); + + $factory = new ElementFactory(); + $mjGroupElement = $factory->create($groupNode); + + expect($mjGroupElement->getAttribute('direction'))->toBe('rtl'); + + $styles = $mjGroupElement->getStyles(); + expect($styles['td']['direction'])->toBe('rtl'); +}); + +it('will correctly set css class attribute', function () { + $groupNode = new MjmlNode( + 'mj-group', + ['css-class' => 'custom-group'], + null, + false, + null + ); + + $factory = new ElementFactory(); + $mjGroupElement = $factory->create($groupNode); + + expect($mjGroupElement->getAttribute('css-class'))->toBe('custom-group'); +}); + +it('will correctly set all custom properties', function () { + $groupNode = new MjmlNode( + 'mj-group', + [ + 'width' => '500px', + 'vertical-align' => 'bottom', + 'background-color' => '#eeeeee', + 'direction' => 'rtl', + 'css-class' => 'my-group', + ], + null, + false, + null + ); + + $factory = new ElementFactory(); + $mjGroupElement = $factory->create($groupNode); + + expect($mjGroupElement->getAttribute('width'))->toBe('500px'); + expect($mjGroupElement->getAttribute('vertical-align'))->toBe('bottom'); + expect($mjGroupElement->getAttribute('background-color'))->toBe('#eeeeee'); + expect($mjGroupElement->getAttribute('direction'))->toBe('rtl'); + expect($mjGroupElement->getAttribute('css-class'))->toBe('my-group'); + + $styles = $mjGroupElement->getStyles(); + expect($styles['td']['vertical-align'])->toBe('bottom'); + expect($styles['td']['background-color'])->toBe('#eeeeee'); + expect($styles['td']['direction'])->toBe('rtl'); +});