| /* |
| * Copyright (C) 2016 Igalia, S.L. |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "AccessibilitySVGObject.h" |
| |
| #include "AccessibilityNodeObjectInlines.h" |
| #include "AXObjectCacheInlines.h" |
| #include "AXUtilities.h" |
| #include "AccessibilityObjectInlines.h" |
| #include "ElementChildIteratorInlines.h" |
| #include "ElementInlines.h" |
| #include "HTMLNames.h" |
| #include "RenderIterator.h" |
| #include "RenderObject.h" |
| #include "RenderObjectInlines.h" |
| #include "RenderText.h" |
| #include "SVGAElement.h" |
| #include "SVGDescElement.h" |
| #include "SVGElementTypeHelpers.h" |
| #include "SVGGElement.h" |
| #include "SVGNames.h" |
| #include "SVGTitleElement.h" |
| #include "SVGUseElement.h" |
| #include "TypedElementDescendantIteratorInlines.h" |
| #include "XLinkNames.h" |
| #include <wtf/Language.h> |
| #include <wtf/NeverDestroyed.h> |
| |
| namespace WebCore { |
| |
| AccessibilitySVGObject::AccessibilitySVGObject(AXID axID, RenderObject& renderer, AXObjectCache& cache, bool isSVGRoot) |
| : AccessibilityRenderObject(axID, renderer, cache) |
| { |
| m_isSVGRoot = isSVGRoot; |
| } |
| |
| AccessibilitySVGObject::AccessibilitySVGObject(AXID axID, Element& element, AXObjectCache& cache, bool isSVGRoot) |
| : AccessibilityRenderObject(axID, element, cache) |
| { |
| m_isSVGRoot = isSVGRoot; |
| } |
| |
| AccessibilitySVGObject::~AccessibilitySVGObject() = default; |
| |
| Ref<AccessibilitySVGObject> AccessibilitySVGObject::create(AXID axID, RenderObject& renderer, AXObjectCache& cache, bool isSVGRoot) |
| { |
| return adoptRef(*new AccessibilitySVGObject(axID, renderer, cache, isSVGRoot)); |
| } |
| |
| Ref<AccessibilitySVGObject> AccessibilitySVGObject::create(AXID axID, Element& element, AXObjectCache& cache, bool isSVGRoot) |
| { |
| return adoptRef(*new AccessibilitySVGObject(axID, element, cache, isSVGRoot)); |
| } |
| |
| AccessibilityObject* AccessibilitySVGObject::targetForUseElement() const |
| { |
| RefPtr use = dynamicDowncast<SVGUseElement>(element()); |
| if (!use) |
| return nullptr; |
| |
| auto href = use->href(); |
| if (href.isEmpty()) |
| href = getAttribute(HTMLNames::hrefAttr); |
| |
| auto target = SVGURIReference::targetElementFromIRIString(href, Ref { use->treeScopeForSVGReferences() }); |
| CheckedPtr cache = axObjectCache(); |
| return cache ? cache->getOrCreate(target.element.get()) : nullptr; |
| } |
| |
| void AccessibilitySVGObject::accessibilityText(Vector<AccessibilityText>& textOrder) const |
| { |
| // Compute both description and help text together to avoid redundant language |
| // matching, which showed up on samples taken on Speedometer 3.1. |
| auto [titleChild, descChild] = matchingTitleAndDescChildren(); |
| |
| String descriptionText = descriptionFromTitleChild(titleChild.get()); |
| if (!descriptionText.isEmpty()) |
| textOrder.append(AccessibilityText(descriptionText, AccessibilityTextSource::Alternative)); |
| |
| String helptext = helpTextFromChildren(titleChild.get(), descChild.get(), descriptionText); |
| if (!helptext.isEmpty()) |
| textOrder.append(AccessibilityText(WTF::move(helptext), AccessibilityTextSource::Help)); |
| } |
| |
| void AccessibilitySVGObject::alternativeText(Vector<AccessibilityText>& textOrder) const |
| { |
| // https://w3c.github.io/svg-aam/#mapping_additional_nd |
| // Per SVG-AAM ยง7.1, the accessible name comes from aria-labelledby/aria-label, |
| // a <title> child, xlink:title (on <a>), the <use> target, or alt (on <image>). |
| // Expose this as Alternative text so host-language parents (e.g. <a> or <button>) |
| // pick it up via textUnderElement when deriving their own accname. |
| String descriptionText = description(); |
| if (!descriptionText.isEmpty()) |
| textOrder.append(AccessibilityText(WTF::move(descriptionText), AccessibilityTextSource::Alternative)); |
| } |
| |
| auto AccessibilitySVGObject::matchingTitleAndDescChildren() const -> MatchingLanguageChildren |
| { |
| RefPtr element = this->element(); |
| if (!element) |
| return { }; |
| |
| // Iterate over SVG children once, bucketing <title> and <desc> elements, |
| // then do language matching with a single languageIncludingAncestors() call. |
| // This is an optimization based on a profile taken on Speedometer. |
| Vector<String> titleLangs; |
| Vector<Element*> titleElements; |
| RefPtr<Element> titleFallback; |
| bool hasTitleWithLang = false; |
| Vector<String> descLangs; |
| Vector<Element*> descElements; |
| RefPtr<Element> descFallback; |
| bool hasDescWithLang = false; |
| |
| for (Ref child : childrenOfType<SVGElement>(*element)) { |
| const auto& lang = child->attributeWithoutSynchronization(SVGNames::langAttr); |
| if (is<SVGTitleElement>(child.get())) { |
| titleLangs.append(lang); |
| titleElements.append(const_cast<Element*>(static_cast<const Element*>(child.ptr()))); |
| if (lang.isEmpty()) { |
| if (!titleFallback) |
| titleFallback = child.ptr(); |
| } else |
| hasTitleWithLang = true; |
| } else if (is<SVGDescElement>(child.get())) { |
| descLangs.append(lang); |
| descElements.append(const_cast<Element*>(static_cast<const Element*>(child.ptr()))); |
| if (lang.isEmpty()) { |
| if (!descFallback) |
| descFallback = child.ptr(); |
| } else |
| hasDescWithLang = true; |
| } |
| } |
| |
| // If no child has a lang attribute, language matching can't improve on the |
| // fallback. Skip the expensive languageIncludingAncestors() and NSLocale calls. |
| if (!hasTitleWithLang && !hasDescWithLang) |
| return { WTF::move(titleFallback), WTF::move(descFallback) }; |
| |
| String languageCode = languageIncludingAncestors(); |
| if (languageCode.isEmpty()) |
| languageCode = defaultLanguage(); |
| |
| auto matchInList = [&](Vector<String>& langs, Vector<Element*>& elements, RefPtr<Element>& fallback) -> RefPtr<Element> { |
| bool exactMatch; |
| size_t index = indexOfBestMatchingLanguageInList(languageCode, langs, exactMatch); |
| if (index < langs.size()) |
| return elements[index]; |
| return fallback; |
| }; |
| |
| RefPtr titleChild = hasTitleWithLang ? matchInList(titleLangs, titleElements, titleFallback) : WTF::move(titleFallback); |
| RefPtr descChild = hasDescWithLang ? matchInList(descLangs, descElements, descFallback) : WTF::move(descFallback); |
| return { WTF::move(titleChild), WTF::move(descChild) }; |
| } |
| |
| String AccessibilitySVGObject::descriptionFromTitleChild(Element* titleChild) const |
| { |
| // Priority per SVG AAM: aria-labelledby/label, title child, xlink:title, use target, alt. |
| String result = ariaAccessibilityDescription(); |
| if (!result.isEmpty()) |
| return result; |
| |
| if (titleChild) { |
| result = titleChild->textContent(); |
| if (!result.isEmpty()) |
| return result; |
| } |
| |
| RefPtr element = this->element(); |
| if (is<SVGAElement>(element.get())) { |
| const auto& xlinkTitle = element->attributeWithoutSynchronization(XLinkNames::titleAttr); |
| if (!xlinkTitle.isEmpty()) |
| return xlinkTitle; |
| } |
| |
| if (RefPtr target = targetForUseElement()) { |
| // Avoid infinite recursion from circular <use> references by tracking ones we're currently resolving. |
| static NeverDestroyed<HashSet<Element*>> elementsResolvingDescription; |
| if (elementsResolvingDescription->add(element.get()).isNewEntry) { |
| auto result = target->description(); |
| elementsResolvingDescription->remove(element.get()); |
| return result; |
| } |
| } |
| |
| if (m_renderer && m_renderer->isRenderOrLegacyRenderSVGImage()) { |
| const auto& alt = getAttribute(HTMLNames::altAttr); |
| if (!alt.isNull()) |
| return alt; |
| } |
| |
| return { }; |
| } |
| |
| String AccessibilitySVGObject::helpTextFromChildren(Element* titleChild, Element* descChild, const String& descriptionText) const |
| { |
| // Priority per SVG AAM: aria-describedby, desc child, use target, title child (if != description). |
| RefPtr element = this->element(); |
| if (!element) |
| return { }; |
| |
| String result = ariaDescribedByAttribute(); |
| if (!result.isEmpty()) |
| return result; |
| |
| if (descChild) { |
| result = descChild->textContent(); |
| if (!result.isEmpty()) |
| return result; |
| } |
| |
| if (RefPtr target = targetForUseElement()) { |
| // Avoid infinite recursion from circular <use> references by tracking ones we're currently resolving. |
| static NeverDestroyed<HashSet<Element*>> elementsResolvingHelpText; |
| if (elementsResolvingHelpText->add(element.get()).isNewEntry) { |
| auto result = target->helpText(); |
| elementsResolvingHelpText->remove(element.get()); |
| return result; |
| } |
| } |
| |
| if (titleChild) { |
| auto titleText = titleChild->textContent(); |
| if (titleText != descriptionText) |
| return titleText; |
| } |
| |
| return { }; |
| } |
| |
| String AccessibilitySVGObject::description() const |
| { |
| auto [titleChild, descChild] = matchingTitleAndDescChildren(); |
| return descriptionFromTitleChild(titleChild.get()); |
| } |
| |
| String AccessibilitySVGObject::helpText() const |
| { |
| auto [titleChild, descChild] = matchingTitleAndDescChildren(); |
| String descriptionText = descriptionFromTitleChild(titleChild.get()); |
| return helpTextFromChildren(titleChild.get(), descChild.get(), descriptionText); |
| } |
| |
| bool AccessibilitySVGObject::hasTitleOrDescriptionChild() const |
| { |
| RefPtr element = this->element(); |
| if (!element) |
| return false; |
| |
| for (const auto& child : childrenOfType<SVGElement>(*element)) { |
| if (is<SVGTitleElement>(child) || is<SVGDescElement>(child)) |
| return true; |
| } |
| return false; |
| } |
| |
| bool AccessibilitySVGObject::computeIsIgnored() const |
| { |
| // According to the SVG Accessibility API Mappings spec, items should be excluded if: |
| // * They would be excluded according to the Core Accessibility API Mappings. |
| // * They are neither perceivable nor interactive. |
| // * Their first mappable role is presentational, unless they have a global ARIA |
| // attribute (covered by Core AAM) or at least one 'title' or 'desc' child element. |
| // * They have an ancestor with Children Presentational: True (covered by Core AAM) |
| |
| AccessibilityObjectInclusion decision = defaultObjectInclusion(); |
| if (decision == AccessibilityObjectInclusion::IgnoreObject) |
| return true; |
| |
| if (!m_renderer || m_renderer->isLegacyRenderSVGHiddenContainer() || m_renderer->isRenderSVGHiddenContainer()) |
| return true; |
| |
| // The SVG AAM states objects with at least one 'title' or 'desc' element MUST be included. |
| // At this time, the presence of a matching 'lang' attribute is not mentioned in the spec. |
| if (hasTitleOrDescriptionChild()) |
| return false; |
| |
| if (ignoredFromPresentationalRole()) |
| return true; |
| |
| if (ariaRoleAttribute() != AccessibilityRole::Unknown) |
| return false; |
| |
| // The SVG AAM states text elements should also be included, if they have content. |
| if (m_renderer->isRenderSVGText() || m_renderer->isRenderSVGTextPath()) { |
| for (CheckedRef child : childrenOfType<RenderText>(downcast<RenderElement>(*m_renderer))) { |
| if (!child->containsOnlyCollapsibleWhitespace()) |
| return false; |
| } |
| } |
| |
| // SVG shapes should not be included unless there's a concrete reason for inclusion. |
| // https://rawgit.com/w3c/aria/master/svg-aam/svg-aam.html#exclude_elements |
| if (m_renderer->isRenderOrLegacyRenderSVGShape()) { |
| if (canSetFocusAttribute() || element()->hasEventListeners()) |
| return false; |
| if (RefPtr svgParent = Accessibility::findAncestor<AccessibilityObject>(*this, true, [] (const AccessibilityObject& object) { |
| return object.hasAttributesRequiredForInclusion() || object.isAccessibilitySVGRoot(); |
| })) |
| return !svgParent->hasAttributesRequiredForInclusion(); |
| return true; |
| } |
| |
| return AccessibilityRenderObject::computeIsIgnored(); |
| } |
| |
| bool AccessibilitySVGObject::inheritsPresentationalRole() const |
| { |
| if (canSetFocusAttribute()) |
| return false; |
| |
| auto role = this->role(); |
| if (role != AccessibilityRole::SVGTextPath && role != AccessibilityRole::SVGTSpan) |
| return false; |
| |
| for (RefPtr parent = parentObject(); parent; parent = parent->parentObject()) { |
| if (is<AccessibilityRenderObject>(*parent) && parent->hasElementName(ElementName::SVG_text)) |
| return parent->role() == AccessibilityRole::Presentational; |
| } |
| |
| return false; |
| } |
| |
| AccessibilityRole AccessibilitySVGObject::determineAriaRoleAttribute() const |
| { |
| auto role = AccessibilityRenderObject::determineAriaRoleAttribute(); |
| if (role != AccessibilityRole::Presentational) |
| return role; |
| |
| // The presence of a 'title' or 'desc' child element trumps PresentationalRole. |
| // https://lists.w3.org/Archives/Public/public-svg-a11y/2016Apr/0016.html |
| // At this time, the presence of a matching 'lang' attribute is not mentioned. |
| return hasTitleOrDescriptionChild() ? AccessibilityRole::Unknown : role; |
| } |
| |
| AccessibilityRole AccessibilitySVGObject::determineAccessibilityRole() |
| { |
| if (m_ariaRole != AccessibilityRole::Unknown) |
| return m_ariaRole; |
| |
| RefPtr element = this->element(); |
| if (is<SVGGElement>(element)) { |
| // https://w3c.github.io/svg-aam/#include_elements |
| // g elements are generic (like a div) unless they have a name or is focusable. |
| // Evaluated before the renderer check so display:contents <g> still exposes its role. |
| if (WebCore::hasAccNameAttribute(*element) || hasTitleOrDescriptionChild() || canSetFocusAttribute()) |
| return AccessibilityRole::Group; |
| return AccessibilityRole::Generic; |
| } |
| |
| if (!m_renderer) |
| return AccessibilityRole::Unknown; |
| |
| if (isAccessibilitySVGRoot()) |
| return AccessibilityRole::Generic; |
| |
| if (m_renderer->isRenderOrLegacyRenderSVGShape() || m_renderer->isRenderOrLegacyRenderSVGPath() || m_renderer->isRenderOrLegacyRenderSVGImage() || is<SVGUseElement>(element)) |
| return AccessibilityRole::Image; |
| if (m_renderer->isRenderOrLegacyRenderSVGForeignObject()) |
| return AccessibilityRole::Generic; |
| if (m_renderer->isRenderSVGInlineText()) |
| return AccessibilityRole::StaticText; |
| if (m_renderer->isRenderSVGText()) |
| return AccessibilityRole::SVGText; |
| if (m_renderer->isRenderSVGTextPath()) |
| return AccessibilityRole::SVGTextPath; |
| if (m_renderer->isRenderSVGTSpan()) |
| return AccessibilityRole::SVGTSpan; |
| if (is<SVGAElement>(element)) { |
| if (element->isLink() || hasClickHandler()) |
| return AccessibilityRole::Link; |
| } |
| |
| return AccessibilityRenderObject::determineAccessibilityRole(); |
| } |
| |
| AccessibilityObject* AccessibilitySVGObject::parentObject() const |
| { |
| if (m_parent) { |
| // If a parent was set because this is a remote SVG resource, use that. |
| AX_ASSERT(m_isSVGRoot); |
| return m_parent.get(); |
| } |
| |
| // Otherwise, we should rely on the standard render tree for the parent. |
| return AccessibilityRenderObject::parentObject(); |
| } |
| |
| bool AccessibilitySVGObject::isRootWithAccessibleContent() const |
| { |
| if (!isAccessibilitySVGRoot()) |
| return false; |
| |
| RefPtr rootElement = this->element(); |
| if (!rootElement) |
| return false; |
| |
| auto isAccessibleSVGElement = [] (const SVGElement& element) -> bool { |
| // The presence of an SVGTitle or SVGDesc element is enough to deem the SVG hierarchy as accessible. |
| if (is<SVGTitleElement>(element) |
| || is<SVGDescElement>(element)) |
| return true; |
| |
| // Text content is accessible. |
| if (element.isTextContent()) |
| return true; |
| |
| // If the role or aria-label attributes are specified, this is accessible. |
| if (!element.attributeWithoutSynchronization(HTMLNames::roleAttr).isEmpty() |
| || !element.attributeWithoutSynchronization(HTMLNames::aria_labelAttr).isEmpty()) |
| return true; |
| |
| return false; |
| }; |
| |
| RefPtr svgRootElement = dynamicDowncast<SVGElement>(*rootElement); |
| if (svgRootElement && isAccessibleSVGElement(*svgRootElement)) |
| return true; |
| |
| // This SVG hierarchy is accessible if any of its descendants is accessible. |
| for (const Ref descendant : descendantsOfType<SVGElement>(*rootElement)) { |
| if (isAccessibleSVGElement(descendant.get())) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| } // namespace WebCore |