Skip to content

Commit b9f41c9

Browse files
authored
Add support for <title> and <desc> elements in SVG for accessibility
When passing title and/or desc as attributes to ux_icon(), they are now embedded directly into the SVG as <title> and <desc> child elements. Each <title> and <desc> element is given a unique id and automatically referenced via aria-labelledby, if no such attribute was already set. This ensures compliance with accessibility best practices for inline SVG content. See: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title https://developer.mozilla.org/en-US/docs/Web/SVG/Element/desc
1 parent 9e5f700 commit b9f41c9

File tree

1 file changed

+33
-7
lines changed

1 file changed

+33
-7
lines changed

src/Icons/src/Icon.php

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,27 +139,53 @@ public function __construct(
139139
public function toHtml(): string
140140
{
141141
$htmlAttributes = '';
142-
foreach ($this->attributes as $name => $value) {
142+
$innerSvg = $this->innerSvg;
143+
$attributes = $this->attributes;
144+
145+
// Extract and remove title/desc attributes if present
146+
$title = $attributes['title'] ?? null;
147+
$desc = $attributes['desc'] ?? null;
148+
unset($attributes['title'], $attributes['desc']);
149+
150+
// Prepare <title> and <desc> elements
151+
$labelledByIds = [];
152+
$a11yContent = '';
153+
154+
if ($title) {
155+
$titleId = 'title-' . bin2hex(random_bytes(4));
156+
$labelledByIds[] = $titleId;
157+
$a11yContent .= sprintf('<title id="%s">%s</title>', $titleId, htmlspecialchars((string) $title, ENT_QUOTES));
158+
}
159+
160+
if ($desc) {
161+
$descId = 'desc-' . bin2hex(random_bytes(4));
162+
$labelledByIds[] = $descId;
163+
$a11yContent .= sprintf('<desc id="%s">%s</desc>', $descId, htmlspecialchars((string) $desc, ENT_QUOTES));
164+
}
165+
166+
// Only add aria-labelledby if not already present and we have content
167+
if ($a11yContent !== '' && !isset($attributes['aria-labelledby'])) {
168+
$attributes['aria-labelledby'] = implode(' ', $labelledByIds);
169+
}
170+
171+
// Build final attributes string
172+
foreach ($attributes as $name => $value) {
143173
if (false === $value) {
144174
continue;
145175
}
146176

147-
// Special case for aria-* attributes
148-
// https://www.w3.org/TR/wai-aria-1.1/#state_prop_def
149177
if (true === $value && str_starts_with($name, 'aria-')) {
150178
$value = 'true';
151179
}
152180

153181
$htmlAttributes .= ' '.$name;
182+
154183
if (true === $value) {
155184
continue;
156185
}
157186

158-
$value = htmlspecialchars($value, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
187+
$value = htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
159188
$htmlAttributes .= '="'.$value.'"';
160-
}
161-
162-
return '<svg'.$htmlAttributes.'>'.$this->innerSvg.'</svg>';
163189
}
164190

165191
public function getInnerSvg(): string

0 commit comments

Comments
 (0)