From 928200fa965b47423a883f8d0a3d88c2843a330f Mon Sep 17 00:00:00 2001 From: Josh Bruce Date: Sun, 4 Oct 2026 19:04:53 -0400 Subject: [PATCH 1/4] add: Password Forms element --- src/Forms/Password.php | 126 +++++++++++++++++++++++++++++ src/Forms/PasswordAutocomplete.php | 19 +++++ src/Forms/Select.php | 12 ++- tests/Forms/PasswordTest.php | 80 ++++++++++++++++++ 4 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 src/Forms/Password.php create mode 100644 src/Forms/PasswordAutocomplete.php create mode 100644 tests/Forms/PasswordTest.php diff --git a/src/Forms/Password.php b/src/Forms/Password.php new file mode 100644 index 0000000..6280aa4 --- /dev/null +++ b/src/Forms/Password.php @@ -0,0 +1,126 @@ + $this->name . '-warning'; + } + // phpcs:enable + + /** + * @var string[] + */ + private array $labelProperties = []; + + /** + * @var string[] + */ + private array $inputProperties = []; + + + private PasswordAutocomplete $autocomplete = PasswordAutocomplete::CurrentPassword; + + public static function create( + string|Stringable $label, + string|Stringable $name, + string|PropertyInterface $warningMessage = '' + ): self { + return new self($label, $name, $warningMessage); + } + + final private function __construct( + private readonly string|Stringable $label, + private readonly string|Stringable $name, + private readonly string|PropertyInterface $warningMessage = '' + ) { + } + + public function labelProps(string ...$properties): self + { + $this->labelProperties = $properties; + return $this; + } + + public function inputProps(string ...$properties): self + { + $this->inputProperties = $properties; + return $this; + } + + public function forNewPassword(): self + { + $this->autocomplete = PasswordAutocomplete::NewPassword; + return $this; + } + + public function autocompleteOff(): self + { + $this->autocomplete = PasswordAutocomplete::Off; + return $this; + } + + public function autocompleteOn(): self + { + $this->autocomplete = PasswordAutocomplete::On; + return $this; + } + + private function warningMessage(): string|PropertyInterface + { + if ($this->warningMessage === '') { + return ''; + } + + if (is_string($this->warningMessage)) { + return Element::p($this->warningMessage)->props( + 'id ' . $this->warningId + ); + } + + return $this->warningMessage->prop('id ' . $this->warningId); + } + + public function __toString(): string + { + $elements = []; + $elements[] = Element::label($this->label)->props( + 'for ' . $this->name, + ...$this->labelProperties + ); + + $input = Element::input()->omitEndTag()->props( + 'type password', + 'id ' . $this->name, + 'name ' . $this->name, + 'autocomplete ' . $this->autocomplete->value, + ...$this->inputProperties + ); + + $warningMessage = self::warningMessage(); + $hasWarningMessage = ($warningMessage !== ''); + if ($hasWarningMessage) { + $input = $input->prop('aria-invalid true'); + $input = $input->prop('aria-describedby ' . $this->warningId); + } + $elements[] = $input; + + if ($hasWarningMessage) { + $elements[] = $warningMessage; + } + + return $elements + |> (fn(array $e) => array_map('strval', $e)) + |> (fn(array $e) => implode('', $e)); + } +} diff --git a/src/Forms/PasswordAutocomplete.php b/src/Forms/PasswordAutocomplete.php new file mode 100644 index 0000000..ea99085 --- /dev/null +++ b/src/Forms/PasswordAutocomplete.php @@ -0,0 +1,19 @@ +options as $value => $content) { $value = (string) $value; $option = Element::option($content)->props('value ' . $value); @@ -180,7 +182,7 @@ private function selectDropdown(): Element $this->label )->props('for ' . $this->name, ...$this->labelProperties), $input, - self::warningMessage() + $warningMessage )->props(...$this->wrapperProperties); } @@ -189,7 +191,9 @@ private function selectOther(): Element $elements = []; $type = $this->type === SelectType::Checkbox ? 'checkbox' : 'radio'; - $hasWarningMessage = (self::warningMessage() !== ''); + $warningMessage = self::warningMessage(); + $hasWarningMessage = ($warningMessage !== ''); + foreach ($this->options as $value => $content) { $value = (string) $value; $id = $this->name . '-' . $value; @@ -215,7 +219,7 @@ private function selectOther(): Element } if ($hasWarningMessage) { - $elements[] = self::warningMessage(); + $elements[] = $warningMessage; } return Element::fieldset( diff --git a/tests/Forms/PasswordTest.php b/tests/Forms/PasswordTest.php new file mode 100644 index 0000000..00d6bda --- /dev/null +++ b/tests/Forms/PasswordTest.php @@ -0,0 +1,80 @@ +Password

