diff --git a/README.md b/README.md index 0886f1f..f3d2961 100644 --- a/README.md +++ b/README.md @@ -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 `-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 diff --git a/src/Forms/Fieldset.php b/src/Forms/Fieldset.php index 289a542..5bb0376 100644 --- a/src/Forms/Fieldset.php +++ b/src/Forms/Fieldset.php @@ -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 diff --git a/tests/Forms/FieldsetTest.php b/tests/Forms/FieldsetTest.php new file mode 100644 index 0000000..4e6482c --- /dev/null +++ b/tests/Forms/FieldsetTest.php @@ -0,0 +1,109 @@ +props('href /guidance/') + )->props('id original', 'class custom-warning', 'data-example preserved'); + $before = (string) $warning; + $fieldset = Fieldset::create('Group', 'group', $warning); + $expected = '
Group' + . '
'; + + 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( + '
First

' + . 'Check the value.

', + $firstHtml + ); + parent::assertSame( + '
Second

' + . 'Check the value.

', + $secondHtml + ); + parent::assertStringContainsString( + '

Check the value.

', + $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', 'Check the value.'); + $expected = '
Group' + . '

Check the value.

'; + + parent::assertSame($expected, (string) $fieldset); + parent::assertSame($expected, (string) $fieldset); + } + + #[Test] + public function empty_warning_emits_no_warning_element(): void + { + parent::assertSame( + '
Group
', + (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 = ''; + $fieldset = Fieldset::create('Group', 'group', 'Check the group.')->elements( + Element::div($child), + $rawChild + ); + $expected = '
Group
' . $before . '
' + . $rawChild . '

Check the group.

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