From 05ca67d5d5f087d671e5e8f2d706b624bb48ac00 Mon Sep 17 00:00:00 2001 From: Josh Bruce Date: Sun, 4 Oct 2026 13:26:58 -0400 Subject: [PATCH 1/2] add: Label props to select form control --- src/Forms/Select.php | 15 ++++++-- tests/Forms/SelectTest.php | 73 +++++++++++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/src/Forms/Select.php b/src/Forms/Select.php index 84be7e5..ae7911f 100644 --- a/src/Forms/Select.php +++ b/src/Forms/Select.php @@ -16,6 +16,11 @@ class Select implements Stringable */ private array $wrapperProperties = []; + /** + * @var string[] + */ + private array $labelProperties = []; + private SelectType $type = SelectType::Dropdown; /** @@ -49,6 +54,12 @@ public function wrapperProps(string ...$properties): self return $this; } + public function labelProps(string ...$properties): self + { + $this->labelProperties = $properties; + return $this; + } + public function dropdown(): self { $this->type = SelectType::Dropdown; @@ -133,7 +144,7 @@ private function selectDropdown(): Element return Element::div( Element::label( $this->label - )->props('for ' . $this->name), + )->props('for ' . $this->name, ...$this->labelProperties), Element::select( ...$elements )->props('id ' . $this->name, 'name ' . $this->name) @@ -162,7 +173,7 @@ private function selectOther(): Element $elements[] = Element::div($input, $label); } return Element::fieldset( - Element::legend($this->label), + Element::legend($this->label)->props(...$this->labelProperties), ...$elements ); } diff --git a/tests/Forms/SelectTest.php b/tests/Forms/SelectTest.php index 22920d4..9eb110f 100644 --- a/tests/Forms/SelectTest.php +++ b/tests/Forms/SelectTest.php @@ -10,6 +10,55 @@ class SelectTest extends TestCase { + #[Test] + public function can_add_props_to_label(): void // phpcs: ignore + { + $expected = << + html; + + $result = (string) Select::create( + label: 'Select', + name: 'select', + options: [ + 'value' => 'display', + 'value2' => 'display2' + ] + )->labelProps('is select-dropdown'); + + parent::assertSame($expected, $result); + + $expected = <<Select
+ html; + + $result = (string) Select::create( + label: 'Select', + name: 'select', + options: [ + 'value' => 'display', + 'value2' => 'display2' + ] + )->checkbox()->labelProps('is checkbox'); + + parent::assertSame($expected, $result); + + $expected = <<Select
+ html; + + $result = (string) Select::create( + label: 'Select', + name: 'select', + options: [ + 'value' => 'display', + 'value2' => 'display2' + ] + )->radio()->labelProps('is radio'); + + parent::assertSame($expected, $result); + } + #[Test] public function can_be_checkboxes(): void // phpcs: ignore { @@ -18,16 +67,16 @@ public function can_be_checkboxes(): void // phpcs: ignore html; $result = (string) Select::create( - 'Select your option', - 'select', - [ + label: 'Select your option', + name: 'select', + options: [ 'value' => 'display', 'value2' => 'display2' ], - ['value', 'value2'] + selected: ['value', 'value2'] )->checkbox(); - $this->assertSame($expected, $result); + parent::assertSame($expected, $result); } #[Test] @@ -46,7 +95,7 @@ public function can_be_radio_buttons(): void // phpcs: ignore 'value' )->radio(); - $this->assertSame($expected, $result); + parent::assertSame($expected, $result); } #[Test] @@ -64,7 +113,7 @@ public function dropdown_mode_is_explicit(): void // phpcs: ignore ] )->radio()->dropdown(); - $this->assertSame($expected, $result); + parent::assertSame($expected, $result); } #[Test] @@ -83,7 +132,7 @@ public function checkbox_mode_is_explicit(): void // phpcs: ignore 'value' )->radio()->checkbox(); - $this->assertSame($expected, $result); + parent::assertSame($expected, $result); } #[Test] @@ -101,7 +150,7 @@ public function can_add_properties_to_wrapper(): void // phpcs: ignore ] )->wrapperProps('id some-id', 'class some-token'); - $this->assertSame($expected, $result); + parent::assertSame($expected, $result); } #[Test] @@ -121,7 +170,7 @@ public function can_preselect_option(): void // phpcs:ignore 'value2' ); - $this->assertSame($expected, $result); + parent::assertSame($expected, $result); } #[Test] @@ -139,7 +188,7 @@ public function is_expected_base(): void // phpcs:ignore ] ); - $this->assertSame($expected, $result); + parent::assertSame($expected, $result); } #[Test] @@ -158,6 +207,6 @@ public function error_is_selected_value_always_string(): void // phpcs:ignore ] ); - $this->assertSame($expected, $result); + parent::assertSame($expected, $result); } } From fb710df5854d7e0878018b7445edebcf8692963e Mon Sep 17 00:00:00 2001 From: Josh Bruce Date: Sun, 4 Oct 2026 15:42:23 -0400 Subject: [PATCH 2/2] add: Warning message to Select --- src/Element.php | 4 ++- src/Forms/Select.php | 69 +++++++++++++++++++++++++++++--------- src/PropertyInterface.php | 11 ++++++ tests/Forms/SelectTest.php | 44 ++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 src/PropertyInterface.php diff --git a/src/Element.php b/src/Element.php index c5da44c..2fa8678 100644 --- a/src/Element.php +++ b/src/Element.php @@ -5,7 +5,9 @@ use Stringable; -class Element implements Stringable +use Eightfold\HtmlBuilder\PropertyInterface; + +class Element implements Stringable, PropertyInterface { /** * The following attributes will be placed in this order if used in an diff --git a/src/Forms/Select.php b/src/Forms/Select.php index ae7911f..d9ab4db 100644 --- a/src/Forms/Select.php +++ b/src/Forms/Select.php @@ -6,11 +6,18 @@ use Stringable; use Eightfold\HtmlBuilder\Element; +use Eightfold\HtmlBuilder\PropertyInterface; use Eightfold\HtmlBuilder\Forms\SelectType; class Select implements Stringable { + // phpcs:disable + private string $warningId { + get => $this->name . '-warning'; + } + // phpcs:enable + /** * @var string[] */ @@ -31,9 +38,10 @@ public static function create( string|Stringable $label, string|Stringable $name, array $options, - string|array $selected = [] + string|array $selected = [], + string|PropertyInterface $warningMessage = '' ): self { - return new self($label, $name, $options, $selected); + return new self($label, $name, $options, $selected, $warningMessage); } /** @@ -44,7 +52,8 @@ final private function __construct( private readonly string|Stringable $label, private readonly string|Stringable $name, private readonly array $options, - private string|array $selected = [] + private readonly string|array $selected = [], + private readonly string|PropertyInterface $warningMessage = '' ) { } @@ -80,7 +89,7 @@ public function checkbox(): self private function hasSelected(): bool { - $selected = $this->selected(); + $selected = self::selected(); if (is_string($selected) and strlen($selected) > 0) { return true; @@ -111,22 +120,37 @@ private function selected(): string|array private function isSelected(string $value): bool { - if ($this->hasSelected() === false) { + if (self::hasSelected() === false) { return false; } - if (is_array($this->selected())) { - return in_array($value, $this->selected()); + if (is_array(self::selected())) { + return in_array($value, self::selected()); + } + return $value === self::selected(); + } + + private function warningMessage(): string|PropertyInterface + { + if ($this->warningMessage === '') { + return ''; + } + + if (is_string($this->warningMessage)) { + return Element::p($this->warningMessage)->props( + 'id ' . $this->warningId + ); } - return $value === $this->selected(); + + return $this->warningMessage->prop('id ' . $this->warningId); } public function __toString(): string { if ($this->type === SelectType::Dropdown) { - return (string) $this->selectDropdown(); + return (string) self::selectDropdown(); } - return (string) $this->selectOther(); + return (string) self::selectOther(); } private function selectDropdown(): Element @@ -135,19 +159,26 @@ private function selectDropdown(): Element foreach ($this->options as $value => $content) { $value = (string) $value; $option = Element::option($content)->props('value ' . $value); - if ($this->isSelected($value)) { + if (self::isSelected($value)) { $option = $option->prop('selected selected'); } $elements[] = $option; } + $input = Element::select( + ...$elements + )->props('id ' . $this->name, 'name ' . $this->name); + if (self::warningMessage() !== '') { + $input = $input->prop('aria-invalid true'); + $input = $input->prop('aria-describedby ' . $this->warningId); + } + return Element::div( Element::label( $this->label )->props('for ' . $this->name, ...$this->labelProperties), - Element::select( - ...$elements - )->props('id ' . $this->name, 'name ' . $this->name) + $input, + self::warningMessage() )->props(...$this->wrapperProperties); } @@ -167,10 +198,16 @@ private function selectOther(): Element 'type ' . $type, 'value ' . $value ); - if ($this->isSelected($value)) { + + if (self::isSelected($value)) { $input = $input->prop('checked checked'); } - $elements[] = Element::div($input, $label); + + if (self::warningMessage() !== '') { + $input = $input->prop('aria-invalid true'); + $input = $input->prop('aria-describedby ' . $this->warningId); + } + $elements[] = Element::div($input, $label, self::warningMessage()); } return Element::fieldset( Element::legend($this->label)->props(...$this->labelProperties), diff --git a/src/PropertyInterface.php b/src/PropertyInterface.php new file mode 100644 index 0000000..1818e83 --- /dev/null +++ b/src/PropertyInterface.php @@ -0,0 +1,11 @@ + + html; + + $result = (string) Select::create( + label: 'Select', + name: 'select', + options: [ + 'value' => 'display', + 'value2' => 'display2' + ], + warningMessage: Element::ul( + Element::li('Invalid value') + ) + ); + + parent::assertSame($expected, $result); + } + + #[Test] + public function warning_message_can_be_string(): void // phpcs: ignore + { + $expected = <<

Invalid value

+ html; + + $result = (string) Select::create( + label: 'Select', + name: 'select', + options: [ + 'value' => 'display', + 'value2' => 'display2' + ], + warningMessage: 'Invalid value' + ); + + parent::assertSame($expected, $result); + } + #[Test] public function can_add_props_to_label(): void // phpcs: ignore {