Component
*
* Wrapper around HtmlLink and AccessibleLink Value Objects for template usage.
* Provides consistent, accessible link rendering throughout the application.
*
* Usage in templates:
* Dashboard
* External Link
* Current Page
* Download PDF
* Link with custom class
*/
#[ComponentName('a')]
final readonly class A implements StaticComponent
{
private AccessibleLink $link;
private string $text;
public function __construct(
string $content = '',
array $attributes = []
) {
// Content is the link text
$this->text = $content;
// Extract href from attributes (default to '#' if not provided)
$href = $attributes['href'] ?? '#';
// Extract other link attributes
$title = $attributes['title'] ?? null;
$class = $attributes['class'] ?? null;
$target = $attributes['target'] ?? null;
$rel = $attributes['rel'] ?? null;
$external = isset($attributes['external']);
$download = $attributes['download'] ?? null;
// Extract ARIA attributes
$ariaLabel = $attributes['aria-label'] ?? null;
$ariaCurrent = $attributes['aria-current'] ?? null;
$ariaDescribedBy = $attributes['aria-described-by'] ?? null;
// Build HtmlLink
$htmlLink = HtmlLink::create($href, $this->text);
if ($title !== null) {
$htmlLink = $htmlLink->withTitle($title);
}
if ($class !== null) {
$htmlLink = $htmlLink->withClass($class);
}
// Handle target attribute
if ($target !== null) {
$linkTarget = match ($target) {
'_blank' => LinkTarget::BLANK,
'_self' => LinkTarget::SELF,
'_parent' => LinkTarget::PARENT,
'_top' => LinkTarget::TOP,
default => null
};
if ($linkTarget !== null) {
$htmlLink = $htmlLink->withTarget($linkTarget);
}
}
// Handle rel attribute
if ($rel !== null) {
$relValues = array_map(
fn(string $r) => match (trim($r)) {
'noopener' => LinkRel::NOOPENER,
'noreferrer' => LinkRel::NOREFERRER,
'nofollow' => LinkRel::NOFOLLOW,
'external' => LinkRel::EXTERNAL,
default => null
},
explode(' ', $rel)
);
$relValues = array_filter($relValues);
if (!empty($relValues)) {
$htmlLink = $htmlLink->withRel(...$relValues);
}
}
// Handle external links (auto-adds security attributes)
if ($external) {
$htmlLink = HtmlLink::external($href, $this->text);
if ($class !== null) {
$htmlLink = $htmlLink->withClass($class);
}
if ($title !== null) {
$htmlLink = $htmlLink->withTitle($title);
}
}
// Handle downloads
if ($download !== null) {
$htmlLink = HtmlLink::download($href, $this->text, $download !== '' ? $download : null);
if ($class !== null) {
$htmlLink = $htmlLink->withClass($class);
}
if ($title !== null) {
$htmlLink = $htmlLink->withTitle($title);
}
}
// Wrap in AccessibleLink for ARIA support
$link = AccessibleLink::fromHtmlLink($htmlLink);
if ($ariaLabel !== null) {
$link = $link->withAriaLabel($ariaLabel);
}
if ($ariaCurrent !== null) {
$link = $link->withAriaCurrent(true, $ariaCurrent);
}
if ($ariaDescribedBy !== null) {
$link = new AccessibleLink(
baseLink: $link->baseLink,
ariaLabel: $link->ariaLabel,
ariaDescribedBy: $ariaDescribedBy,
ariaCurrent: $link->ariaCurrent,
ariaCurrentValue: $link->ariaCurrentValue,
);
}
// Store the AccessibleLink
$this->link = $link;
}
public function getRootNode(): Node
{
$a = new ElementNode('a');
// Add href
$a->setAttribute('href', (string) $this->link->baseLink->href);
// Add class if present
if ($this->link->baseLink->cssClass !== null) {
$a->setAttribute('class', $this->link->baseLink->cssClass);
}
// Add title if present
if ($this->link->baseLink->title !== null) {
$a->setAttribute('title', $this->link->baseLink->title);
}
// Add target if present
if ($this->link->baseLink->target !== null) {
$a->setAttribute('target', $this->link->baseLink->target->value);
}
// Add rel if present
if (!empty($this->link->baseLink->rel)) {
$relValues = array_map(fn($r) => $r->value, $this->link->baseLink->rel);
$a->setAttribute('rel', implode(' ', $relValues));
}
// Add download if present
if ($this->link->baseLink->download !== null) {
$a->setAttribute('download', $this->link->baseLink->download);
}
// Add ARIA attributes
if ($this->link->ariaLabel !== null) {
$a->setAttribute('aria-label', $this->link->ariaLabel);
}
if ($this->link->ariaCurrent) {
$a->setAttribute('aria-current', $this->link->ariaCurrentValue ?? 'true');
}
if ($this->link->ariaDescribedBy !== null) {
$a->setAttribute('aria-describedby', $this->link->ariaDescribedBy);
}
// Add text content
$textNode = new TextNode($this->text);
$a->appendChild($textNode);
return $a;
}
}