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
44namespace WebCore {
45
46AccessibilitySVGElement::AccessibilitySVGElement(RenderObject* renderer)
47 : AccessibilityRenderObject(renderer)
48{
49}
50
51AccessibilitySVGElement::~AccessibilitySVGElement() = default;
52
53Ref<AccessibilitySVGElement> AccessibilitySVGElement::create(RenderObject* renderer)
54{
55 return adoptRef(*new AccessibilitySVGElement(renderer));
56}
57
58AccessibilityObject* 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
74template <typename ChildrenType>
75Element* 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
109void 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
120String 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
168String 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
209bool 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
261bool 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
278AccessibilityRole 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
295AccessibilityRole 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