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 @@ -75,6 +75,19 @@ Output:
<input required>
```

## Form control guidance

`TextInput`, `EmailInput`, `TextArea`, and all `Select` modes preserve help IDs
supplied through `inputProps('aria-describedby help-id')` when adding a warning.
With a warning, the control sets `aria-invalid="true"` and appends
`<control-id>-warning` to `aria-describedby`, removing duplicate references while
preserving their order. Use `content()` to supply the referenced help elements.
Overriding the control's `id()` also changes its generated warning ID.

`tests/ControlGuidanceTest.php` checks these relationships across control types.
Assertion failures identify the relationship, control type and ID, and rendered
HTML, alongside PHPUnit's named case and expected/actual diff.

## Performance

The `/comparisons` folder can be used to run performance tests locally to compare the same output using:
Expand Down
6 changes: 3 additions & 3 deletions src/Element.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

namespace Eightfold\HtmlBuilder;

use Stringable;

use Eightfold\HtmlBuilder\PropertyInterface;

class Element implements Stringable, PropertyInterface
use Stringable;

class Element implements PropertyInterface
{
/**
* The following attributes will be placed in this order if used in an
Expand Down
44 changes: 44 additions & 0 deletions src/Forms/ControlProperties.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);

namespace Eightfold\HtmlBuilder\Forms;

final class ControlProperties
{
/**
* @param list<string> $inputProperties
* @return list<string>
*/
public static function withWarning(
array $inputProperties,
string $warningId
): array {
$properties = [];
$descriptionIds = [];

$ariaProperty = 'aria-describedby ';
foreach ($inputProperties as $property) {
if (str_starts_with($property, $ariaProperty)) {
$ids = preg_split(
'/\s+/',
trim(substr($property, strlen($ariaProperty)))
);

foreach ($ids === false ? [] : $ids as $id) {
if ($id !== '') {
$descriptionIds[] = $id;
}
}
} else {
$properties[] = $property;
}
}

$descriptionIds[] = $warningId;

$properties[] = 'aria-invalid true';
$properties[] = $ariaProperty . implode(' ', array_unique($descriptionIds));

return $properties;
}
}
88 changes: 59 additions & 29 deletions src/Forms/EmailInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@

class EmailInput implements Stringable
{
// phpcs:disable
private string $warningId {
get => $this->name . '-warning';
}
// phpcs:enable
private string $id;

/**
* @var string[]
*/
private array $wrapperProperties = [];

/**
* @var string[]
Expand All @@ -26,6 +27,11 @@ class EmailInput implements Stringable
*/
private array $inputProperties = [];

/**
* @var array<string|Stringable>
*/
private array $content = [];

public static function create(
string|Stringable $label,
string|Stringable $name,
Expand All @@ -41,6 +47,19 @@ final private function __construct(
private readonly string $value,
private readonly string|PropertyInterface $warningMessage = ''
) {
$this->id = (string) $name;
}

public function id(string $id): self
{
$this->id = $id;
return $this;
}

public function wrapperProps(string ...$properties): self
{
$this->wrapperProperties = $properties;
return $this;
}

public function labelProps(string ...$properties): self
Expand All @@ -55,51 +74,62 @@ public function inputProps(string ...$properties): self
return $this;
}

private function warningMessage(): string|PropertyInterface
public function content(string|Stringable ...$content): self
{
$this->content = $content;
return $this;
}

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

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

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

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

public function __toString(): string
{
$elements = [];
$elements[] = Element::label($this->label)->props(
'for ' . $this->name,
...$this->labelProperties
);
$inputProperties = $this->warningMessage === ''
? $this->inputProperties
: ControlProperties::withWarning(
array_values($this->inputProperties),
$this->id . '-warning'
);

$input = Element::input()->omitEndTag()->props(
$input = Element::input()->omitEndTag()->props(...[
...$inputProperties,
'type email',
'id ' . $this->name,
'id ' . $this->id,
'name ' . $this->name,
'value ' . $this->value,
'autocorrect off',
'autocapitalize off',
...$this->inputProperties
);

$warningMessage = self::warningMessage();
$hasWarningMessage = ($warningMessage !== '');
if ($hasWarningMessage) {
$input = $input->prop('aria-invalid true');
$input = $input->prop('aria-describedby ' . $this->warningId);
}
$elements[] = $input;
'autocorrect off'
]);

$warning = '';
if ($this->warningMessage !== '') {
$warningId = $this->id . '-warning';

if ($hasWarningMessage) {
$elements[] = $warningMessage;
$warning = (string) self::warningMessage($warningId);
}

return implode('', array_map('strval', $elements));
return (string) Element::div(
Element::label($this->label)->props(
'for ' . $this->id,
...$this->labelProperties
),
$input,
...[...$this->content, $warning]
)->props(...$this->wrapperProperties);
}
}
Loading
Loading