1 | /* |
2 | * Copyright (C) 2016 Igalia, S.L. |
3 | * All rights reserved. |
4 | * |
5 | * Redistribution and use in source and binary forms, with or without |
6 | * modification, are permitted provided that the following conditions |
7 | * are met: |
8 | * |
9 | * 1. Redistributions of source code must retain the above copyright |
10 | * notice, this list of conditions and the following disclaimer. |
11 | * 2. Redistributions in binary form must reproduce the above copyright |
12 | * notice, this list of conditions and the following disclaimer in the |
13 | * documentation and/or other materials provided with the distribution. |
14 | * |
15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
19 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
26 | */ |
27 | |
28 | #include "config.h" |
29 | #include "AccessibilitySVGElement.h" |
30 | |
31 | #include "AXObjectCache.h" |
32 | #include "ElementIterator.h" |
33 | #include "HTMLNames.h" |
34 | #include "RenderIterator.h" |
35 | #include "RenderText.h" |
36 | #include "SVGAElement.h" |
37 | #include "SVGDescElement.h" |
38 | #include "SVGGElement.h" |
39 | #include "SVGTitleElement.h" |
40 | #include "SVGUseElement.h" |
41 | #include "XLinkNames.h" |
42 | #include <wtf/Language.h> |
43 | |
44 | namespace WebCore { |
45 | |
46 | AccessibilitySVGElement::AccessibilitySVGElement(RenderObject* renderer) |
47 | : AccessibilityRenderObject(renderer) |
48 | { |
49 | } |
50 | |
51 | AccessibilitySVGElement::~AccessibilitySVGElement() = default; |
52 | |
53 | Ref<AccessibilitySVGElement> AccessibilitySVGElement::create(RenderObject* renderer) |
54 | { |
55 | return adoptRef(*new AccessibilitySVGElement(renderer)); |
56 | } |
57 | |
58 | AccessibilityObject* AccessibilitySVGElement::targetForUseElement() const |
59 | { |
60 | if (!is<SVGUseElement>(element())) |
61 | return nullptr; |
62 | |
63 | SVGUseElement& use = downcast<SVGUseElement>(*element()); |
64 | String href = use.href(); |
65 | if (href.isEmpty()) |
66 | href = getAttribute(HTMLNames::hrefAttr); |
67 | |
68 | auto target = SVGURIReference::targetElementFromIRIString(href, use.treeScope()); |
69 | if (!target.element) |
70 | return nullptr; |
71 | return axObjectCache()->getOrCreate(target.element.get()); |
72 | } |
73 | |
74 | template <typename ChildrenType> |
75 | Element* AccessibilitySVGElement::childElementWithMatchingLanguage(ChildrenType& children) const |
76 | { |
77 | String languageCode = language(); |
78 | if (languageCode.isEmpty()) |
79 | languageCode = defaultLanguage(); |
80 | |
81 | // The best match for a group of child SVG2 'title' or 'desc' elements may be the one |
82 | // which lacks a 'lang' attribute value. However, indexOfBestMatchingLanguageInList() |
83 | // currently bases its decision on non-empty strings. Furthermore, we cannot count on |
84 | // that child element having a given position. So we'll look for such an element while |
85 | // building the language list and save it as our fallback. |
86 | |
87 | Element* fallback = nullptr; |
88 | Vector<String> childLanguageCodes; |
89 | Vector<Element*> elements; |
90 | for (auto& child : children) { |
91 | auto& lang = child.attributeWithoutSynchronization(SVGNames::langAttr); |
92 | childLanguageCodes.append(lang); |
93 | elements.append(&child); |
94 | |
95 | // The current draft of the SVG2 spec states if there are multiple equally-valid |
96 | // matches, the first match should be used. |
97 | if (lang.isEmpty() && !fallback) |
98 | fallback = &child; |
99 | } |
100 | |
101 | bool exactMatch; |
102 | size_t index = indexOfBestMatchingLanguageInList(languageCode, childLanguageCodes, exactMatch); |
103 | if (index < childLanguageCodes.size()) |
104 | return elements[index]; |
105 | |
106 | return fallback; |
107 | } |
108 | |
109 | void AccessibilitySVGElement::accessibilityText(Vector<AccessibilityText>& textOrder) const |
110 | { |
111 | String description = accessibilityDescription(); |
112 | if (!description.isEmpty()) |
113 | textOrder.append(AccessibilityText(description, AccessibilityTextSource::Alternative)); |
114 | |
115 | String helptext = helpText(); |
116 | if (!helptext.isEmpty()) |
117 | textOrder.append(AccessibilityText(helptext, AccessibilityTextSource::Help)); |
118 | } |
119 | |
120 | String AccessibilitySVGElement::accessibilityDescription() const |
121 | { |
122 | // According to the SVG Accessibility API Mappings spec, the order of priority is: |
123 | // 1. aria-labelledby |
124 | // 2. aria-label |
125 | // 3. a direct child title element (selected according to language) |
126 | // 4. xlink:title attribute |
127 | // 5. for a use element, the accessible name calculated for the re-used content |
128 | // 6. for text container elements, the text content |
129 | |
130 | String ariaDescription = ariaAccessibilityDescription(); |
131 | if (!ariaDescription.isEmpty()) |
132 | return ariaDescription; |
133 | |
134 | auto titleElements = childrenOfType<SVGTitleElement>(*element()); |
135 | if (auto titleChild = childElementWithMatchingLanguage(titleElements)) |
136 | return titleChild->textContent(); |
137 | |
138 | if (is<SVGAElement>(element())) { |
139 | auto& xlinkTitle = element()->attributeWithoutSynchronization(XLinkNames::titleAttr); |
140 | if (!xlinkTitle.isEmpty()) |
141 | return xlinkTitle; |
142 | } |
143 | |
144 | if (m_renderer->isSVGText()) { |
145 | AccessibilityTextUnderElementMode mode; |
146 | String text = textUnderElement(mode); |
147 | if (!text.isEmpty()) |
148 | return text; |
149 | } |
150 | |
151 | if (is<SVGUseElement>(element())) { |
152 | if (AccessibilityObject* target = targetForUseElement()) |
153 | return target->accessibilityDescription(); |
154 | } |
155 | |
156 | // FIXME: This is here to not break the svg-image.html test. But 'alt' is not |
157 | // listed as a supported attribute of the 'image' element in the SVG spec: |
158 | // https://www.w3.org/TR/SVG/struct.html#ImageElement |
159 | if (m_renderer->isSVGImage()) { |
160 | const AtomicString& alt = getAttribute(HTMLNames::altAttr); |
161 | if (!alt.isNull()) |
162 | return alt; |
163 | } |
164 | |
165 | return String(); |
166 | } |
167 | |
168 | String AccessibilitySVGElement::helpText() const |
169 | { |
170 | // According to the SVG Accessibility API Mappings spec, the order of priority is: |
171 | // 1. aria-describedby |
172 | // 2. a direct child desc element |
173 | // 3. for a use element, the accessible description calculated for the re-used content |
174 | // 4. for text container elements, the text content, if not used for the name |
175 | // 5. a direct child title element that provides a tooltip, if not used for the name |
176 | |
177 | String describedBy = ariaDescribedByAttribute(); |
178 | if (!describedBy.isEmpty()) |
179 | return describedBy; |
180 | |
181 | auto descriptionElements = childrenOfType<SVGDescElement>(*element()); |
182 | if (auto descriptionChild = childElementWithMatchingLanguage(descriptionElements)) |
183 | return descriptionChild->textContent(); |
184 | |
185 | if (is<SVGUseElement>(element())) { |
186 | AccessibilityObject* target = targetForUseElement(); |
187 | if (target) |
188 | return target->helpText(); |
189 | } |
190 | |
191 | String description = accessibilityDescription(); |
192 | |
193 | if (m_renderer->isSVGText()) { |
194 | AccessibilityTextUnderElementMode mode; |
195 | String text = textUnderElement(mode); |
196 | if (!text.isEmpty() && text != description) |
197 | return text; |
198 | } |
199 | |
200 | auto titleElements = childrenOfType<SVGTitleElement>(*element()); |
201 | if (auto titleChild = childElementWithMatchingLanguage(titleElements)) { |
202 | if (titleChild->textContent() != description) |
203 | return titleChild->textContent(); |
204 | } |
205 | |
206 | return String(); |
207 | } |
208 | |
209 | bool AccessibilitySVGElement::computeAccessibilityIsIgnored() const |
210 | { |
211 | // According to the SVG Accessibility API Mappings spec, items should be excluded if: |
212 | // * They would be excluded according to the Core Accessibility API Mappings. |
213 | // * They are neither perceivable nor interactive. |
214 | // * Their first mappable role is presentational, unless they have a global ARIA |
215 | // attribute (covered by Core AAM) or at least one 'title' or 'desc' child element. |
216 | // * They have an ancestor with Children Presentational: True (covered by Core AAM) |
217 | |
218 | AccessibilityObjectInclusion decision = defaultObjectInclusion(); |
219 | if (decision == AccessibilityObjectInclusion::IgnoreObject) |
220 | return true; |
221 | |
222 | if (m_renderer->isSVGHiddenContainer()) |
223 | return true; |
224 | |
225 | // The SVG AAM states objects with at least one 'title' or 'desc' element MUST be included. |
226 | // At this time, the presence of a matching 'lang' attribute is not mentioned in the spec. |
227 | for (const auto& child : childrenOfType<SVGElement>(*element())) { |
228 | if ((is<SVGTitleElement>(child) || is<SVGDescElement>(child))) |
229 | return false; |
230 | } |
231 | |
232 | if (roleValue() == AccessibilityRole::Presentational || inheritsPresentationalRole()) |
233 | return true; |
234 | |
235 | if (ariaRoleAttribute() != AccessibilityRole::Unknown) |
236 | return false; |
237 | |
238 | // The SVG AAM states text elements should also be included, if they have content. |
239 | if (m_renderer->isSVGText() || m_renderer->isSVGTextPath()) { |
240 | for (auto& child : childrenOfType<RenderText>(downcast<RenderElement>(*m_renderer))) { |
241 | if (!child.isAllCollapsibleWhitespace()) |
242 | return false; |
243 | } |
244 | } |
245 | |
246 | // SVG shapes should not be included unless there's a concrete reason for inclusion. |
247 | // https://rawgit.com/w3c/aria/master/svg-aam/svg-aam.html#exclude_elements |
248 | if (m_renderer->isSVGShape()) { |
249 | if (canSetFocusAttribute() || element()->hasEventListeners()) |
250 | return false; |
251 | if (auto svgParent = AccessibilityObject::matchedParent(*this, true, [] (const AccessibilityObject& object) { |
252 | return object.hasAttributesRequiredForInclusion() || object.isAccessibilitySVGRoot(); |
253 | })) |
254 | return !svgParent->hasAttributesRequiredForInclusion(); |
255 | return true; |
256 | } |
257 | |
258 | return AccessibilityRenderObject::computeAccessibilityIsIgnored(); |
259 | } |
260 | |
261 | bool AccessibilitySVGElement::inheritsPresentationalRole() const |
262 | { |
263 | if (canSetFocusAttribute()) |
264 | return false; |
265 | |
266 | AccessibilityRole role = roleValue(); |
267 | if (role != AccessibilityRole::SVGTextPath && role != AccessibilityRole::SVGTSpan) |
268 | return false; |
269 | |
270 | for (AccessibilityObject* parent = parentObject(); parent; parent = parent->parentObject()) { |
271 | if (is<AccessibilityRenderObject>(*parent) && parent->element()->hasTagName(SVGNames::textTag)) |
272 | return parent->roleValue() == AccessibilityRole::Presentational; |
273 | } |
274 | |
275 | return false; |
276 | } |
277 | |
278 | AccessibilityRole AccessibilitySVGElement::determineAriaRoleAttribute() const |
279 | { |
280 | AccessibilityRole role = AccessibilityRenderObject::determineAriaRoleAttribute(); |
281 | if (role != AccessibilityRole::Presentational) |
282 | return role; |
283 | |
284 | // The presence of a 'title' or 'desc' child element trumps PresentationalRole. |
285 | // https://lists.w3.org/Archives/Public/public-svg-a11y/2016Apr/0016.html |
286 | // At this time, the presence of a matching 'lang' attribute is not mentioned. |
287 | for (const auto& child : childrenOfType<SVGElement>(*element())) { |
288 | if ((is<SVGTitleElement>(child) || is<SVGDescElement>(child))) |
289 | return AccessibilityRole::Unknown; |
290 | } |
291 | |
292 | return role; |
293 | } |
294 | |
295 | AccessibilityRole AccessibilitySVGElement::determineAccessibilityRole() |
296 | { |
297 | if ((m_ariaRole = determineAriaRoleAttribute()) != AccessibilityRole::Unknown) |
298 | return m_ariaRole; |
299 | |
300 | Element* svgElement = element(); |
301 | |
302 | if (m_renderer->isSVGShape() || m_renderer->isSVGPath() || m_renderer->isSVGImage() || is<SVGUseElement>(svgElement)) |
303 | return AccessibilityRole::Image; |
304 | if (m_renderer->isSVGForeignObject() || is<SVGGElement>(svgElement)) |
305 | return AccessibilityRole::Group; |
306 | if (m_renderer->isSVGText()) |
307 | return AccessibilityRole::SVGText; |
308 | if (m_renderer->isSVGTextPath()) |
309 | return AccessibilityRole::SVGTextPath; |
310 | if (m_renderer->isSVGTSpan()) |
311 | return AccessibilityRole::SVGTSpan; |
312 | if (is<SVGAElement>(svgElement)) |
313 | return AccessibilityRole::WebCoreLink; |
314 | |
315 | return AccessibilityRenderObject::determineAccessibilityRole(); |
316 | } |
317 | |
318 | } // namespace WebCore |
319 | |