Skip to content

Commit f6fd6d6

Browse files
mikadamczykGrabowskiM
authored andcommitted
Enhanced ListField component with additional item properties and validation
1 parent 2948b0e commit f6fd6d6

File tree

2 files changed

+204
-2
lines changed

2 files changed

+204
-2
lines changed

src/lib/Twig/Components/AltRadio/ListField.php

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,17 @@
1515

1616
/**
1717
* @phpstan-type AltRadioItem array{
18+
* id: non-empty-string,
1819
* value: string|int,
1920
* label: string,
20-
* disabled?: bool
21+
* disabled?: bool,
22+
* tileClass?: string,
23+
* attributes?: array<string, mixed>,
24+
* label_attributes?: array<string, mixed>,
25+
* inputWrapperClassName?: string,
26+
* labelClassName?: string,
27+
* name?: string,
28+
* required?: bool
2129
* }
2230
* @phpstan-type AltRadioItems list<AltRadioItem>
2331
*/
@@ -32,7 +40,40 @@ protected function configurePropsResolver(OptionsResolver $resolver): void
3240
{
3341
$this->validateListFieldProps($resolver);
3442

35-
// TODO: check if items have value and label component
43+
$resolver->setOptions('items', static function (OptionsResolver $itemsResolver): void {
44+
$itemsResolver->setPrototype(true);
45+
46+
$itemsResolver
47+
->define('id')
48+
->required()
49+
->allowedTypes('string')
50+
->allowedValues(static fn (string $value): bool => trim($value) !== '');
51+
52+
$itemsResolver
53+
->define('value')
54+
->required()
55+
->allowedTypes('string', 'int');
56+
57+
$itemsResolver
58+
->define('label')
59+
->required()
60+
->allowedTypes('string');
61+
62+
$itemsResolver
63+
->define('disabled')
64+
->allowedTypes('bool')
65+
->default(false);
66+
67+
$itemsResolver
68+
->define('tileClass')
69+
->allowedTypes('string')
70+
->default('');
71+
72+
$itemsResolver
73+
->define('attributes')
74+
->allowedTypes('array')
75+
->default([]);
76+
});
3677

3778
$resolver->setDefaults(['direction' => 'horizontal']);
3879
$resolver->setDefaults(['value' => '']);
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Tests\Integration\DesignSystemTwig\Twig\Components\AltRadio;
10+
11+
use Ibexa\DesignSystemTwig\Twig\Components\AltRadio\ListField;
12+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
13+
use Symfony\Component\DomCrawler\Crawler;
14+
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
15+
use Symfony\UX\TwigComponent\Test\InteractsWithTwigComponents;
16+
17+
final class ListFieldTest extends KernelTestCase
18+
{
19+
use InteractsWithTwigComponents;
20+
21+
public function testMount(): void
22+
{
23+
$component = $this->mountTwigComponent(ListField::class, $this->baseProps());
24+
25+
self::assertInstanceOf(
26+
ListField::class,
27+
$component,
28+
'Component should mount as AltRadio\\ListField.'
29+
);
30+
}
31+
32+
public function testDefaultRenderProducesWrapperAndItems(): void
33+
{
34+
$crawler = $this->renderTwigComponent(
35+
ListField::class,
36+
$this->baseProps()
37+
)->crawler();
38+
39+
$wrapper = $this->getWrapper($crawler);
40+
$classes = $this->getClassAttr($wrapper);
41+
42+
self::assertStringContainsString('ids-field', $classes, 'Wrapper should include "ids-field".');
43+
self::assertStringContainsString('ids-field--list', $classes, 'Wrapper should include "ids-field--list".');
44+
self::assertStringContainsString('ids-alt-radio-list-field', $classes, 'Wrapper should include alt radio modifier.');
45+
46+
$items = $crawler->filter('.ids-choice-inputs-list__items .ids-alt-radio');
47+
self::assertSame(2, $items->count(), 'Should render exactly two alt radio items.');
48+
49+
$firstTile = $this->getTile($items->eq(0));
50+
$secondTile = $this->getTile($items->eq(1));
51+
52+
self::assertStringContainsString('Pick A', $this->getText($firstTile), 'First tile should render its label.');
53+
self::assertStringContainsString('Pick B', $this->getText($secondTile), 'Second tile should render its label.');
54+
}
55+
56+
public function testPerItemTileClassAndDisabledFlagAreForwarded(): void
57+
{
58+
$props = $this->baseProps();
59+
$props['items'][0]['tileClass'] = 'is-featured';
60+
$props['items'][0]['disabled'] = true;
61+
62+
$crawler = $this->renderTwigComponent(
63+
ListField::class,
64+
$props
65+
)->crawler();
66+
67+
$items = $crawler->filter('.ids-choice-inputs-list__items .ids-alt-radio');
68+
$firstAltRadio = $items->eq(0);
69+
$firstTile = $this->getTile($firstAltRadio);
70+
71+
self::assertStringContainsString(
72+
'is-featured',
73+
$this->getClassAttr($firstTile),
74+
'Custom tile class should be merged onto the tile element.'
75+
);
76+
77+
$firstInput = $this->getAltRadioInput($firstAltRadio);
78+
self::assertNotNull(
79+
$firstInput->attr('disabled'),
80+
'Disabled=true on the item should render native "disabled" attribute.'
81+
);
82+
}
83+
84+
public function testInvalidTileClassTypeCausesResolverErrorOnMount(): void
85+
{
86+
$this->expectException(InvalidOptionsException::class);
87+
88+
$this->mountTwigComponent(ListField::class, $this->baseProps([
89+
'items' => [
90+
[
91+
'id' => 'opt-a',
92+
'value' => 'A',
93+
'label' => 'Pick A',
94+
'tileClass' => ['not-a-string'],
95+
],
96+
],
97+
]));
98+
}
99+
100+
/**
101+
* @param array<string, mixed> $overrides
102+
*
103+
* @return array<string, mixed>
104+
*/
105+
private function baseProps(array $overrides = []): array
106+
{
107+
return array_replace([
108+
'name' => 'group',
109+
'items' => [
110+
[
111+
'id' => 'opt-a',
112+
'value' => 'A',
113+
'label' => 'Pick A',
114+
],
115+
[
116+
'id' => 'opt-b',
117+
'value' => 'B',
118+
'label' => 'Pick B',
119+
],
120+
],
121+
], $overrides);
122+
}
123+
124+
private function getWrapper(Crawler $crawler): Crawler
125+
{
126+
$node = $crawler->filter('.ids-field')->first();
127+
self::assertGreaterThan(0, $node->count(), 'Wrapper ".ids-field" should be present.');
128+
129+
return $node;
130+
}
131+
132+
private function getAltRadioInput(Crawler $scope): Crawler
133+
{
134+
$node = $scope->filter('.ids-alt-radio__source > input')->first();
135+
self::assertGreaterThan(
136+
0,
137+
$node->count(),
138+
'Alt radio input should be present under ".ids-alt-radio__source > input".'
139+
);
140+
141+
return $node;
142+
}
143+
144+
private function getTile(Crawler $scope): Crawler
145+
{
146+
$node = $scope->filter('.ids-alt-radio__tile')->first();
147+
self::assertGreaterThan(0, $node->count(), 'Tile ".ids-alt-radio__tile" should be present.');
148+
149+
return $node;
150+
}
151+
152+
private function getClassAttr(Crawler $node): string
153+
{
154+
return (string) $node->attr('class');
155+
}
156+
157+
private function getText(Crawler $node): string
158+
{
159+
return trim($node->text(''));
160+
}
161+
}

0 commit comments

Comments
 (0)