Invalid value

+ html; + + $result = (string) Password::create( + 'Password', + 'password', + 'Invalid value' + )->forNewPassword(); + + parent::assertSame($expected, $result); + } + + #[Test] + public function can_change_autocomplete(): void // phpcs:ignore + { + $expected = <<Password + html; + + $result = (string) Password::create( + 'Password', + 'password' + )->forNewPassword(); + + parent::assertSame($expected, $result); + + $expected = <<Password + html; + + $result = (string) Password::create( + 'Password', + 'password' + )->autocompleteOn(); + + parent::assertSame($expected, $result); + + $expected = <<Password + html; + + $result = (string) Password::create( + 'Password', + 'password' + )->autocompleteOff(); + + parent::assertSame($expected, $result); + } + + #[Test] + public function is_expected_base(): void // phpcs:ignore + { + $expected = <<Password + html; + + $result = (string) Password::create( + 'Password', + 'password' + ); + + parent::assertSame($expected, $result); + } +} From 6829e7ad0092aa1338522650be3afde722a0417a Mon Sep 17 00:00:00 2001 From: Josh Bruce Date: Sun, 4 Oct 2026 19:26:21 -0400 Subject: [PATCH 2/4] add: Text Input Forms control --- src/Forms/TextInput.php | 102 ++++++++++++++++++++++++++++++++++ tests/Forms/TextInputTest.php | 43 ++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 src/Forms/TextInput.php create mode 100644 tests/Forms/TextInputTest.php diff --git a/src/Forms/TextInput.php b/src/Forms/TextInput.php new file mode 100644 index 0000000..85f44d2 --- /dev/null +++ b/src/Forms/TextInput.php @@ -0,0 +1,102 @@ + $this->name . '-warning'; + } + // phpcs:enable + + /** + * @var string[] + */ + private array $labelProperties = []; + + /** + * @var string[] + */ + private array $inputProperties = []; + + public static function create( + string|Stringable $label, + string|Stringable $name, + string|PropertyInterface $warningMessage = '' + ): self { + return new self($label, $name, $warningMessage); + } + + final private function __construct( + private readonly string|Stringable $label, + private readonly string|Stringable $name, + private readonly string|PropertyInterface $warningMessage = '' + ) { + } + + public function labelProps(string ...$properties): self + { + $this->labelProperties = $properties; + return $this; + } + + public function inputProps(string ...$properties): self + { + $this->inputProperties = $properties; + return $this; + } + + private function warningMessage(): string|PropertyInterface + { + if ($this->warningMessage === '') { + return ''; + } + + if (is_string($this->warningMessage)) { + return Element::p($this->warningMessage)->props( + 'id ' . $this->warningId + ); + } + + return $this->warningMessage->prop('id ' . $this->warningId); + } + + public function __toString(): string + { + $elements = []; + $elements[] = Element::label($this->label)->props( + 'for ' . $this->name, + ...$this->labelProperties + ); + + $input = Element::input()->omitEndTag()->props( + 'type text', + 'id ' . $this->name, + 'name ' . $this->name, + ...$this->inputProperties + ); + + $warningMessage = self::warningMessage(); + $hasWarningMessage = ($warningMessage !== ''); + if ($hasWarningMessage) { + $input = $input->prop('aria-invalid true'); + $input = $input->prop('aria-describedby ' . $this->warningId); + } + $elements[] = $input; + + if ($hasWarningMessage) { + $elements[] = $warningMessage; + } + + return $elements + |> (fn(array $e) => array_map('strval', $e)) + |> (fn(array $e) => implode('', $e)); + } +} diff --git a/tests/Forms/TextInputTest.php b/tests/Forms/TextInputTest.php new file mode 100644 index 0000000..723d0ff --- /dev/null +++ b/tests/Forms/TextInputTest.php @@ -0,0 +1,43 @@ +Text

