Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,19 @@ argument and explicit mode methods instead. A warning forces
warning, caller-supplied guidance and invalid-state properties remain unchanged.
Other repeated properties use the element builder's normal last-value behavior.

### Fieldset warnings

`Fieldset::create($label, $name, $warningMessage = '')` renders one group warning
with the ID `<name>-warning`. String warnings are wrapped in a paragraph and
retain caller-supplied markup. `PropertyInterface` warnings retain their element,
content, and other properties; the generated ID is applied to a local clone,
leaving the caller's warning object unchanged. Repeated rendering is stable, and
the same warning object can be reused across components with independent IDs.

Fieldset does not inspect or rewrite arbitrary children passed to `elements()`.
Callers remain responsible for setting `aria-invalid` and `aria-describedby` on
member controls, including references to the group warning when appropriate.

### Migrating from wrapperProps

In 5.0, use `containerProps()` instead of `wrapperProps()` on all form
Expand Down
8 changes: 5 additions & 3 deletions src/Forms/Fieldset.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,21 @@ public function labelProps(string ...$properties): self
return $this;
}

private function warningMessage(): string|PropertyInterface
private function warningMessage(): string
{
if ($this->warningMessage === '') {
return '';
}

if (is_string($this->warningMessage)) {
return Element::p($this->warningMessage)->props(
return (string) Element::p($this->warningMessage)->props(
'id ' . $this->warningId
);
}

return $this->warningMessage->prop('id ' . $this->warningId);
$warning = clone $this->warningMessage;

return (string) $warning->prop('id ' . $this->warningId);
}

public function __toString(): string
Expand Down
109 changes: 109 additions & 0 deletions tests/Forms/FieldsetTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);

namespace Eightfold\HtmlBuilder\Tests;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

use Eightfold\HtmlBuilder\Element;
use Eightfold\HtmlBuilder\Forms\Fieldset;
use Eightfold\HtmlBuilder\Forms\TextInput;

final class FieldsetTest extends TestCase
{
#[Test]
public function object_warning_is_rendered_locally_without_mutating_the_original(): void
{
$warning = Element::div(
Element::a('Review guidance')->props('href /guidance/')
)->props('id original', 'class custom-warning', 'data-example preserved');
$before = (string) $warning;
$fieldset = Fieldset::create('Group', 'group', $warning);
$expected = '<fieldset><legend>Group</legend>'
. '<div id="group-warning" class="custom-warning" data-example="preserved">'
. '<a href="/guidance/">Review guidance</a></div></fieldset>';

for ($render = 0; $render < 3; $render++) {
$html = (string) $fieldset;
parent::assertSame($before, (string) $warning);
parent::assertSame($expected, $html);
parent::assertSame(1, substr_count($html, 'id="group-warning"'));
}
}

#[Test]
public function shared_object_warning_keeps_independent_component_ids(): void
{
$warning = Element::p('Check the value.')->props('id original', 'class warning');
$before = (string) $warning;
$first = Fieldset::create('First', 'first', $warning);
$second = Fieldset::create('Second', 'second', $warning);
$input = TextInput::create('Text', 'text', warningMessage: $warning)->id('custom');

$firstHtml = (string) $first;
$secondHtml = (string) $second;
$inputHtml = (string) $input;

parent::assertSame(
'<fieldset><legend>First</legend><p id="first-warning" class="warning">'
. 'Check the value.</p></fieldset>',
$firstHtml
);
parent::assertSame(
'<fieldset><legend>Second</legend><p id="second-warning" class="warning">'
. 'Check the value.</p></fieldset>',
$secondHtml
);
parent::assertStringContainsString(
'<p id="custom-warning" class="warning">Check the value.</p>',
$inputHtml
);
parent::assertSame($firstHtml, (string) $first);
parent::assertSame($secondHtml, (string) $second);
parent::assertSame($inputHtml, (string) $input);
parent::assertSame($before, (string) $warning);
}

#[Test]
public function string_warning_preserves_markup_in_one_paragraph(): void
{
$fieldset = Fieldset::create('Group', 'group', '<strong>Check</strong> the value.');
$expected = '<fieldset><legend>Group</legend>'
. '<p id="group-warning"><strong>Check</strong> the value.</p></fieldset>';

parent::assertSame($expected, (string) $fieldset);
parent::assertSame($expected, (string) $fieldset);
}

#[Test]
public function empty_warning_emits_no_warning_element(): void
{
parent::assertSame(
'<fieldset><legend>Group</legend></fieldset>',
(string) Fieldset::create('Group', 'group')
);
}

#[Test]
public function warning_does_not_rewrite_arbitrary_child_controls(): void
{
$child = Element::input()->omitEndTag()->props(
'id child',
'aria-describedby child-help group-warning',
'aria-invalid false'
);
$before = (string) $child;
$rawChild = '<input id="raw" aria-describedby="raw-help">';
$fieldset = Fieldset::create('Group', 'group', 'Check the group.')->elements(
Element::div($child),
$rawChild
);
$expected = '<fieldset><legend>Group</legend><div>' . $before . '</div>'
. $rawChild . '<p id="group-warning">Check the group.</p></fieldset>';

parent::assertSame($expected, (string) $fieldset);
parent::assertSame($expected, (string) $fieldset);
parent::assertSame($before, (string) $child);
}
}
Loading