Skip to content

Commit 1fdd7aa

Browse files
Let LabelDecorator mark required elements as such with an asterisk (#319)
Add implementation of LabelDecorator that marks required fields with tooltips and creates an explanation below the form if necessary. resolves #239
1 parent 818f1dd commit 1fdd7aa

File tree

5 files changed

+220
-4
lines changed

5 files changed

+220
-4
lines changed

asset/css/compat.less

100644100755
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ form.icinga-form {
2525
}
2626
}
2727

28+
.icinga-controls {
29+
.required-hint {
30+
font-weight: bold;
31+
color: var(--default-text-color-light, @default-text-color-light);
32+
}
33+
}
34+
35+
2836
// Button styles
2937

3038
// The `form` selector is only required to overrule the hover effect applied by Icinga Web.

src/Compat/CompatForm.php

100644100755
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
use ipl\Html\Contract\FormSubmitElement;
88
use ipl\Html\Contract\HtmlElementInterface;
99
use ipl\Html\Form;
10+
use ipl\Html\FormDecoration\DecoratorChain;
1011
use ipl\Html\FormElement\SubmitButtonElement;
1112
use ipl\Html\FormElement\SubmitElement;
1213
use ipl\Html\HtmlDocument;
1314
use ipl\Html\HtmlString;
1415
use ipl\I18n\Translation;
1516
use ipl\Web\FormDecorator\IcingaFormDecorator;
17+
use ipl\Html\Contract\FormDecoration;
18+
use ipl\Web\Compat\FormDecorator\LabelDecorator;
1619

1720
class CompatForm extends Form
1821
{
@@ -39,9 +42,9 @@ public function applyDefaultElementDecorators(): static
3942
$this->addElementDecoratorLoaderPaths([
4043
['ipl\\Web\\Compat\\FormDecorator', 'Decorator']
4144
]);
42-
45+
$labelDecorator = new LabelDecorator();
4346
$this->setDefaultElementDecorators([
44-
'Label',
47+
$labelDecorator,
4548
[
4649
'name' => 'HtmlTag',
4750
'options' => [
@@ -74,6 +77,7 @@ public function applyDefaultElementDecorators(): static
7477
]
7578
],
7679
]);
80+
$this->getDecorators()->addDecorator($labelDecorator);
7781

7882
return $this;
7983
}

src/Compat/FormDecorator/LabelDecorator.php

100644100755
Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,73 @@
22

33
namespace ipl\Web\Compat\FormDecorator;
44

5+
use ipl\Html\Contract\DecorationResult;
6+
use ipl\Html\Contract\Form;
57
use ipl\Html\Contract\FormElement;
8+
use ipl\Html\Contract\FormDecoration;
9+
use ipl\Html\Contract\MutableHtml;
10+
use ipl\Html\HtmlDocument;
611
use ipl\Html\HtmlString;
712
use ipl\Html\FormDecoration\LabelDecorator as IplHtmlLabelDecorator;
813
use ipl\Html\ValidHtml;
14+
use ipl\Html\Attributes;
15+
use ipl\Html\HtmlElement;
16+
use ipl\I18n\Translation;
17+
use ipl\html\Text;
918