Invalid value

+ html; + + $result = (string) TextInput::create( + 'Text', + 'text', + 'Invalid value' + ); + + parent::assertSame($expected, $result); + } + + #[Test] + public function is_expected_base(): void // phpcs:ignore + { + $expected = <<Text + html; + + $result = (string) TextInput::create( + 'Text', + 'text' + ); + + parent::assertSame($expected, $result); + } +} From 5173e7f41d30e1aede96e83f1be674fbb3a89c19 Mon Sep 17 00:00:00 2001 From: Josh Bruce Date: Sun, 4 Oct 2026 19:32:47 -0400 Subject: [PATCH 3/4] add: Email Input Forms control --- src/Forms/EmailInput.php | 105 +++++++++++++++++++++++++++++++++ src/Forms/TextInput.php | 5 +- tests/Forms/EmailInputTest.php | 59 ++++++++++++++++++ tests/Forms/TextInputTest.php | 22 ++++++- 4 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 src/Forms/EmailInput.php create mode 100644 tests/Forms/EmailInputTest.php diff --git a/src/Forms/EmailInput.php b/src/Forms/EmailInput.php new file mode 100644 index 0000000..ce9911a --- /dev/null +++ b/src/Forms/EmailInput.php @@ -0,0 +1,105 @@ + $this->name . '-warning'; + } + // phpcs:enable + + /** + * @var string[] + */ + private array $labelProperties = []; + + /** + * @var string[] + */ + private array $inputProperties = []; + + public static function create( + string|Stringable $label, + string|Stringable $name, + string $value = '', + string|PropertyInterface $warningMessage = '' + ): self { + return new self($label, $name, $value, $warningMessage); + } + + final private function __construct( + private readonly string|Stringable $label, + private readonly string|Stringable $name, + private readonly string $value, + private readonly string|PropertyInterface $warningMessage = '' + ) { + } + + public function labelProps(string ...$properties): self + { + $this->labelProperties = $properties; + return $this; + } + + public function inputProps(string ...$properties): self + { + $this->inputProperties = $properties; + return $this; + } + + private function warningMessage(): string|PropertyInterface + { + if ($this->warningMessage === '') { + return ''; + } + + if (is_string($this->warningMessage)) { + return Element::p($this->warningMessage)->props( + 'id ' . $this->warningId + ); + } + + return $this->warningMessage->prop('id ' . $this->warningId); + } + + public function __toString(): string + { + $elements = []; + $elements[] = Element::label($this->label)->props( + 'for ' . $this->name, + ...$this->labelProperties + ); + + $input = Element::input()->omitEndTag()->props( + 'type email', + 'id ' . $this->name, + 'name ' . $this->name, + 'value ' . $this->value, + ...$this->inputProperties + ); + + $warningMessage = self::warningMessage(); + $hasWarningMessage = ($warningMessage !== ''); + if ($hasWarningMessage) { + $input = $input->prop('aria-invalid true'); + $input = $input->prop('aria-describedby ' . $this->warningId); + } + $elements[] = $input; + + if ($hasWarningMessage) { + $elements[] = $warningMessage; + } + + return $elements + |> (fn(array $e) => array_map('strval', $e)) + |> (fn(array $e) => implode('', $e)); + } +} diff --git a/src/Forms/TextInput.php b/src/Forms/TextInput.php index 85f44d2..f89b60d 100644 --- a/src/Forms/TextInput.php +++ b/src/Forms/TextInput.php @@ -29,14 +29,16 @@ class TextInput implements Stringable public static function create( string|Stringable $label, string|Stringable $name, + string $value = '', string|PropertyInterface $warningMessage = '' ): self { - return new self($label, $name, $warningMessage); + return new self($label, $name, $value, $warningMessage); } final private function __construct( private readonly string|Stringable $label, private readonly string|Stringable $name, + private readonly string $value, private readonly string|PropertyInterface $warningMessage = '' ) { } @@ -80,6 +82,7 @@ public function __toString(): string 'type text', 'id ' . $this->name, 'name ' . $this->name, + 'value ' . $this->value, ...$this->inputProperties ); diff --git a/tests/Forms/EmailInputTest.php b/tests/Forms/EmailInputTest.php new file mode 100644 index 0000000..d524ee5 --- /dev/null +++ b/tests/Forms/EmailInputTest.php @@ -0,0 +1,59 @@ +Email + html; + + $result = (string) EmailInput::create( + 'Email', + 'email', + 'support@8fold.pro' + ); + + parent::assertSame($expected, $result); + } + + #[Test] + public function can_set_warning_message(): void // phpcs:ignore + { + $expected = <<Email

