diff --git a/src/Plugin/UiPatterns/PropType/AttributesPropType.php b/src/Plugin/UiPatterns/PropType/AttributesPropType.php index ee5fabb5d4eb038eb87ad68442bf9f40fa1416eb..e68252d38db83152a7b3575035ca9b43626f159e 100644 --- a/src/Plugin/UiPatterns/PropType/AttributesPropType.php +++ b/src/Plugin/UiPatterns/PropType/AttributesPropType.php @@ -58,7 +58,10 @@ class AttributesPropType extends PropTypePluginBase { It is possible because PropTypeInterface::normalize() is called by ComponentElementAlter::alter() after SDC is doing the validation. */ - return new Attribute($value); + if (is_array($value)) { + return new Attribute($value); + } + return $value; } } diff --git a/src/Plugin/UiPatterns/PropType/LinksPropType.php b/src/Plugin/UiPatterns/PropType/LinksPropType.php index 4bbaa1c2e652619e33bc25eedf26a341e6bd0295..2d241098df927e703acfe2e54d0a61a8f80c1a96 100644 --- a/src/Plugin/UiPatterns/PropType/LinksPropType.php +++ b/src/Plugin/UiPatterns/PropType/LinksPropType.php @@ -9,6 +9,7 @@ use Drupal\Component\Serialization\Json; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Template\Attribute; use Drupal\Core\Url; use Drupal\ui_patterns\Attribute\PropType; use Drupal\ui_patterns\PropTypePluginBase; @@ -120,6 +121,7 @@ class LinksPropType extends PropTypePluginBase implements ContainerFactoryPlugin unset($item["link"]); } $item = self::normalizeUrl($item); + $item = static::normalizeAttributes($item, "link_attributes"); if (array_key_exists("below", $item)) { $item["below"] = self::normalize($item["below"]); } @@ -130,16 +132,20 @@ class LinksPropType extends PropTypePluginBase implements ContainerFactoryPlugin /** * Normalize attributes in an item. */ - private static function normalizeAttributes(array $item): array { - if (array_key_exists("attributes", $item) && is_a($item["attributes"], '\Drupal\Core\Template\Attribute')) { - // In UI Patterns Settings, we kept the Attribute object here. It is not - // possible anymore because SDC will not validate it against the prop - // type schema. - $item["attributes"] = $item["attributes"]->toArray(); - foreach ($item["attributes"] as $attr => $value) { - if ($value instanceof Url) { - $item["attributes"][$attr] = $value->toString(); - } + protected static function normalizeAttributes(array $item, string $property = "attributes"): array { + if (!array_key_exists($property, $item)) { + return $item; + } + // Empty PHP arrays are converted in JSON arrays instead of JSON objects + // by json_encode(), so it is better to remove them. + if (is_array($item[$property]) && empty($item[$property])) { + unset($item[$property]); + return $item; + } + // Convert URL objects to strings. + foreach ($item[$property] as $attr => $value) { + if ($value instanceof Url) { + $item[$property][$attr] = $value->toString(); } } return $item; @@ -179,15 +185,32 @@ class LinksPropType extends PropTypePluginBase implements ContainerFactoryPlugin self::setHrefLang($options); self::setActiveClass($options, $url); if (isset($options["attributes"])) { - // In UI Patterns Settings, we built an Attribute object here. It is not - // possible anymore because SDC will not validate it against the prop - // type schema. - $item["link_attributes"] = $options["attributes"]; - foreach ($item["link_attributes"] as $attr => $value) { - if ($value instanceof Url) { - $item["link_attributes"][$attr] = $value->toString(); - } - } + $item = self:: mergeUrlAttributes($options["attributes"], $item); + } + return $item; + } + + /** + * Merge Url attributes. + * + * $options["attributes"] is always an associative array of HTML attributes. + * But $item["link_attributes"] can vary. + */ + private static function mergeUrlAttributes(array $url_attributes, array $item): array { + if (!isset($item["link_attributes"])) { + $item["link_attributes"] = $url_attributes; + return $item; + } + if (is_array($item["link_attributes"])) { + $item["link_attributes"] = array_merge( + $item["link_attributes"], + $url_attributes, + ); + return $item; + } + if (is_a($item["link_attributes"], '\Drupal\Core\Template\Attribute')) { + $item["link_attributes"]->merge(new Attribute($url_attributes)); + return $item; } return $item; } diff --git a/tests/src/Unit/PropTypeNormalization/LinksPropTypeNormalizationTest.php b/tests/src/Unit/PropTypeNormalization/LinksPropTypeNormalizationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1d2e3f3f26588352cf996efe00ed26655e2ceb44 --- /dev/null +++ b/tests/src/Unit/PropTypeNormalization/LinksPropTypeNormalizationTest.php @@ -0,0 +1,392 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\ui_patterns\Unit; + +use Drupal\Core\Template\Attribute; +use Drupal\Core\Url; +use Drupal\Tests\UnitTestCase; +use Drupal\ui_patterns\Plugin\UiPatterns\PropType\LinksPropType; + +/** + * @coversDefaultClass \Drupal\ui_patterns\Plugin\UiPatterns\PropType\LinksPropType + * + * @group ui_patterns + */ +final class LinksPropTypeNormalizationTest extends UnitTestCase { + + /** + * @covers ::canonicalize + * + * @dataProvider provideNormalizationData + */ + public function testNormalize(array $value, array $expected): void { + $normalized = LinksPropType::normalize($value); + self::assertEquals($normalized, $expected); + } + + /** + * Provide data for testNormalize. + */ + public static function provideNormalizationData(): \Generator { + $data = [ + "Empty value" => [ + "value" => [], + "expected" => [], + ], + "Standardized structure, flat, only primitives" => self::standardizedFlatPrimitives(), + // "Standardized structure, flat, with objects" => + // self::standardizedFlatObjects(), + "Breadcrumb structure, as generated by the core service" => self::breadcrumb(), + "Mini pager, as generated by the Views module" => self::viewsMiniPager(), + "Pager's pages, as generated by the Views module" => self::pagerPages(), + "Pager's navigation links, as generated by the Views module" => self::pagesNavigationLinks(), + // "Menu, as generated by the Menu module" => self::menu(), + "Where link_attributes is already manually set" => self::linkAttributes(), + ]; + foreach ($data as $label => $test) { + yield $label => [ + $test['value'], + $test['expected'], + ]; + }; + } + + /** + * Standardized structure, flat, only primitives. + */ + protected static function standardizedFlatPrimitives() { + $value = [ + [ + "title" => "With an absolute URL", + "url" => "http://wwww.example.org/foo/bar", + ], + [ + "title" => "With a relative URL", + "url" => "/foo/bar", + "attributes" => [ + "foo" => "bar", + ], + "link_attributes" => [ + "foo" => "baz", + ], + ], + ]; + $expected = $value; + return [ + "value" => $value, + "expected" => $expected, + ]; + } + + /** + * Standardized structure, flat, with objects. + */ + protected static function standardizedFlatObjects() { + $value = [ + [ + "title" => "With an absolute URL", + "url" => Url::fromUri("http://wwww.example.org//foo/bar"), + ], + [ + "title" => "With a relative URL", + "url" => Url::fromUserInput("/foo/bar", ["attributes" => ["foo" => "baz"]]), + "attributes" => new Attribute([ + "foo" => "bar", + ]), + ], + [ + "title" => "With empty attributes", + "url" => Url::fromUserInput("/foo/bar", ["attributes" => []]), + "attributes" => new Attribute([]), + ], + ]; + $expected = [ + [ + "title" => "With an absolute URL", + "url" => "http://wwww.example.org/foo/bar", + ], + [ + "title" => "With a relative URL", + "url" => "/foo/bar", + "attributes" => new Attribute([ + "foo" => "bar", + ]), + ], + [ + "title" => "With empty attributes", + "url" => "/foo/bar", + ], + ]; + return [ + "value" => $value, + "expected" => $expected, + ]; + } + + /** + * Breadcrumb structure, as generated by the core service. + * + * However, we replaced TranslatableMarkup("Home") by a simple string to + * avoid initializing a service container in the test. + */ + protected static function breadcrumb() { + $value = [ + [ + "text" => "Home", + "url" => "/", + ], + [ + "text" => "Foo", + "url" => "/foo", + ], + ]; + $expected = [ + [ + "title" => "Home", + "url" => "/", + ], + [ + "title" => "Foo", + "url" => "/foo", + ], + ]; + return [ + "value" => $value, + "expected" => $expected, + ]; + } + + /** + * Mini pager, as generated by the Views module. + * + * Usually, we don't expect the theme owner to use the normalization + * directly, but to add some custom logic around 'current' in a preprocess. + */ + protected static function viewsMiniPager() { + $value = [ + "current" => 2, + "previous" => [ + "href" => + "/articles?page=0", + "text" => "‹‹", + "attributes" => new Attribute(), + ], + "next" => [ + "href" => "/articles?page=2", + "text" => "››", + "attributes" => new Attribute(), + ], + ]; + $expected = [ + [ + "title" => "‹‹", + "url" => "/articles?page=0", + "attributes" => new Attribute(), + ], + [ + "title" => "››", + "url" => "/articles?page=2", + "attributes" => new Attribute(), + ], + ]; + return [ + "value" => $value, + "expected" => $expected, + ]; + } + + /** + * Pager's pages, as generated by the Views module. + */ + protected static function pagerPages() { + $value = [ + [ + "href" => "?page=0", + "attributes" => new Attribute(), + ], + [ + "href" => "?page=1", + "attributes" => new Attribute([ + "aria-current" => "page", + ]), + ], + [ + "href" => "?page=2", + "attributes" => new Attribute(), + ], + [ + "href" => "?page=3", + "attributes" => new Attribute(), + ], + ]; + $expected = [ + [ + "title" => 0, + "url" => "?page=0", + "attributes" => new Attribute(), + ], + [ + "title" => 1, + "url" => "?page=1", + "attributes" => new Attribute([ + "aria-current" => "page", + ]), + ], + [ + "title" => 2, + "url" => "?page=2", + "attributes" => new Attribute(), + ], + [ + "title" => 3, + "url" => "?page=3", + "attributes" => new Attribute(), + ], + ]; + return [ + "value" => $value, + "expected" => $expected, + ]; + } + + /** + * Pager's navigation links, as generated by the Views module. + */ + protected static function pagesNavigationLinks() { + $value = [ + "first" => [ + "attributes" => new Attribute(), + "href" => "?page=0", + "text" => "« First", + ], + "previous" => [ + "attributes" => new Attribute(), + "href" => "?page=0", + "text" => "‹‹", + ], + "next" => [ + "attributes" => new Attribute(), + "href" => "?page=2", + "text" => "››", + ], + "last" => [ + "attributes" => new Attribute(), + "href" => "?page=3", + "text" => "Last »", + ], + ]; + $expected = [ + [ + "attributes" => new Attribute(), + "title" => "« First", + "url" => "?page=0", + ], + [ + "attributes" => new Attribute(), + "title" => "‹‹", + "url" => "?page=0", + ], + [ + "attributes" => new Attribute(), + "title" => "››", + "url" => "?page=2", + ], + [ + "attributes" => new Attribute(), + "title" => "Last »", + "url" => "?page=3", + ], + ]; + return [ + "value" => $value, + "expected" => $expected, + ]; + } + + /** + * Menu, as generated by the Menu module. + * + * However, we replaced TranslatableMarkup() by a simple string to avoid + * initializing a service container in the test. + * And we replaced Url::fromRoute() by Url::fromUserInput() to avoid + * initializing a service container in the test. + */ + protected static function menu() { + $value = [ + "user.page" => [ + "is_expanded" => FALSE, + "is_collapsed" => FALSE, + "in_active_trail" => FALSE, + "attributes" => new Attribute(), + "title" => "My account", + "url" => Url::fromUserInput("/user", ["set_active_class" => TRUE]), + "below" => [], + ], + "user.logout" => [ + "is_expanded" => FALSE, + "is_collapsed" => FALSE, + "in_active_trail" => FALSE, + "attributes" => new Attribute(), + "title" => "Log out", + "url" => Url::fromUserInput("/user/logout", ["set_active_class" => TRUE]), + "below" => [], + ], + ]; + $expected = [ + [ + "title" => "My account", + "url" => "/user", + ], + [ + "title" => "Log out", + "url" => "/user/logout", + ], + ]; + return [ + "value" => $value, + "expected" => $expected, + ]; + } + + /** + * Where link_attributes is already manually set. + * + * Instead of being generated from an Url object. + */ + protected static function linkAttributes() { + $value = [ + [ + "href" => "?page=0", + "attributes" => new Attribute(), + "link_attributes" => new Attribute([ + 'class' => [ + 'display-flex', + 'flex-align-center', + 'flex-no-wrap', + ], + ]), + ], + ]; + $expected = [ + [ + "title" => 0, + "url" => "?page=0", + "attributes" => new Attribute(), + "link_attributes" => new Attribute([ + 'class' => [ + 'display-flex', + 'flex-align-center', + 'flex-no-wrap', + ], + ]), + ], + ]; + return [ + "value" => $value, + "expected" => $expected, + ]; + } + +}