10-
class LabelDecorator extends IplHtmlLabelDecorator
19+
class LabelDecorator extends IplHtmlLabelDecorator implements FormDecoration
1120
{
21+
use Translation;
22+
23+
/**
24+
* @var bool Whether an explanation for the asterisk of required fields needs to be added
25+
*/
26+
protected bool $requiredExplanationNeeded = false;
27+
28+
/**
29+
* Decorates the label of the form element and adds a tooltip if it is required
30+
*/
1231
protected function getElementLabel(FormElement $formElement): ?ValidHtml
1332
{
14-
return parent::getElementLabel($formElement) ?? HtmlString::create(' ');
33+
$result = parent::getElementLabel($formElement);
34+
if ($result === null) {
35+
$result = HtmlString::create(' ');
36+
} elseif ($result instanceof MutableHtml) {
37+
if ($formElement->isRequired()) {
38+
$formElement->setAttribute('aria-required', 'true');
39+
$requiredHint = new HtmlElement(
40+
'span',
41+
Attributes::create([
42+
'class' => 'required-hint',
43+
'aria-hidden' => 'true',
44+
'title' => $this->translate('Required')
45+
]),
46+
Text::create(" *")
47+
);
48+
$this->requiredExplanationNeeded = true;
49+
$result->addHtml($requiredHint);
50+
}
51+
}
52+
53+
return $result;
54+
}
55+
56+
/**
57+
* Appends an explanation of the asterisk for required fields if at least one such field exists
58+
*/
59+
public function decorateForm(DecorationResult $result, Form $form): void
60+
{
61+
if ($this->requiredExplanationNeeded) {
62+
$requiredExplanation = new HtmlElement(
63+
'ul',
64+
Attributes::create(['class' => 'form-info',]),
65+
new HtmlElement(
66+
'li',
67+
null,
68+
Text::create(sprintf($this->translate('%s Required field'), '*'))
69+
)
70+
);
71+
$result->append($requiredExplanation);
72+
}
1573
}
1674
}

tests/Compat/CompatFormTest.php

100644100755
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace ipl\Tests\Web\Compat;
44

55
use ipl\Html\FormElement\SubmitElement;
6+
use ipl\I18n\NoopTranslator;
7+
use ipl\I18n\StaticTranslator;
68
use ipl\Tests\Html\TestCase;
79
use ipl\Web\Compat\CompatForm;
810

@@ -14,6 +16,7 @@ class CompatFormTest extends TestCase
1416
protected function setUp(): void
1517
{
1618
$this->form = new CompatForm();
19+
StaticTranslator::$instance = new NoopTranslator();
1720
}
1821

1922
public function testDuplicateSubmitButtonApplied(): void
@@ -150,4 +153,55 @@ public function testDuplicateSubmitButtonRespectsOriginalAttributes(): void
150153
$this->assertSame($submitButton->getAttributes()->get('class')->getValue(), 'autosubmit');
151154
$this->assertSame($prefixButton->getAttributes()->get('class')->getValue(), 'primary-submit-btn-duplicate');
152155
}
156+
157+
public function testLabelDecoration(): void
158+
{
159+
$this->form->applyDefaultElementDecorators()
160+
->addElement(
161+
'text',
162+
'test_text_non_required',
163+
['required' => false, 'label' => 'test_non_required', 'id' => 'test-id-required']
164+
)
165+
->addElement('text', 'test_text_no_label')
166+
->addElement(
167+
'text',
168+
'test_text_required',
169+
['required' => true, 'label' => 'test_required', 'id' => 'test-id-non-required']
170+
);
171+
172+
$expected = <<<'HTML'
173+
<form class="icinga-form icinga-controls" method="POST">
174+
<div class="control-group">
175+
<div class="control-label-group">
176+
<label class="form-element-label" for="test-id-required">
177+
test_non_required
178+
</label>
179+
</div>
180+
<input name="test_text_non_required" type="text" id="test-id-required"/>
181+
</div>
182+
<div class="control-group">
183+
<div class="control-label-group">
184+
&nbsp;
185+
</div>
186+
<input name="test_text_no_label" type="text"/>
187+
</div>
188+
<div class="control-group">
189+
<div class="control-label-group">
190+
<label class="form-element-label" for="test-id-non-required">
191+
test_required
192+
<span class="required-hint" aria-hidden="true" title="Required"> *</span>
193+
</label>
194+
</div>
195+
<input required aria-required="true" name="test_text_required" type="text" id="test-id-non-required"/>
196+
</div>
197+
<ul class="form-info">
198+
<li>
199+
* Required field
200+
</li>
201+
</ul>
202+
</form>
203+
HTML;
204+
205+
$this->assertHtml($expected, $this->form);
206+
}
153207
}

tests/Compat/FormDecorator/LabelDecoratorTest.php

