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; } }