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