100644100755
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
namespace ipl\Tests\Web\Compat\FormDecorator;
44

5+
use ipl\Html\Contract\Form;
56
use ipl\Html\FormDecoration\FormElementDecorationResult;
67
use ipl\Html\FormElement\TextElement;
8+
use ipl\I18n\NoopTranslator;
9+
use ipl\I18n\StaticTranslator;
710
use ipl\Tests\Html\TestCase as IplHtmlTestCase;
811
use ipl\Web\Compat\FormDecorator\LabelDecorator;
912

@@ -14,6 +17,7 @@ class LabelDecoratorTest extends IplHtmlTestCase
1417
public function setUp(): void
1518
{
1619
$this->decorator = new LabelDecorator();
20+
StaticTranslator::$instance = new NoopTranslator();
1721
}
1822

1923
public function testWithoutLabelAttribute(): void
@@ -23,4 +27,92 @@ public function testWithoutLabelAttribute(): void
2327

2428
$this->assertSame('&nbsp;', $results->assemble()->render());
2529
}
30+
31+
public function testRequiredElement(): void
32+
{
33+
$formElement = new TextElement('test', [
34+
'required' => true,
35+
'label' => 'test-label',
36+
'id' => 'test-id'
37+
]);
38+
39+
$results = new FormElementDecorationResult();
40+
$this->decorator->decorateFormElement($results, $formElement);
41+
42+
$html = <<<'HTML'
43+
<label class="form-element-label" for="test-id">test-label
44+
<span class="required-hint" aria-hidden="true" title="Required"> *</span></label>
45+
HTML;
46+
47+
$this->assertHtml($html, $results->assemble());
48+
$this->assertSame($formElement->getAttribute('aria-required')->getValue(), 'true');
49+
}
50+
51+
public function testNonRequiredElement(): void
52+
{
53+
$formElement = new TextElement('test', [
54+
'required' => false,
55+
'label' => 'test-label'
56+
]);
57+
58+
$results = new FormElementDecorationResult();
59+
$this->decorator->decorateFormElement($results, $formElement);
60+
61+
$this->assertStringNotContainsString(
62+
'<span class="required-hint" aria-hidden="true" title="Required"> *</span>',
63+
$results->assemble()->render()
64+
);
65+
$this->assertFalse($formElement->hasAttribute('aria-required'));
66+
}
67+
68+
public function testFormDecoration(): void
69+
{
70+
$formElements = [
71+
new TextElement('test_required_1', [
72+
'required' => true,
73+
'label' => 'test-label'
74+
]),
75+
new TextElement('test_required_2', [
76+
'required' => true,
77+
'label' => 'test-label'
78+
]),
79+
new TextElement('test_no_label'),
80+
new TextElement('test_non_required', [
81+
'required' => false,
82+
'label' => 'test-label'
83+
]),
84+
];
85+
86+
$results = new FormElementDecorationResult();
87+
$formStub = $this->createStub(Form::class);
88+
foreach ($formElements as $formElement) {
89+
$this->decorator->decorateFormElement($results, $formElement);
90+
}
91+
92+
$this->decorator->decorateForm($results, $formStub);
93+
$assembledResults = $results->assemble();
94+
$this->assertStringEndsWith(
95+
'</label><ul class="form-info"><li>* Required field</li></ul>',
96+
$assembledResults
97+
);
98+
}
99+
100+
public function testFormWithoutRequiredElements(): void
101+
{
102+
$formElement = new TextElement('test', [
103+
'required' => false,
104+
'label' => 'test-label'
105+
]);
106+
107+
$results = new FormElementDecorationResult();
108+
$formStub = $this->createStub(Form::class);
109+
110+
$this->decorator->decorateFormElement($results, $formElement);
111+
$this->decorator->decorateForm($results, $formStub);
112+
113+
$this->assertStringEndsNotWith(
114+
'<ul class="form-info"><li>* Required field</li></ul>',
115+
$results->assemble()
116+
);
117+
}
26118
}

0 commit comments

Comments
 (0)