diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php
index 94efbe064f9..944f0c7ea50 100644
--- a/extra/html-extra/HtmlExtension.php
+++ b/extra/html-extra/HtmlExtension.php
@@ -15,8 +15,6 @@
use Twig\Environment;
use Twig\Error\RuntimeError;
use Twig\Extension\AbstractExtension;
-use Twig\Extension\CoreExtension;
-use Twig\Extension\EscaperExtension;
use Twig\Runtime\EscaperRuntime;
use Twig\TwigFilter;
use Twig\TwigFunction;
@@ -144,21 +142,32 @@ public static function htmlAttrMerge(...$arrays): array
throw new RuntimeError(sprintf('The "attr_merge" filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1));
}
- $array = CoreExtension::toArray($array);
+ $array = (array) ($array);
- foreach (['class', 'style', 'data', 'aria'] as $deepMergeKey) {
- if (isset($array[$deepMergeKey])) {
- $value = $array[$deepMergeKey];
- unset($array[$deepMergeKey]);
+ foreach (['data', 'aria'] as $flatOutKey) {
+ if (!isset($array[$flatOutKey])) {
+ continue;
+ }
+ $values = (array) $array[$flatOutKey];
+ foreach ($values as $key => $value) {
+ $result[$flatOutKey.'-'.$key] = $value;
+ }
+ unset($array[$flatOutKey]);
+ }
- if (!is_iterable($value)) {
- $value = (array) $value;
- }
+ foreach (['class', 'style'] as $deepMergeKey) {
+ if (!isset($array[$deepMergeKey])) {
+ continue;
+ }
- $value = CoreExtension::toArray($value);
+ $value = $array[$deepMergeKey];
+ unset($array[$deepMergeKey]);
- $result[$deepMergeKey] = array_merge($result[$deepMergeKey] ?? [], $value);
+ if (!is_iterable($value)) {
+ $value = (array) $value;
}
+
+ $result[$deepMergeKey] = array_merge($result[$deepMergeKey] ?? [], $value);
}
$result = array_merge($result, $array);
diff --git a/extra/html-extra/Tests/HtmlAttrMergeTest.php b/extra/html-extra/Tests/HtmlAttrMergeTest.php
index e39c4b87b53..fb478649e68 100644
--- a/extra/html-extra/Tests/HtmlAttrMergeTest.php
+++ b/extra/html-extra/Tests/HtmlAttrMergeTest.php
@@ -19,11 +19,150 @@ public function testMerge(array $expected, array $inputs): void
public function htmlAttrProvider(): \Generator
{
- yield 'simple test' => [
- ['id' => 'some-id', 'class' => ['some-class']],
+ yield 'merging different attributes from two arrays' => [
+ ['id' => 'some-id', 'label' => 'some-label'],
[
['id' => 'some-id'],
- ['class' => 'some-class'],
+ ['label' => 'some-label'],
+ ]
+ ];
+
+ yield 'merging different attributes from three arrays' => [
+ ['id' => 'some-id', 'label' => 'some-label', 'role' => 'main'],
+ [
+ ['id' => 'some-id'],
+ ['label' => 'some-label'],
+ ['role' => 'main'],
+ ]
+ ];
+
+ yield 'merging different attributes from Traversables' => [
+ ['id' => 'some-id', 'label' => 'some-label', 'role' => 'main'],
+ [
+ new \ArrayIterator(['id' => 'some-id']),
+ new \ArrayIterator(['label' => 'some-label']),
+ new \ArrayIterator(['role' => 'main']),
+ ]
+ ];
+
+ yield 'later keys override previous ones' => [
+ ['id' => 'other'],
+ [
+ ['id' => 'this'],
+ ['id' => 'that'],
+ ['id' => 'other'],
+ ]
+ ];
+
+ yield 'ignore empty strings or arrays passed as arguments' => [
+ ['some' => 'attribute'],
+ [
+ ['some' => 'attribute'],
+ [], // empty array
+ '', // empty string
+ ]
+ ];
+
+ yield 'keep "true" and "false" boolean values' => [
+ ['disabled' => true, 'enabled' => false],
+ [
+ ['disabled' => true],
+ ['enabled' => false],
+ ]
+ ];
+
+ yield 'consolidate values for the "class" key' => [
+ ['class' => ['foo', 'bar', 'baz']],
+ [
+ ['class' => ['foo']],
+ ['class' => 'bar'], // string, not array
+ ['class' => ['baz']],
+ ]
+ ];
+
+ yield 'class values can be overridden when they use names (array keys)' => [
+ ['class' => ['foo', 'bar', 'importance' => 'high']],
+ [
+ ['class' => 'foo'],
+ ['class' => ['bar', 'importance' => 'low']],
+ ['class' => ['importance' => 'high']],
+ ]
+ ];
+
+ yield 'inline style values with numerical keys are merely collected' => [
+ ['style' => ['font-weight: light', 'color: green', 'font-weight: bold']],
+ [
+ ['style' => ['font-weight: light']],
+ ['style' => ['color: green', 'font-weight: bold']],
+ ]
+ ];
+
+ yield 'inline style values can be overridden when they use names (array keys)' => [
+ ['style' => ['font-weight' => 'bold', 'color' => 'red']],
+ [
+ ['style' => ['font-weight' => 'light']],
+ ['style' => ['color' => 'green', 'font-weight' => 'bold']],
+ ['style' => ['color' => 'red']],
+ ]
+ ];
+
+ yield 'no merging happens when mixing numerically indexed inline styles with named ones' => [
+ ['style' => ['color: green', 'color' => 'red']],
+ [
+ ['style' => ['color: green']],
+ ['style' => ['color' => 'red']],
+ ]
+ ];
+
+ yield 'turning aria attributes from array to flat keys' => [
+ ['aria-role' => 'banner'],
+ [
+ ['aria' => ['role' => 'main']],
+ ['aria' => ['role' => 'banner']],
+ ]
+ ];
+
+ yield 'merging aria attributes, where the array values overrides the flat one' => [
+ ['aria-role' => 'navigation'],
+ [
+ ['aria-role' => 'main'],
+ ['aria' => ['role' => 'banner']],
+ ['aria' => ['role' => 'navigation']],
+ ]
+ ];
+
+ yield 'merging aria attributes, where the flat ones overrides the array' => [
+ ['aria-role' => 'navigation'],
+ [
+ ['aria' => ['role' => 'main']],
+ ['aria-role' => 'banner'],
+ ['aria-role' => 'navigation'],
+ ]
+ ];
+
+ yield 'turning data attributes from array to flat keys' => [
+ ['data-test' => 'bar'],
+ [
+ ['data' => ['test' => 'foo']],
+ ['data' => ['test' => 'bar']],
+ ]
+ ];
+
+ yield 'merging data attributes, where the array values overrides the flat one' => [
+ ['data-test' => 'baz'],
+ [
+ ['data-test' => 'foo'],
+ ['data' => ['test' => 'bar']],
+ ['data' => ['test' => 'baz']],
+ ]
+ ];
+
+ yield 'merging data attributes, where the flat ones overrides the array' => [
+ ['data-test' => 'baz'],
+ [
+ ['data' => ['test' => 'foo']],
+ ['data-test' => 'bar'],
+ ['data-test' => 'baz'],
]
];
}