Invalid value

+ html; + + $result = (string) EmailInput::create( + label: 'Email', + name: 'email', + warningMessage: 'Invalid value' + ); + + parent::assertSame($expected, $result); + } + + #[Test] + public function is_expected_base(): void // phpcs:ignore + { + $expected = <<Email + html; + + $result = (string) EmailInput::create( + 'Email', + 'email' + ); + + parent::assertSame($expected, $result); + } +} diff --git a/tests/Forms/TextInputTest.php b/tests/Forms/TextInputTest.php index 723d0ff..e7b0579 100644 --- a/tests/Forms/TextInputTest.php +++ b/tests/Forms/TextInputTest.php @@ -11,16 +11,32 @@ class TextInputTest extends TestCase { #[Test] - public function can_set_warning_message(): void // phpcs:ignore + public function can_set_value(): void // phpcs:ignore { $expected = <<Text

Invalid value

+ html; $result = (string) TextInput::create( 'Text', 'text', - 'Invalid value' + 'Posted value' + ); + + parent::assertSame($expected, $result); + } + + #[Test] + public function can_set_warning_message(): void // phpcs:ignore + { + $expected = <<Text

Invalid value

+ html; + + $result = (string) TextInput::create( + label: 'Text', + name: 'text', + warningMessage: 'Invalid value' ); parent::assertSame($expected, $result); From 9eeb387330d756c79d8a479e7aa60b581c6a974d Mon Sep 17 00:00:00 2001 From: Josh Bruce Date: Sun, 4 Oct 2026 19:35:40 -0400 Subject: [PATCH 4/4] fix: PHP 8.4 compatibility --- src/Forms/EmailInput.php | 4 +--- src/Forms/Password.php | 4 +--- src/Forms/TextInput.php | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Forms/EmailInput.php b/src/Forms/EmailInput.php index ce9911a..058df56 100644 --- a/src/Forms/EmailInput.php +++ b/src/Forms/EmailInput.php @@ -98,8 +98,6 @@ public function __toString(): string $elements[] = $warningMessage; } - return $elements - |> (fn(array $e) => array_map('strval', $e)) - |> (fn(array $e) => implode('', $e)); + return implode('', array_map('strval', $elements)); } } diff --git a/src/Forms/Password.php b/src/Forms/Password.php index 6280aa4..825493b 100644 --- a/src/Forms/Password.php +++ b/src/Forms/Password.php @@ -119,8 +119,6 @@ public function __toString(): string $elements[] = $warningMessage; } - return $elements - |> (fn(array $e) => array_map('strval', $e)) - |> (fn(array $e) => implode('', $e)); + return implode('', array_map('strval', $elements)); } } diff --git a/src/Forms/TextInput.php b/src/Forms/TextInput.php index f89b60d..d533770 100644 --- a/src/Forms/TextInput.php +++ b/src/Forms/TextInput.php @@ -98,8 +98,6 @@ public function __toString(): string $elements[] = $warningMessage; } - return $elements - |> (fn(array $e) => array_map('strval', $e)) - |> (fn(array $e) => implode('', $e)); + return implode('', array_map('strval', $elements)); } }