diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/content/renderer/accessibility | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-chromium-85-based.tar.gz |
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/content/renderer/accessibility')
13 files changed, 1841 insertions, 654 deletions
diff --git a/chromium/content/renderer/accessibility/aom_content_ax_tree.cc b/chromium/content/renderer/accessibility/aom_content_ax_tree.cc index 4bce29368ff..67f9ebc0a04 100644 --- a/chromium/content/renderer/accessibility/aom_content_ax_tree.cc +++ b/chromium/content/renderer/accessibility/aom_content_ax_tree.cc @@ -7,6 +7,7 @@ #include <string> #include "content/common/ax_content_node_data.h" +#include "content/common/ax_content_tree_update.h" #include "content/renderer/accessibility/render_accessibility_impl.h" #include "third_party/blink/public/web/web_ax_enums.h" #include "ui/accessibility/ax_enum_util.h" diff --git a/chromium/content/renderer/accessibility/ax_image_annotator.cc b/chromium/content/renderer/accessibility/ax_image_annotator.cc index 3a38e7be07f..ffff4842975 100644 --- a/chromium/content/renderer/accessibility/ax_image_annotator.cc +++ b/chromium/content/renderer/accessibility/ax_image_annotator.cc @@ -9,10 +9,13 @@ #include <vector> #include "base/base64.h" +#include "base/i18n/char_iterator.h" #include "base/stl_util.h" #include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" #include "base/strings/utf_string_conversions.h" #include "content/public/common/content_client.h" +#include "content/renderer/accessibility/ax_image_stopwords.h" #include "content/renderer/render_frame_impl.h" #include "crypto/sha2.h" #include "third_party/blink/public/strings/grit/blink_strings.h" @@ -21,6 +24,7 @@ #include "third_party/blink/public/web/web_element.h" #include "third_party/blink/public/web/web_local_frame.h" #include "third_party/blink/public/web/web_node.h" +#include "ui/accessibility/accessibility_features.h" #include "ui/gfx/geometry/size.h" #include "url/gurl.h" @@ -114,6 +118,34 @@ void AXImageAnnotator::OnImageRemoved(blink::WebAXObject& image) { image_annotations_.erase(lookup); } +// static +bool AXImageAnnotator::ImageNameHasMostlyStopwords( + const std::string& image_name) { + // Split the image name into words by splitting on all whitespace and + // punctuation. Reject any words that are classified as stopwords. + // If there are 3 or fewer unicode codepoints remaining, classify + // the string as "mostly stopwords". + // + // More details and analysis in this (Google-internal) design doc: + // http://goto.google.com/augment-existing-image-descriptions + const char* separators = "0123456789`~!@#$%^&*()[]{}\\|;:'\",.<>?/-_=+ "; + std::vector<std::string> words = base::SplitString( + image_name, separators, base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); + int remaining_codepoints = 0; + for (const std::string& word : words) { + if (AXImageStopwords::GetInstance().IsImageStopword(word.c_str())) + continue; + + base::i18n::UTF8CharIterator iter(&word); + while (!iter.end()) { + remaining_codepoints++; + iter.Advance(); + } + } + + return (remaining_codepoints <= 3); +} + #if defined(CONTENT_IMPLEMENTATION) ContentClient* AXImageAnnotator::GetContentClient() const { return content::GetContentClient(); diff --git a/chromium/content/renderer/accessibility/ax_image_annotator.h b/chromium/content/renderer/accessibility/ax_image_annotator.h index fcc1a316106..88a3069d280 100644 --- a/chromium/content/renderer/accessibility/ax_image_annotator.h +++ b/chromium/content/renderer/accessibility/ax_image_annotator.h @@ -55,6 +55,8 @@ class CONTENT_EXPORT AXImageAnnotator : public base::CheckedObserver { void OnImageUpdated(blink::WebAXObject& image); void OnImageRemoved(blink::WebAXObject& image); + static bool ImageNameHasMostlyStopwords(const std::string& image_name); + private: // Keeps track of the image data and the automatic annotation for each image. class ImageInfo final { diff --git a/chromium/content/renderer/accessibility/ax_image_stopwords.cc b/chromium/content/renderer/accessibility/ax_image_stopwords.cc new file mode 100644 index 00000000000..d563264bda1 --- /dev/null +++ b/chromium/content/renderer/accessibility/ax_image_stopwords.cc @@ -0,0 +1,575 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <vector> + +#include "base/containers/flat_set.h" +#include "base/i18n/case_conversion.h" +#include "base/i18n/char_iterator.h" +#include "base/no_destructor.h" +#include "base/stl_util.h" +#include "base/strings/string16.h" +#include "base/strings/string_piece.h" +#include "base/strings/string_split.h" +#include "base/strings/utf_string_conversions.h" +#include "content/renderer/accessibility/ax_image_stopwords.h" +#include "third_party/icu/source/common/unicode/uchar.h" + +namespace content { + +namespace { + +// List of image stopwords for all languages. See ax_image_stopwords.h +// for information about how image stopwords are defined and how they're +// used. +// +// The stopwords are encoded here as a single long string delimited by +// newlines. This is much more efficient than an array of strings, which +// in practice takes ~6x more storage in the resulting binary. +// +// Current size as of June 2020: +// 369 unique words +// 2542 bytes uncompressed +// 1127 bytes gzipped +const char kImageStopwordsUtf8[] = { + // + // Language-independent stopwords. + // + + "com\n" + "edu\n" + "http\n" + "https\n" + "www\n" + + // + // English. + // + + // General English stopwords. + "and\n" + "are\n" + "for\n" + "from\n" + "how\n" + "online\n" + "our\n" + "the\n" + "this\n" + "with\n" + "you\n" + "your\n" + + // English image-specific stopwords. + "art\n" + "avatar\n" + "background\n" + "backgrounds\n" + "black\n" + "download\n" + "drawing\n" + "drawings\n" + "free\n" + "gif\n" + "icon\n" + "icons\n" + "illustration\n" + "illustrations\n" + "image\n" + "images\n" + "jpeg\n" + "jpg\n" + "logo\n" + "logos\n" + "meme\n" + "memes\n" + "photo\n" + "photos\n" + "picture\n" + "pictures\n" + "png\n" + "stock\n" + "transparent\n" + "vector\n" + "vectors\n" + "video\n" + "videos\n" + "wallpaper\n" + "wallpapers\n" + "white\n" + + // Alt text from may images starts with "Image may contain". + "may\n" + "contain\n" + + // Many images on the web have the alt text "Highlights info row." + "highlights\n" + "info\n" + "row\n" + + // Google Photos images are labeled as "portrait" or "landscape". + "portrait\n" + "landscape\n" + + // Reddit says "post image". + "post\n" + + // Months and month abbreviations. Often used as part of the date/time + // when a photograph was taken. + "january\n" + "jan\n" + "february\n" + "feb\n" + "march\n" + "mar\n" + "april\n" + "apr\n" + "may\n" + "june\n" + "jun\n" + "july\n" + "jul\n" + "august\n" + "aug\n" + "september\n" + "sep\n" + "october\n" + "oct" + "november" + "nov\n" + "december\n" + "dec\n" + + // Days of the week. + "monday\n" + "mon\n" + "tuesday\n" + "tue\n" + "wednesday\n" + "wed\n" + "thursday\n" + "thu\n" + "friday\n" + "fri\n" + "saturday\n" + "sat\n" + "sunday\n" + "sun\n" + + // + // French + // + + // General French stopwords. + "les\n" // the + "pour\n" // for + "des\n" // of the + "sur\n" // on + "avec\n" // with + "une\n" // one + "dans\n" // in + "est\n" // is + "plus\n" // more + "par\n" // by + "vous\n" // you + "pas\n" // not + "qui\n" // who + "aux\n" // to the + "son\n" // his/her/its + "nous\n" // we + "voir\n" // see + + // French Image stopwords. + "noir\n" // black + "blanc\n" // white + "dessin\n" // drawing + "font\n" // background + "peinture\n" // painting + + // Months. + "janvier\n" + "janv\n" + "février\n" + "févr\n" + "mars\n" + "avril\n" + "mai\n" + "juin\n" + "juillet\n" + "juil\n" + "août\n" + "septembre\n" + "sept\n" + "octobre\n" + "oct\n" + "novembre\n" + "nov\n" + "décembre\n" + "déc\n" + + // Days of the week. + "lundi\n" + "lun\n" + "mardi\n" + "mar\n" + "mercredi\n" + "mer\n" + "jeudi\n" + "jeu\n" + "vendredi\n" + "ven\n" + "samedi\n" + "sam\n" + "dimanche\n" + "dim\n" + + // + // Italian + // + + "con\n" // with + "per\n" // through, by + "non\n" // not + "come\n" // as + "più\n" // more + "dal\n" // da + il + "dallo\n" // da + lo + "dai\n" // da + i + "dagli\n" // da + gli + "dall\n" // da + l' + "dagl\n" // da + gll' + "dalla\n" // da + la + "dalle\n" // da + le + "del\n" // di + il + "dello\n" // di + lo + "dei\n" // di + i + "degli\n" // di + gli + "dell\n" // di + l' + "degl\n" // di + gl' + "della\n" // di + la + "delle\n" // di + le + "nel\n" // in + el + "nello\n" // in + lo + "nei\n" // in + i + "negli\n" // in + gli + "nell\n" // in + l' + "negl\n" // in + gl' + "nella\n" // in + la + "nelle\n" // in + le + "sul\n" // su + il + "sullo\n" // su + lo + "sui\n" // su + i + "sugli\n" // su + gli + "sull\n" // su + l' + "sugl\n" // su + gl' + "sulla\n" // su + la + "sulle\n" // su + le + + // Images + "arte\n" + "immagini\n" + "illustrazione\n" + "fotografia\n" + "icona\n" + + "bianca\n" // white + "bianco\n" // white + "nera\n" // black + "nero\n" // black + "contenere\n" // contain (image may contain...) + + // Months. + "gennaio\n" + "genn\n" + "febbraio\n" + "febbr\n" + "marzo\n" + "mar\n" + "aprile\n" + "apr\n" + "maggio\n" + "magg\n" + "giugno\n" + "luglio\n" + "agosto\n" + "settembre\n" + "sett\n" + "ottobre\n" + "ott\n" + "novembre\n" + "nov\n" + "dicembre\n" + "dic\n" + + // Weekdays. + "lunedì\n" + "lun\n" + "martedì\n" + "mar\n" + "mercoledì\n" + "mer\n" + "giovedì\n" + "gio\n" + "venerdì\n" + "ven\n" + "sabato\n" + "sab\n" + "domenica\n" + "dom\n" + + // + // German + // + + // General German stopwords. + "und\n" // and + "mit\n" // with + "für\n" // for + "der\n" // the + "die\n" // the + "von\n" // of, from + "auf\n" // on + "das\n" // the + "aus\n" // out of + "ist\n" // is + "ein\n" // one + "eine\n" // one + "sie\n" // they, she + "den\n" // the + "zum\n" // zu + dem + "zur\n" // zu + der + "bei\n" // by + "des\n" // the + "sprüche\n" // claims (to be) + "oder\n" // or + + // German Image stopwords. + "bild\n" // picture + "bilder\n" // pictures + "foto\n" // photo + "schwarz\n" // black + "weiß\n" // white + + // Months. + "januar\n" + "jan\n" + "jän\n" + "februar\n" + "feb\n" + "märz\n" + "april\n" + "apr\n" + "mai\n" + "juni\n" + "juli\n" + "august\n" + "aug\n" + "september\n" + "sept\n" + "oktober\n" + "okt\n" + "november\n" + "nov\n" + "dezember\n" + "dez\n" + + // Weekdays. + "montag\n" + "dienstag\n" + "mittwoch\n" + "donnerstag\n" + "freitag\n" + "samstag\n" + "sonntag\n" + + // + // Spanish + // + + // General Spanish stopwords. + "con\n" // with + "para\n" // by + "del\n" // of the + "que\n" // that + "los\n" // the + "las\n" // the + "una\n" // one + "por\n" // for + "más\n" // more + "como\n" // how + + // Spanish image stopwords. + "dibujos\n" // drawings + "imagen\n" // images + "arte\n" // art + "fondo\n" // background + "fondos\n" // backgrounds + "diseño\n" // design + "ilustración\n" // illustration + "imagenes\n" // images + "blanca\n" // white + "blanco\n" // white + "negra\n" // black + "negro\n" // black + + // Months. + "enero\n" + "febrero\n" + "feb\n" + "marzo\n" + "abril\n" + "abr\n" + "mayo\n" + "junio\n" + "jun\n" + "julio\n" + "jul\n" + "agosto\n" + "septiembre\n" + "sept\n" + "set\n" + "octubre\n" + "oct\n" + "noviembre\n" + "nov\n" + "diciembre\n" + "dic\n" + + // Weekdays. Weekday abbreviations in Spanish are two-letters which + // don't need to be listed here (anything less than 3 letters is + // considered a stopword already). + "lunes\n" + "martes\n" + "miércoles\n" + "jueves\n" + "viernes\n" + "sábado\n" + "domingo\n" + + // + // Hindi + // + + // General Hindi stopwords. + "में\n" // in + "लिए\n" // for + "नहीं\n" // no + "एक\n" // one + "साथ\n" // with + "दिया\n" // gave + "किया\n" // did + "रहे\n" // are + "सकता\n" // can + "इस\n" // this + "शामिल\n" // include + "तारीख\n" // the date + + // Hindi image stopwords. + "चित्र\n" // picture + "वीडियो\n" // video + + // Months + "जनवरी\n" + "फरवरी\n" + "मार्च\n" + "अप्रैल\n" + "मई\n" + "जून\n" + "जुलाई\n" + "अगस्त\n" + "सितंबर\n" + "अक्टूबर\n" + "नवंबर\n" + "दिसंबर\n" + + // Weekdays. + "सोमवार\n" + "मंगलवार\n" + "बुधवार\n" + "बृहस्पतिवार\n" + "शुक्रवार\n" + "शनिवार\n" + "रविवार\n"}; + +} // namespace + +// static +AXImageStopwords& AXImageStopwords::GetInstance() { + static base::NoDestructor<AXImageStopwords> instance; + return *instance; +} + +AXImageStopwords::AXImageStopwords() { + // Parse the newline-delimited stopwords from kImageStopwordsUtf8 and store + // them as a flat_set of type StringPiece. This is very memory-efficient + // because it avoids ever needing to copy any of the strings; each StringPiece + // is just a pointer into kImageStopwordsUtf8 and flat_set acts like a set but + // basically just does a binary search. + std::vector<base::StringPiece> stopwords = + base::SplitStringPiece(kImageStopwordsUtf8, "\n", base::TRIM_WHITESPACE, + base::SPLIT_WANT_NONEMPTY); + + // It's inefficient to add things to a flat_set one at a time. Copy them + // all over at once. + stopword_set_ = stopwords; +} + +AXImageStopwords::~AXImageStopwords() = default; + +bool AXImageStopwords::IsImageStopword(const char* word_utf8) const { + base::string16 word_utf16 = base::UTF8ToUTF16(word_utf8); + + // It's not really meaningful, but since short words are stopwords, for + // simplicity we define the empty string to be a stopword too. + if (word_utf16.empty()) + return true; + + // Canonicalize case, this is like "ToLower" for many languages but + // works independently of the current locale. + word_utf16 = base::i18n::FoldCase(word_utf16); + + // Count the number of distinct codepoints from a supported unicode block. + base::i18n::UTF16CharIterator iter(&word_utf16); + int supported_count = 0; + int total_count = 0; + while (!iter.end()) { + total_count++; + int32_t codepoint = iter.get(); + UBlockCode block_code = ublock_getCode(codepoint); + switch (block_code) { + case UBLOCK_BASIC_LATIN: + case UBLOCK_LATIN_1_SUPPLEMENT: + case UBLOCK_LATIN_EXTENDED_A: + case UBLOCK_DEVANAGARI: + supported_count++; + break; + default: + break; + } + iter.Advance(); + } + + // Treat any string of 2 or fewer characters in any of these unicode + // blocks as a stopword. Of course, there are rare exceptions, but they're + // acceptable for this heuristic. All that means is that we won't count + // those words when trying to determine if the alt text for an image + // has meaningful text or not. The odds are good that a 1 or 2 character + // word is not meaningful. + // + // Note: this assumption might not be valid for some unicode blocks, + // like Chinese. That's why the heuristic only applies to certain unicode + // blocks where we believe this to be a reasonable assumption. + // + // Note that in Devanagari (the script used for the Hindi language, among + // others) a word sometimes looks like a single character (one glyph) but it's + // actually two or more unicode codepoints (consonants and vowels that are + // joined together), which is why this heuristic still works. Anything with + // two or fewer unicode codepoints is an extremely short word. + if (supported_count == total_count && total_count <= 2) + return true; + + return base::Contains(stopword_set_, base::UTF16ToUTF8(word_utf16)); +} + +} // namespace content diff --git a/chromium/content/renderer/accessibility/ax_image_stopwords.h b/chromium/content/renderer/accessibility/ax_image_stopwords.h new file mode 100644 index 00000000000..2a6239867d1 --- /dev/null +++ b/chromium/content/renderer/accessibility/ax_image_stopwords.h @@ -0,0 +1,71 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_RENDERER_ACCESSIBILITY_AX_IMAGE_STOPWORDS_H_ +#define CONTENT_RENDERER_ACCESSIBILITY_AX_IMAGE_STOPWORDS_H_ + +#include "base/containers/flat_set.h" +#include "base/no_destructor.h" +#include "base/stl_util.h" +#include "base/strings/string_piece.h" +#include "content/common/content_export.h" + +namespace content { + +// Maintains a set of image stopwords and provides a function to check +// whether or not a given word is an image stopword. +// +// A stopword in general is a word that's filtered out before doing +// natural language processing. In English, common stopwords include +// "the" or "of" - they are words that are part of grammatically correct +// sentences but don't add any useful semantics themselves. +// +// This set is used as part of an algorithm to determine whether the +// accessible label for an image (including the "alt" attribute and +// other attributes) contains a useful description or not. For this +// application, both common stopwords like "the", but also image-related +// words like "image" and "photo" are included, because an image that's +// just labeled with the word "photo" is essentially unlabeled. +// +// Stopwords from all supported languages are grouped together, because +// it's simpler to just have one set rather than to try to split by the +// element language (which is sometimes wrong). This leads to a small +// but acceptable number of false positives if a stopword in one language +// is a meaningful word in another language. +// +// The set of supported languages should include all of the languages +// that we can generate automatic image descriptions for. This will grow +// over time. +// +// Words consisting of just one or two characters made up of letters from +// Latin alphabets are always considered stopwords, but that doesn't +// generalize to all languages / character sets. +// +// The set of stopwords was obtained by extracting the alt text of images +// from billions of web pages, tokenizing, counting, and then manually +// categorizing the top words, with the help of dictionaries and language +// experts. More details in this (Google-internal) design doc: +// http://goto.google.com/augment-existing-image-descriptions +class CONTENT_EXPORT AXImageStopwords { + public: + static AXImageStopwords& GetInstance(); + + // The input should be a word, after already splitting by punctuation and + // whitespace. Returns true if the word is an image stopword. + // Case-insensitive and language-neutral (includes words from all + // languages). + bool IsImageStopword(const char* utf8_string) const; + + private: + friend base::NoDestructor<AXImageStopwords>; + + AXImageStopwords(); + ~AXImageStopwords(); + + base::flat_set<base::StringPiece> stopword_set_; +}; + +} // namespace content + +#endif // CONTENT_RENDERER_ACCESSIBILITY_AX_IMAGE_ANNOTATOR_H_ diff --git a/chromium/content/renderer/accessibility/ax_image_stopwords_unittest.cc b/chromium/content/renderer/accessibility/ax_image_stopwords_unittest.cc new file mode 100644 index 00000000000..21d6d938df8 --- /dev/null +++ b/chromium/content/renderer/accessibility/ax_image_stopwords_unittest.cc @@ -0,0 +1,116 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/renderer/accessibility/ax_image_stopwords.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { + +TEST(AXImageStopwordsTest, EmptyStringIsAStopword) { + AXImageStopwords& stopwords = AXImageStopwords::GetInstance(); + EXPECT_TRUE(stopwords.IsImageStopword("")); +} + +TEST(AXImageStopwordsTest, English) { + AXImageStopwords& stopwords = AXImageStopwords::GetInstance(); + EXPECT_TRUE(stopwords.IsImageStopword("the")); + EXPECT_TRUE(stopwords.IsImageStopword("http")); + EXPECT_TRUE(stopwords.IsImageStopword("for")); + EXPECT_TRUE(stopwords.IsImageStopword("with")); + EXPECT_TRUE(stopwords.IsImageStopword("background")); + EXPECT_TRUE(stopwords.IsImageStopword("sunday")); + EXPECT_TRUE(stopwords.IsImageStopword("november")); + EXPECT_FALSE(stopwords.IsImageStopword("cat")); + EXPECT_FALSE(stopwords.IsImageStopword("obama")); + EXPECT_FALSE(stopwords.IsImageStopword("heart")); + EXPECT_FALSE(stopwords.IsImageStopword("home")); +} + +TEST(AXImageStopwordsTest, EnglishCaseInsensitive) { + AXImageStopwords& stopwords = AXImageStopwords::GetInstance(); + EXPECT_TRUE(stopwords.IsImageStopword("the")); + EXPECT_TRUE(stopwords.IsImageStopword("THE")); + EXPECT_TRUE(stopwords.IsImageStopword("tHe")); +} + +TEST(AXImageStopwordsTest, EnglishAllShortWordsAreStopwords) { + AXImageStopwords& stopwords = AXImageStopwords::GetInstance(); + // One and two-letter words are always stopwords no matter what + // characters, as long as they're Latin characters. + EXPECT_TRUE(stopwords.IsImageStopword("q")); + EXPECT_TRUE(stopwords.IsImageStopword("I")); + EXPECT_TRUE(stopwords.IsImageStopword("ff")); + EXPECT_TRUE(stopwords.IsImageStopword("zU")); + + // Three-letter words aren't stopwords unless they're in our set. + EXPECT_FALSE(stopwords.IsImageStopword("fff")); + EXPECT_FALSE(stopwords.IsImageStopword("GGG")); +} + +TEST(AXImageStopwordsTest, French) { + AXImageStopwords& stopwords = AXImageStopwords::GetInstance(); + EXPECT_TRUE(stopwords.IsImageStopword("les")); + EXPECT_TRUE(stopwords.IsImageStopword("BLANC")); + EXPECT_TRUE(stopwords.IsImageStopword("FÉVR")); + EXPECT_FALSE(stopwords.IsImageStopword("modèle")); + EXPECT_FALSE(stopwords.IsImageStopword("recettes")); + EXPECT_FALSE(stopwords.IsImageStopword("ciel")); +} + +TEST(AXImageStopwordsTest, Italian) { + AXImageStopwords& stopwords = AXImageStopwords::GetInstance(); + EXPECT_TRUE(stopwords.IsImageStopword("con")); + EXPECT_TRUE(stopwords.IsImageStopword("PIÙ")); + EXPECT_TRUE(stopwords.IsImageStopword("Immagini")); + EXPECT_FALSE(stopwords.IsImageStopword("montagna")); + EXPECT_FALSE(stopwords.IsImageStopword("pubblico")); + EXPECT_FALSE(stopwords.IsImageStopword("Cultura")); +} + +TEST(AXImageStopwordsTest, German) { + AXImageStopwords& stopwords = AXImageStopwords::GetInstance(); + EXPECT_TRUE(stopwords.IsImageStopword("und")); + EXPECT_TRUE(stopwords.IsImageStopword("FÜR")); + EXPECT_TRUE(stopwords.IsImageStopword("sprüche")); + EXPECT_FALSE(stopwords.IsImageStopword("haus")); + EXPECT_FALSE(stopwords.IsImageStopword("gesichter")); + EXPECT_FALSE(stopwords.IsImageStopword("Deutsche")); +} + +TEST(AXImageStopwordsTest, Spanish) { + AXImageStopwords& stopwords = AXImageStopwords::GetInstance(); + EXPECT_TRUE(stopwords.IsImageStopword("con")); + EXPECT_TRUE(stopwords.IsImageStopword("MÁS")); + EXPECT_TRUE(stopwords.IsImageStopword("enero")); + EXPECT_FALSE(stopwords.IsImageStopword("tortuga")); + EXPECT_FALSE(stopwords.IsImageStopword("flores")); + EXPECT_FALSE(stopwords.IsImageStopword("actividades")); +} + +TEST(AXImageStopwordsTest, Hindi) { + AXImageStopwords& stopwords = AXImageStopwords::GetInstance(); + EXPECT_TRUE(stopwords.IsImageStopword("रहे")); + EXPECT_TRUE(stopwords.IsImageStopword("चित्र")); + EXPECT_TRUE(stopwords.IsImageStopword("वीडियो")); + EXPECT_FALSE(stopwords.IsImageStopword("जानिए")); + EXPECT_FALSE(stopwords.IsImageStopword("भारतीय")); +} + +TEST(AXImageStopwordsTest, HindiShortWordsAreStopwords) { + AXImageStopwords& stopwords = AXImageStopwords::GetInstance(); + // All Hindi words with two or fewer unicode codepoints are stopwords. + // Note that these appear to have just one glyph, but they're encoded + // as two codepoints, a consonant and a vowel. + EXPECT_TRUE(stopwords.IsImageStopword("की")); // "of" + EXPECT_TRUE(stopwords.IsImageStopword("में")); // "in" + + // Some two-character Hindi words are stopwords, they're in our set. + EXPECT_TRUE(stopwords.IsImageStopword("एक")); // "one" + + // Other two-character Hindi words are not stopwords. + EXPECT_FALSE(stopwords.IsImageStopword("सदा")); // "always" + EXPECT_FALSE(stopwords.IsImageStopword("खाना")); // "food" +} + +} // namespace content diff --git a/chromium/content/renderer/accessibility/blink_ax_tree_source.cc b/chromium/content/renderer/accessibility/blink_ax_tree_source.cc index 370c3edcba2..030d6487db7 100644 --- a/chromium/content/renderer/accessibility/blink_ax_tree_source.cc +++ b/chromium/content/renderer/accessibility/blink_ax_tree_source.cc @@ -39,6 +39,7 @@ #include "third_party/blink/public/web/web_plugin.h" #include "third_party/blink/public/web/web_plugin_container.h" #include "third_party/blink/public/web/web_view.h" +#include "ui/accessibility/accessibility_features.h" #include "ui/accessibility/accessibility_switches.h" #include "ui/accessibility/ax_enum_util.h" #include "ui/accessibility/ax_role_properties.h" @@ -228,19 +229,20 @@ bool SearchForExactlyOneInnerImage(WebAXObject obj, if (!inner_image->IsDetached()) return false; *inner_image = obj; + } else { + // If we found something else with a name, fail. + if (!ui::IsDocument(obj.Role()) && !ui::IsLink(obj.Role())) { + blink::WebString web_name = obj.GetName(); + if (!base::ContainsOnlyChars(web_name.Utf8(), base::kWhitespaceASCII)) { + return false; + } + } } // Fail if we recursed to |max_depth| and there's more of a subtree. if (max_depth == 0 && obj.ChildCount()) return false; - // If we found something else with a name, fail. - if (obj.Role() != ax::mojom::Role::kRootWebArea) { - blink::WebString web_name = obj.GetName(); - if (!base::ContainsOnlyChars(web_name.Utf8(), base::kWhitespaceASCII)) - return false; - } - // Recurse. for (unsigned int i = 0; i < obj.ChildCount(); i++) { if (!SearchForExactlyOneInnerImage(obj.ChildAt(i), inner_image, @@ -251,12 +253,12 @@ bool SearchForExactlyOneInnerImage(WebAXObject obj, return !inner_image->IsDetached(); } -// Return true if the subtree of |obj|, to a max depth of 2, contains +// Return true if the subtree of |obj|, to a max depth of 3, contains // exactly one image. Return that image in |inner_image|. -bool FindExactlyOneInnerImageInMaxDepthTwo(WebAXObject obj, - WebAXObject* inner_image) { +bool FindExactlyOneInnerImageInMaxDepthThree(WebAXObject obj, + WebAXObject* inner_image) { DCHECK(inner_image); - return SearchForExactlyOneInnerImage(obj, inner_image, /* max_depth = */ 2); + return SearchForExactlyOneInnerImage(obj, inner_image, /* max_depth = */ 3); } std::string GetEquivalentAriaRoleString(const ax::mojom::Role role) { @@ -410,10 +412,6 @@ void BlinkAXTreeSource::SetLoadInlineTextBoxesForId(int32_t id) { load_inline_text_boxes_ids_.insert(id); } -void BlinkAXTreeSource::EnableDOMNodeIDs() { - enable_dom_node_ids_ = true; -} - bool BlinkAXTreeSource::GetTreeData(AXContentTreeData* tree_data) const { CHECK(frozen_); tree_data->doctype = "html"; @@ -574,6 +572,94 @@ void BlinkAXTreeSource::SerializeNode(WebAXObject src, TRACE_EVENT1("accessibility", "BlinkAXTreeSource::SerializeNode", "role", ui::ToString(dst->role)); + SerializeNameAndDescriptionAttributes(src, dst); + + if (accessibility_mode_.has_mode(ui::AXMode::kScreenReader) || + accessibility_mode_.has_mode(ui::AXMode::kPDF)) { + // Heading level. + if (ui::IsHeading(dst->role) && src.HeadingLevel()) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel, + src.HeadingLevel()); + } + + SerializeListAttributes(src, dst); + SerializeTableAttributes(src, dst); + } + + if (accessibility_mode_.has_mode(ui::AXMode::kPDF)) { + SerializePDFAttributes(src, dst); + // Return early. None of the following attributes are needed for PDFs. + return; + } + + SerializeBoundingBoxAttributes(src, dst); + + SerializeSparseAttributes(src, dst); + SerializeValueAttributes(src, dst); + SerializeStateAttributes(src, dst); + SerializeChooserPopupAttributes(src, dst); + + if (accessibility_mode_.has_mode(ui::AXMode::kScreenReader)) { + SerializeStyleAttributes(src, dst); + SerializeMarkerAttributes(src, dst); + if (src.IsInLiveRegion()) + SerializeLiveRegionAttributes(src, dst); + SerializeOtherScreenReaderAttributes(src, dst); + } + + WebNode node = src.GetNode(); + bool is_iframe = false; + if (!node.IsNull() && node.IsElementNode()) { + WebElement element = node.To<WebElement>(); + is_iframe = element.HasHTMLTagName("iframe"); + + SerializeElementAttributes(src, element, dst); + if (accessibility_mode_.has_mode(ui::AXMode::kHTML)) { + SerializeHTMLAttributes(src, element, dst); + } + + if (src.IsEditable()) { + SerializeEditableTextAttributes(src, dst); + } + + // Presence of other ARIA attributes. + if (src.HasAriaAttribute()) + dst->AddBoolAttribute(ax::mojom::BoolAttribute::kHasAriaAttribute, true); + } + + // Add the ids of *indirect* children - those who are children of this node, + // but whose parent is *not* this node. One example is a table + // cell, which is a child of both a row and a column. Because the cell's + // parent is the row, the row adds it as a child, and the column adds it + // as an indirect child. + int child_count = src.ChildCount(); + std::vector<int32_t> indirect_child_ids; + for (int i = 0; i < child_count; ++i) { + WebAXObject child = src.ChildAt(i); + if (!is_iframe && !child.IsDetached() && !IsParentUnignoredOf(src, child)) + indirect_child_ids.push_back(child.AxID()); + } + if (indirect_child_ids.size() > 0) { + dst->AddIntListAttribute(ax::mojom::IntListAttribute::kIndirectChildIds, + indirect_child_ids); + } + + if (src.IsScrollableContainer()) { + SerializeScrollAttributes(src, dst); + } + + if (dst->id == image_data_node_id_) { + // In general, string attributes should be truncated using + // TruncateAndAddStringAttribute, but ImageDataUrl contains a data url + // representing an image, so add it directly using AddStringAttribute. + dst->AddStringAttribute(ax::mojom::StringAttribute::kImageDataUrl, + src.ImageDataUrl(max_image_data_size_).Utf8()); + } +} + +void BlinkAXTreeSource::SerializeBoundingBoxAttributes( + WebAXObject src, + AXContentNodeData* dst) const { WebAXObject offset_container; WebFloatRect bounds_in_container; SkMatrix44 container_transform; @@ -604,30 +690,29 @@ void BlinkAXTreeSource::SerializeNode(WebAXObject src, dst->AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject, true); } +} - if (enable_dom_node_ids_) { - // The DOMNodeID from Blink. Currently only populated when using - // the accessibility tree for PDF exporting. Warning, this is totally - // unrelated to the accessibility node ID, or the ID attribute for an - // HTML element - it's an ID used to uniquely identify nodes in Blink. - int dom_node_id = src.GetDOMNodeId(); - if (dom_node_id) - dst->AddIntAttribute(ax::mojom::IntAttribute::kDOMNodeId, dom_node_id); - } +void BlinkAXTreeSource::SerializePDFAttributes(WebAXObject src, + AXContentNodeData* dst) const { + // The DOMNodeID from Blink. Currently only populated when using + // the accessibility tree for PDF exporting. Warning, this is totally + // unrelated to the accessibility node ID, or the ID attribute for an + // HTML element - it's an ID used to uniquely identify nodes in Blink. + int dom_node_id = src.GetDOMNodeId(); + if (dom_node_id) + dst->AddIntAttribute(ax::mojom::IntAttribute::kDOMNodeId, dom_node_id); +} +void BlinkAXTreeSource::SerializeSparseAttributes( + WebAXObject src, + AXContentNodeData* dst) const { AXContentNodeDataSparseAttributeAdapter sparse_attribute_adapter(dst); src.GetSparseAXAttributes(sparse_attribute_adapter); +} - WebAXObject chooser_popup = src.ChooserPopup(); - if (!chooser_popup.IsNull()) { - int32_t chooser_popup_id = chooser_popup.AxID(); - auto controls_ids = - dst->GetIntListAttribute(ax::mojom::IntListAttribute::kControlsIds); - controls_ids.push_back(chooser_popup_id); - dst->AddIntListAttribute(ax::mojom::IntListAttribute::kControlsIds, - controls_ids); - } - +void BlinkAXTreeSource::SerializeNameAndDescriptionAttributes( + WebAXObject src, + AXContentNodeData* dst) const { ax::mojom::NameFrom name_from; blink::WebVector<WebAXObject> name_objects; blink::WebString web_name = src.GetName(name_from, name_objects); @@ -661,6 +746,17 @@ void BlinkAXTreeSource::SerializeNode(WebAXObject src, web_title.Utf8()); } + if (accessibility_mode_.has_mode(ui::AXMode::kScreenReader)) { + blink::WebString web_placeholder = src.Placeholder(name_from); + if (!web_placeholder.IsEmpty()) + TruncateAndAddStringAttribute(dst, + ax::mojom::StringAttribute::kPlaceholder, + web_placeholder.Utf8()); + } +} + +void BlinkAXTreeSource::SerializeValueAttributes(WebAXObject src, + AXContentNodeData* dst) const { if (src.ValueDescription().length()) { TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kValue, src.ValueDescription().Utf8()); @@ -668,7 +764,10 @@ void BlinkAXTreeSource::SerializeNode(WebAXObject src, TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kValue, src.StringValue().Utf8()); } +} +void BlinkAXTreeSource::SerializeStateAttributes(WebAXObject src, + AXContentNodeData* dst) const { switch (src.Restriction()) { case blink::kWebAXRestrictionReadOnly: dst->SetRestriction(ax::mojom::Restriction::kReadOnly); @@ -685,529 +784,517 @@ void BlinkAXTreeSource::SerializeNode(WebAXObject src, if (!src.Url().IsEmpty()) TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kUrl, src.Url().GetString().Utf8()); +} - // The following set of attributes are only accessed when the accessibility - // mode is set to screen reader mode, otherwise only the more basic - // attributes are populated. - if (accessibility_mode_.has_mode(ui::AXMode::kScreenReader)) { - blink::WebString web_placeholder = src.Placeholder(name_from); - if (!web_placeholder.IsEmpty()) - TruncateAndAddStringAttribute(dst, - ax::mojom::StringAttribute::kPlaceholder, - web_placeholder.Utf8()); +void BlinkAXTreeSource::SerializeStyleAttributes(WebAXObject src, + AXContentNodeData* dst) const { + // Text attributes. + if (src.BackgroundColor()) + dst->AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + src.BackgroundColor()); - if (dst->role == ax::mojom::Role::kColorWell) - dst->AddIntAttribute(ax::mojom::IntAttribute::kColorValue, - src.ColorValue()); + if (src.GetColor()) + dst->AddIntAttribute(ax::mojom::IntAttribute::kColor, src.GetColor()); - if (dst->role == ax::mojom::Role::kLink) { - WebAXObject target = src.InPageLinkTarget(); - if (!target.IsNull()) { - int32_t target_id = target.AxID(); - dst->AddIntAttribute(ax::mojom::IntAttribute::kInPageLinkTargetId, - target_id); - } - } + WebAXObject parent = ParentObjectUnignored(src); + if (src.FontFamily().length()) { + if (parent.IsNull() || parent.FontFamily() != src.FontFamily()) + TruncateAndAddStringAttribute(dst, + ax::mojom::StringAttribute::kFontFamily, + src.FontFamily().Utf8()); + } - if (dst->role == ax::mojom::Role::kRadioButton) { - AddIntListAttributeFromWebObjects( - ax::mojom::IntListAttribute::kRadioGroupIds, - src.RadioButtonsInGroup(), dst); - } + // Font size is in pixels. + if (src.FontSize()) + dst->AddFloatAttribute(ax::mojom::FloatAttribute::kFontSize, + src.FontSize()); - // Text attributes. - if (src.BackgroundColor()) - dst->AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, - src.BackgroundColor()); + if (src.FontWeight()) { + dst->AddFloatAttribute(ax::mojom::FloatAttribute::kFontWeight, + src.FontWeight()); + } - if (src.GetColor()) - dst->AddIntAttribute(ax::mojom::IntAttribute::kColor, src.GetColor()); + if (dst->role == ax::mojom::Role::kListItem && + src.GetListStyle() != ax::mojom::ListStyle::kNone) { + dst->SetListStyle(src.GetListStyle()); + } - WebAXObject parent = ParentObjectUnignored(src); - if (src.FontFamily().length()) { - if (parent.IsNull() || parent.FontFamily() != src.FontFamily()) - TruncateAndAddStringAttribute(dst, - ax::mojom::StringAttribute::kFontFamily, - src.FontFamily().Utf8()); - } + if (src.GetTextDirection() != ax::mojom::TextDirection::kNone) { + dst->SetTextDirection(src.GetTextDirection()); + } - // Font size is in pixels. - if (src.FontSize()) - dst->AddFloatAttribute(ax::mojom::FloatAttribute::kFontSize, - src.FontSize()); + if (src.GetTextPosition() != ax::mojom::TextPosition::kNone) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kTextPosition, + static_cast<int32_t>(src.GetTextPosition())); + } - if (src.FontWeight()) { - dst->AddFloatAttribute(ax::mojom::FloatAttribute::kFontWeight, - src.FontWeight()); - } + int32_t text_style = 0; + ax::mojom::TextDecorationStyle text_overline_style; + ax::mojom::TextDecorationStyle text_strikethrough_style; + ax::mojom::TextDecorationStyle text_underline_style; + src.GetTextStyleAndTextDecorationStyle(&text_style, &text_overline_style, + &text_strikethrough_style, + &text_underline_style); + if (text_style) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kTextStyle, text_style); + } - if (src.AriaCurrentState() != ax::mojom::AriaCurrentState::kNone) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kAriaCurrentState, - static_cast<int32_t>(src.AriaCurrentState())); - } + if (text_overline_style != ax::mojom::TextDecorationStyle::kNone) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kTextOverlineStyle, + static_cast<int32_t>(text_overline_style)); + } - if (src.InvalidState() != ax::mojom::InvalidState::kNone) - dst->SetInvalidState(src.InvalidState()); - if (src.InvalidState() == ax::mojom::InvalidState::kOther && - src.AriaInvalidValue().length()) { - TruncateAndAddStringAttribute( - dst, ax::mojom::StringAttribute::kAriaInvalidValue, - src.AriaInvalidValue().Utf8()); - } + if (text_strikethrough_style != ax::mojom::TextDecorationStyle::kNone) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kTextStrikethroughStyle, + static_cast<int32_t>(text_strikethrough_style)); + } - if (src.CheckedState() != ax::mojom::CheckedState::kNone) { - dst->SetCheckedState(src.CheckedState()); - } + if (text_underline_style != ax::mojom::TextDecorationStyle::kNone) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kTextUnderlineStyle, + static_cast<int32_t>(text_underline_style)); + } +} - if (dst->role == ax::mojom::Role::kListItem && - src.GetListStyle() != ax::mojom::ListStyle::kNone) { - dst->SetListStyle(src.GetListStyle()); - } +void BlinkAXTreeSource::SerializeInlineTextBoxAttributes( + WebAXObject src, + AXContentNodeData* dst) const { + DCHECK_EQ(ax::mojom::Role::kInlineTextBox, dst->role); + + WebVector<int> src_character_offsets; + src.CharacterOffsets(src_character_offsets); + dst->AddIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets, + src_character_offsets.ReleaseVector()); + + WebVector<int> src_word_starts; + WebVector<int> src_word_ends; + src.GetWordBoundaries(src_word_starts, src_word_ends); + dst->AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, + src_word_starts.ReleaseVector()); + dst->AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, + src_word_ends.ReleaseVector()); +} - if (src.GetTextDirection() != ax::mojom::TextDirection::kNone) { - dst->SetTextDirection(src.GetTextDirection()); +void BlinkAXTreeSource::SerializeMarkerAttributes( + WebAXObject src, + AXContentNodeData* dst) const { + // Spelling, grammar and other document markers. + WebVector<ax::mojom::MarkerType> src_marker_types; + WebVector<int> src_marker_starts; + WebVector<int> src_marker_ends; + src.Markers(src_marker_types, src_marker_starts, src_marker_ends); + DCHECK_EQ(src_marker_types.size(), src_marker_starts.size()); + DCHECK_EQ(src_marker_starts.size(), src_marker_ends.size()); + + if (src_marker_types.size()) { + std::vector<int32_t> marker_types; + std::vector<int32_t> marker_starts; + std::vector<int32_t> marker_ends; + marker_types.reserve(src_marker_types.size()); + marker_starts.reserve(src_marker_starts.size()); + marker_ends.reserve(src_marker_ends.size()); + for (size_t i = 0; i < src_marker_types.size(); ++i) { + marker_types.push_back(static_cast<int32_t>(src_marker_types[i])); + marker_starts.push_back(src_marker_starts[i]); + marker_ends.push_back(src_marker_ends[i]); } + dst->AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerTypes, + marker_types); + dst->AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts, + marker_starts); + dst->AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds, + marker_ends); + } +} - if (src.GetTextPosition() != ax::mojom::TextPosition::kNone) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kTextPosition, - static_cast<int32_t>(src.GetTextPosition())); - } +void BlinkAXTreeSource::SerializeLiveRegionAttributes( + WebAXObject src, + AXContentNodeData* dst) const { + DCHECK(src.IsInLiveRegion()); - int32_t text_style = 0; - ax::mojom::TextDecorationStyle text_overline_style; - ax::mojom::TextDecorationStyle text_strikethrough_style; - ax::mojom::TextDecorationStyle text_underline_style; - src.GetTextStyleAndTextDecorationStyle(&text_style, &text_overline_style, - &text_strikethrough_style, - &text_underline_style); - if (text_style) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kTextStyle, text_style); - } + dst->AddBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic, + src.LiveRegionAtomic()); + if (!src.LiveRegionStatus().IsEmpty()) { + TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kLiveStatus, + src.LiveRegionStatus().Utf8()); + } + TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kLiveRelevant, + src.LiveRegionRelevant().Utf8()); + // If we are not at the root of an atomic live region. + if (src.ContainerLiveRegionAtomic() && !src.LiveRegionRoot().IsDetached() && + !src.LiveRegionAtomic()) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kMemberOfId, + src.LiveRegionRoot().AxID()); + } + dst->AddBoolAttribute(ax::mojom::BoolAttribute::kContainerLiveAtomic, + src.ContainerLiveRegionAtomic()); + dst->AddBoolAttribute(ax::mojom::BoolAttribute::kContainerLiveBusy, + src.ContainerLiveRegionBusy()); + TruncateAndAddStringAttribute( + dst, ax::mojom::StringAttribute::kContainerLiveStatus, + src.ContainerLiveRegionStatus().Utf8()); + TruncateAndAddStringAttribute( + dst, ax::mojom::StringAttribute::kContainerLiveRelevant, + src.ContainerLiveRegionRelevant().Utf8()); +} - if (text_overline_style != ax::mojom::TextDecorationStyle::kNone) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kTextOverlineStyle, - static_cast<int32_t>(text_overline_style)); - } +void BlinkAXTreeSource::SerializeListAttributes(WebAXObject src, + AXContentNodeData* dst) const { + if (src.SetSize()) + dst->AddIntAttribute(ax::mojom::IntAttribute::kSetSize, src.SetSize()); - if (text_strikethrough_style != ax::mojom::TextDecorationStyle::kNone) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kTextStrikethroughStyle, - static_cast<int32_t>(text_strikethrough_style)); - } + if (src.PosInSet()) + dst->AddIntAttribute(ax::mojom::IntAttribute::kPosInSet, src.PosInSet()); +} - if (text_underline_style != ax::mojom::TextDecorationStyle::kNone) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kTextUnderlineStyle, - static_cast<int32_t>(text_underline_style)); +void BlinkAXTreeSource::SerializeTableAttributes(WebAXObject src, + AXContentNodeData* dst) const { + const bool is_table_like_role = ui::IsTableLike(dst->role); + if (is_table_like_role) { + int aria_colcount = src.AriaColumnCount(); + if (aria_colcount) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kAriaColumnCount, + aria_colcount); } - if (dst->role == ax::mojom::Role::kInlineTextBox) { - WebVector<int> src_character_offsets; - src.CharacterOffsets(src_character_offsets); - std::vector<int32_t> character_offsets; - character_offsets.reserve(src_character_offsets.size()); - for (size_t i = 0; i < src_character_offsets.size(); ++i) - character_offsets.push_back(src_character_offsets[i]); - dst->AddIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets, - character_offsets); - - WebVector<int> src_word_starts; - WebVector<int> src_word_ends; - src.GetWordBoundaries(src_word_starts, src_word_ends); - std::vector<int32_t> word_starts; - std::vector<int32_t> word_ends; - word_starts.reserve(src_word_starts.size()); - word_ends.reserve(src_word_starts.size()); - for (size_t i = 0; i < src_word_starts.size(); ++i) { - word_starts.push_back(src_word_starts[i]); - word_ends.push_back(src_word_ends[i]); - } - dst->AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, - word_starts); - dst->AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, - word_ends); + int aria_rowcount = src.AriaRowCount(); + if (aria_rowcount) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kAriaRowCount, + aria_rowcount); } + } - if (src.AccessKey().length()) { - TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kAccessKey, - src.AccessKey().Utf8()); + if (ui::IsTableRow(dst->role)) { + WebAXObject header = src.RowHeader(); + if (!header.IsDetached()) { + // TODO(accessibility): these should be computed by ui::AXTableInfo and + // removed here. + dst->AddIntAttribute(ax::mojom::IntAttribute::kTableRowHeaderId, + header.AxID()); } + } - if (src.AutoComplete().length()) { - TruncateAndAddStringAttribute(dst, - ax::mojom::StringAttribute::kAutoComplete, - src.AutoComplete().Utf8()); - } + if (ui::IsCellOrTableHeader(dst->role)) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kTableCellColumnSpan, + src.CellColumnSpan()); + dst->AddIntAttribute(ax::mojom::IntAttribute::kTableCellRowSpan, + src.CellRowSpan()); + } - if (src.Action() != ax::mojom::DefaultActionVerb::kNone) { - dst->SetDefaultActionVerb(src.Action()); + if (ui::IsCellOrTableHeader(dst->role) || ui::IsTableRow(dst->role)) { + // aria-rowindex and aria-colindex are supported on cells, headers and + // rows. + int aria_rowindex = src.AriaRowIndex(); + if (aria_rowindex) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kAriaCellRowIndex, + aria_rowindex); } - blink::WebString display_style = src.ComputedStyleDisplay(); - if (!display_style.IsEmpty()) { - TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kDisplay, - display_style.Utf8()); + int aria_colindex = src.AriaColumnIndex(); + if (aria_colindex) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kAriaCellColumnIndex, + aria_colindex); } + } - if (src.Language().length()) { - if (parent.IsNull() || parent.Language() != src.Language()) - TruncateAndAddStringAttribute( - dst, ax::mojom::StringAttribute::kLanguage, src.Language().Utf8()); - } + if (ui::IsTableHeader(dst->role) && + src.SortDirection() != ax::mojom::SortDirection::kNone) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kSortDirection, + static_cast<int32_t>(src.SortDirection())); + } +} - if (src.KeyboardShortcut().length() && - !dst->HasStringAttribute(ax::mojom::StringAttribute::kKeyShortcuts)) { - TruncateAndAddStringAttribute(dst, - ax::mojom::StringAttribute::kKeyShortcuts, - src.KeyboardShortcut().Utf8()); - } +void BlinkAXTreeSource::SerializeScrollAttributes( + WebAXObject src, + AXContentNodeData* dst) const { + // Only mark as scrollable if user has actual scrollbars to use. + dst->AddBoolAttribute(ax::mojom::BoolAttribute::kScrollable, + src.IsUserScrollable()); + // Provide x,y scroll info if scrollable in any way (programmatically or via + // user). + const gfx::Point& scroll_offset = src.GetScrollOffset(); + dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollX, scroll_offset.x()); + dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollY, scroll_offset.y()); + + const gfx::Point& min_scroll_offset = src.MinimumScrollOffset(); + dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollXMin, + min_scroll_offset.x()); + dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollYMin, + min_scroll_offset.y()); + + const gfx::Point& max_scroll_offset = src.MaximumScrollOffset(); + dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollXMax, + max_scroll_offset.x()); + dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollYMax, + max_scroll_offset.y()); +} - if (!src.NextOnLine().IsDetached()) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId, - src.NextOnLine().AxID()); - } +void BlinkAXTreeSource::SerializeChooserPopupAttributes( + WebAXObject src, + AXContentNodeData* dst) const { + WebAXObject chooser_popup = src.ChooserPopup(); + if (!chooser_popup.IsNull()) { + int32_t chooser_popup_id = chooser_popup.AxID(); + auto controls_ids = + dst->GetIntListAttribute(ax::mojom::IntListAttribute::kControlsIds); + controls_ids.push_back(chooser_popup_id); + dst->AddIntListAttribute(ax::mojom::IntListAttribute::kControlsIds, + controls_ids); + } +} - if (!src.PreviousOnLine().IsDetached()) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId, - src.PreviousOnLine().AxID()); +void BlinkAXTreeSource::SerializeOtherScreenReaderAttributes( + WebAXObject src, + AXContentNodeData* dst) const { + if (dst->role == ax::mojom::Role::kColorWell) + dst->AddIntAttribute(ax::mojom::IntAttribute::kColorValue, + src.ColorValue()); + + if (dst->role == ax::mojom::Role::kLink) { + WebAXObject target = src.InPageLinkTarget(); + if (!target.IsNull()) { + int32_t target_id = target.AxID(); + dst->AddIntAttribute(ax::mojom::IntAttribute::kInPageLinkTargetId, + target_id); } + } - if (!src.AriaActiveDescendant().IsDetached()) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kActivedescendantId, - src.AriaActiveDescendant().AxID()); - } + if (dst->role == ax::mojom::Role::kRadioButton) { + AddIntListAttributeFromWebObjects( + ax::mojom::IntListAttribute::kRadioGroupIds, src.RadioButtonsInGroup(), + dst); + } - if (!src.ErrorMessage().IsDetached()) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kErrormessageId, - src.ErrorMessage().AxID()); - } + if (src.AriaCurrentState() != ax::mojom::AriaCurrentState::kNone) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kAriaCurrentState, + static_cast<int32_t>(src.AriaCurrentState())); + } - if (ui::IsHeading(dst->role) && src.HeadingLevel()) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel, - src.HeadingLevel()); - } else if (ui::SupportsHierarchicalLevel(dst->role) && - src.HierarchicalLevel()) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel, - src.HierarchicalLevel()); - } + if (src.InvalidState() != ax::mojom::InvalidState::kNone) + dst->SetInvalidState(src.InvalidState()); + if (src.InvalidState() == ax::mojom::InvalidState::kOther && + src.AriaInvalidValue().length()) { + TruncateAndAddStringAttribute(dst, + ax::mojom::StringAttribute::kAriaInvalidValue, + src.AriaInvalidValue().Utf8()); + } - if (src.SetSize()) - dst->AddIntAttribute(ax::mojom::IntAttribute::kSetSize, src.SetSize()); - - if (src.PosInSet()) - dst->AddIntAttribute(ax::mojom::IntAttribute::kPosInSet, src.PosInSet()); - - if (src.CanvasHasFallbackContent()) - dst->AddBoolAttribute(ax::mojom::BoolAttribute::kCanvasHasFallback, true); - - // Spelling, grammar and other document markers. - WebVector<ax::mojom::MarkerType> src_marker_types; - WebVector<int> src_marker_starts; - WebVector<int> src_marker_ends; - src.Markers(src_marker_types, src_marker_starts, src_marker_ends); - DCHECK_EQ(src_marker_types.size(), src_marker_starts.size()); - DCHECK_EQ(src_marker_starts.size(), src_marker_ends.size()); - - if (src_marker_types.size()) { - std::vector<int32_t> marker_types; - std::vector<int32_t> marker_starts; - std::vector<int32_t> marker_ends; - marker_types.reserve(src_marker_types.size()); - marker_starts.reserve(src_marker_starts.size()); - marker_ends.reserve(src_marker_ends.size()); - for (size_t i = 0; i < src_marker_types.size(); ++i) { - marker_types.push_back(static_cast<int32_t>(src_marker_types[i])); - marker_starts.push_back(src_marker_starts[i]); - marker_ends.push_back(src_marker_ends[i]); - } - dst->AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerTypes, - marker_types); - dst->AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts, - marker_starts); - dst->AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds, - marker_ends); - } + if (src.CheckedState() != ax::mojom::CheckedState::kNone) { + dst->SetCheckedState(src.CheckedState()); + } - if (src.IsInLiveRegion()) { - dst->AddBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic, - src.LiveRegionAtomic()); - if (!src.LiveRegionStatus().IsEmpty()) { - TruncateAndAddStringAttribute(dst, - ax::mojom::StringAttribute::kLiveStatus, - src.LiveRegionStatus().Utf8()); - } - TruncateAndAddStringAttribute(dst, - ax::mojom::StringAttribute::kLiveRelevant, - src.LiveRegionRelevant().Utf8()); - // If we are not at the root of an atomic live region. - if (src.ContainerLiveRegionAtomic() && - !src.LiveRegionRoot().IsDetached() && !src.LiveRegionAtomic()) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kMemberOfId, - src.LiveRegionRoot().AxID()); - } - dst->AddBoolAttribute(ax::mojom::BoolAttribute::kContainerLiveAtomic, - src.ContainerLiveRegionAtomic()); - dst->AddBoolAttribute(ax::mojom::BoolAttribute::kContainerLiveBusy, - src.ContainerLiveRegionBusy()); - TruncateAndAddStringAttribute( - dst, ax::mojom::StringAttribute::kContainerLiveStatus, - src.ContainerLiveRegionStatus().Utf8()); - TruncateAndAddStringAttribute( - dst, ax::mojom::StringAttribute::kContainerLiveRelevant, - src.ContainerLiveRegionRelevant().Utf8()); - } + if (dst->role == ax::mojom::Role::kInlineTextBox) { + SerializeInlineTextBoxAttributes(src, dst); + } - if (dst->role == ax::mojom::Role::kProgressIndicator || - dst->role == ax::mojom::Role::kMeter || - dst->role == ax::mojom::Role::kScrollBar || - dst->role == ax::mojom::Role::kSlider || - dst->role == ax::mojom::Role::kSpinButton || - (dst->role == ax::mojom::Role::kSplitter && - dst->HasState(ax::mojom::State::kFocusable))) { - float value; - if (src.ValueForRange(&value)) - dst->AddFloatAttribute(ax::mojom::FloatAttribute::kValueForRange, - value); - - float max_value; - if (src.MaxValueForRange(&max_value)) { - dst->AddFloatAttribute(ax::mojom::FloatAttribute::kMaxValueForRange, - max_value); - } + if (src.AccessKey().length()) { + TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kAccessKey, + src.AccessKey().Utf8()); + } - float min_value; - if (src.MinValueForRange(&min_value)) { - dst->AddFloatAttribute(ax::mojom::FloatAttribute::kMinValueForRange, - min_value); - } + if (src.AutoComplete().length()) { + TruncateAndAddStringAttribute(dst, + ax::mojom::StringAttribute::kAutoComplete, + src.AutoComplete().Utf8()); + } - float step_value; - if (src.StepValueForRange(&step_value)) { - dst->AddFloatAttribute(ax::mojom::FloatAttribute::kStepValueForRange, - step_value); - } - } + if (src.Action() != ax::mojom::DefaultActionVerb::kNone) { + dst->SetDefaultActionVerb(src.Action()); + } - if (ui::IsDialog(dst->role)) { - dst->AddBoolAttribute(ax::mojom::BoolAttribute::kModal, src.IsModal()); - } + blink::WebString display_style = src.ComputedStyleDisplay(); + if (!display_style.IsEmpty()) { + TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kDisplay, + display_style.Utf8()); + } - if (dst->role == ax::mojom::Role::kRootWebArea) - TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kHtmlTag, - "#document"); + WebAXObject parent = ParentObjectUnignored(src); + if (src.Language().length()) { + if (parent.IsNull() || parent.Language() != src.Language()) + TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kLanguage, + src.Language().Utf8()); + } - const bool is_table_like_role = ui::IsTableLike(dst->role); - if (is_table_like_role) { - int aria_colcount = src.AriaColumnCount(); - if (aria_colcount) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kAriaColumnCount, - aria_colcount); - } + if (src.KeyboardShortcut().length() && + !dst->HasStringAttribute(ax::mojom::StringAttribute::kKeyShortcuts)) { + TruncateAndAddStringAttribute(dst, + ax::mojom::StringAttribute::kKeyShortcuts, + src.KeyboardShortcut().Utf8()); + } - int aria_rowcount = src.AriaRowCount(); - if (aria_rowcount) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kAriaRowCount, - aria_rowcount); - } - } + if (!src.NextOnLine().IsDetached()) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId, + src.NextOnLine().AxID()); + } - if (ui::IsTableRow(dst->role)) { - WebAXObject header = src.RowHeader(); - if (!header.IsDetached()) { - // TODO(accessibility): these should be computed by ui::AXTableInfo and - // removed here. - dst->AddIntAttribute(ax::mojom::IntAttribute::kTableRowHeaderId, - header.AxID()); - } - } + if (!src.PreviousOnLine().IsDetached()) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId, + src.PreviousOnLine().AxID()); + } - if (ui::IsCellOrTableHeader(dst->role)) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kTableCellColumnSpan, - src.CellColumnSpan()); - dst->AddIntAttribute(ax::mojom::IntAttribute::kTableCellRowSpan, - src.CellRowSpan()); - } + if (!src.AriaActiveDescendant().IsDetached()) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kActivedescendantId, + src.AriaActiveDescendant().AxID()); + } - if (ui::IsCellOrTableHeader(dst->role) || ui::IsTableRow(dst->role)) { - // aria-rowindex and aria-colindex are supported on cells, headers and - // rows. - int aria_rowindex = src.AriaRowIndex(); - if (aria_rowindex) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kAriaCellRowIndex, - aria_rowindex); - } + if (!src.ErrorMessage().IsDetached()) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kErrormessageId, + src.ErrorMessage().AxID()); + } - int aria_colindex = src.AriaColumnIndex(); - if (aria_colindex) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kAriaCellColumnIndex, - aria_colindex); - } - } + if (ui::SupportsHierarchicalLevel(dst->role) && src.HierarchicalLevel()) { + dst->AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel, + src.HierarchicalLevel()); + } - if (ui::IsTableHeader(dst->role) && - src.SortDirection() != ax::mojom::SortDirection::kNone) { - dst->AddIntAttribute(ax::mojom::IntAttribute::kSortDirection, - static_cast<int32_t>(src.SortDirection())); + if (src.CanvasHasFallbackContent()) + dst->AddBoolAttribute(ax::mojom::BoolAttribute::kCanvasHasFallback, true); + + if (dst->role == ax::mojom::Role::kProgressIndicator || + dst->role == ax::mojom::Role::kMeter || + dst->role == ax::mojom::Role::kScrollBar || + dst->role == ax::mojom::Role::kSlider || + dst->role == ax::mojom::Role::kSpinButton || + (dst->role == ax::mojom::Role::kSplitter && + dst->HasState(ax::mojom::State::kFocusable))) { + float value; + if (src.ValueForRange(&value)) + dst->AddFloatAttribute(ax::mojom::FloatAttribute::kValueForRange, value); + + float max_value; + if (src.MaxValueForRange(&max_value)) { + dst->AddFloatAttribute(ax::mojom::FloatAttribute::kMaxValueForRange, + max_value); } - if (dst->role == ax::mojom::Role::kImage) - AddImageAnnotations(src, dst); - - // If a link or web area isn't otherwise labeled and contains - // exactly one image (searching only to a max depth of 2), - // annotate the link/web area with the image's annotation, too. - if (dst->role == ax::mojom::Role::kLink || - dst->role == ax::mojom::Role::kRootWebArea) { - WebAXObject inner_image; - if (FindExactlyOneInnerImageInMaxDepthTwo(src, &inner_image)) - AddImageAnnotations(inner_image, dst); + float min_value; + if (src.MinValueForRange(&min_value)) { + dst->AddFloatAttribute(ax::mojom::FloatAttribute::kMinValueForRange, + min_value); } - WebNode node = src.GetNode(); - if (!node.IsNull() && node.IsElementNode()) { - WebElement element = node.To<WebElement>(); - if (element.HasHTMLTagName("input") && element.HasAttribute("type")) { - TruncateAndAddStringAttribute(dst, - ax::mojom::StringAttribute::kInputType, - element.GetAttribute("type").Utf8()); - } + float step_value; + if (src.StepValueForRange(&step_value)) { + dst->AddFloatAttribute(ax::mojom::FloatAttribute::kStepValueForRange, + step_value); } } - // The majority of the rest of this code computes attributes needed for - // all modes, not just for screen readers. + if (ui::IsDialog(dst->role)) { + dst->AddBoolAttribute(ax::mojom::BoolAttribute::kModal, src.IsModal()); + } - WebNode node = src.GetNode(); - bool is_iframe = false; + if (dst->role == ax::mojom::Role::kRootWebArea) + TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kHtmlTag, + "#document"); + + if (dst->role == ax::mojom::Role::kImage) + AddImageAnnotations(src, dst); + + // If a link or web area isn't otherwise labeled and contains exactly one + // image (searching only to a max depth of 2), and the link doesn't have + // accessible text from an attribute like aria-label, then annotate the + // link/web area with the image's annotation, too. + if ((ui::IsLink(dst->role) || ui::IsDocument(dst->role)) && + dst->GetNameFrom() != ax::mojom::NameFrom::kAttribute) { + WebAXObject inner_image; + if (FindExactlyOneInnerImageInMaxDepthThree(src, &inner_image)) + AddImageAnnotations(inner_image, dst); + } + WebNode node = src.GetNode(); if (!node.IsNull() && node.IsElementNode()) { WebElement element = node.To<WebElement>(); - is_iframe = element.HasHTMLTagName("iframe"); - - if (element.HasAttribute("class")) { - TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kClassName, - element.GetAttribute("class").Utf8()); - } - if (accessibility_mode_.has_mode(ui::AXMode::kHTML)) { - // TODO(ctguil): The tagName in WebKit is lower cased but - // HTMLElement::nodeName calls localNameUpper. Consider adding - // a WebElement method that returns the original lower cased tagName. - TruncateAndAddStringAttribute( - dst, ax::mojom::StringAttribute::kHtmlTag, - base::ToLowerASCII(element.TagName().Utf8())); - for (unsigned i = 0; i < element.AttributeCount(); ++i) { - std::string name = - base::ToLowerASCII(element.AttributeLocalName(i).Utf8()); - if (name != "class") { // class already in kClassName. - std::string value = element.AttributeValue(i).Utf8(); - dst->html_attributes.push_back(std::make_pair(name, value)); - } - } - -// TODO(nektar): Turn off kHTMLAccessibilityMode for automation and Mac -// and remove ifdef. -#if defined(OS_WIN) || defined(OS_CHROMEOS) - if (dst->role == ax::mojom::Role::kMath && element.InnerHTML().length()) { - TruncateAndAddStringAttribute(dst, - ax::mojom::StringAttribute::kInnerHtml, - element.InnerHTML().Utf8()); - } -#endif - } - - if (src.IsEditable()) { - if (src.IsEditableRoot()) - dst->AddBoolAttribute(ax::mojom::BoolAttribute::kEditableRoot, true); - - if (src.IsControl() && - !dst->HasState(ax::mojom::State::kRichlyEditable)) { - // Only for simple input controls -- rich editable areas use AXTreeData. - dst->AddIntAttribute(ax::mojom::IntAttribute::kTextSelStart, - src.SelectionStart()); - dst->AddIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, - src.SelectionEnd()); - } + if (element.HasHTMLTagName("input") && element.HasAttribute("type")) { + TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kInputType, + element.GetAttribute("type").Utf8()); } + } - // ARIA role. - if (element.HasAttribute("role")) { - TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kRole, - element.GetAttribute("role").Utf8()); - } else { - std::string role = GetEquivalentAriaRoleString(dst->role); - if (!role.empty()) - TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kRole, - role); + // aria-dropeffect is deprecated in WAI-ARIA 1.1. + WebVector<ax::mojom::Dropeffect> src_dropeffects; + src.Dropeffects(src_dropeffects); + if (!src_dropeffects.empty()) { + for (auto&& dropeffect : src_dropeffects) { + dst->AddDropeffect(dropeffect); } - - // Presence of other ARIA attributes. - if (src.HasAriaAttribute()) - dst->AddBoolAttribute(ax::mojom::BoolAttribute::kHasAriaAttribute, true); - - // Frames and iframes: - // If there are children, the fallback content has been rendered and should - // be used instead. For example, the fallback content may be rendered if - // there was an error loading an <object>. In that case, only expose the - // children. A node should not have both children and a child tree. - WebFrame* frame = WebFrame::FromFrameOwnerElement(element); - if (frame && !src.ChildCount()) - dst->child_routing_id = RenderFrame::GetRoutingIdForWebFrame(frame); } +} - // Add the ids of *indirect* children - those who are children of this node, - // but whose parent is *not* this node. One example is a table - // cell, which is a child of both a row and a column. Because the cell's - // parent is the row, the row adds it as a child, and the column adds it - // as an indirect child. - int child_count = src.ChildCount(); - std::vector<int32_t> indirect_child_ids; - for (int i = 0; i < child_count; ++i) { - WebAXObject child = src.ChildAt(i); - if (!is_iframe && !child.IsDetached() && !IsParentUnignoredOf(src, child)) - indirect_child_ids.push_back(child.AxID()); - } - if (indirect_child_ids.size() > 0) { - dst->AddIntListAttribute(ax::mojom::IntListAttribute::kIndirectChildIds, - indirect_child_ids); +void BlinkAXTreeSource::SerializeEditableTextAttributes( + WebAXObject src, + AXContentNodeData* dst) const { + DCHECK(src.IsEditable()); + + if (src.IsEditableRoot()) + dst->AddBoolAttribute(ax::mojom::BoolAttribute::kEditableRoot, true); + + if (src.IsNativeTextControl()) { + // Selection offsets are only used for plain text controls, (input of a text + // field type, and textarea). Rich editable areas, such as contenteditables, + // use AXTreeData. + // + // TODO(nektar): Remove kTextSelStart and kTextSelEnd from the renderer. + dst->AddIntAttribute(ax::mojom::IntAttribute::kTextSelStart, + src.SelectionStart()); + dst->AddIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, + src.SelectionEnd()); } +} - if (src.IsScrollableContainer()) { - // Only mark as scrollable if user has actual scrollbars to use. - dst->AddBoolAttribute(ax::mojom::BoolAttribute::kScrollable, - src.IsUserScrollable()); - // Provide x,y scroll info if scrollable in any way (programmatically or via - // user). - const gfx::Point& scroll_offset = src.GetScrollOffset(); - dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollX, scroll_offset.x()); - dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollY, scroll_offset.y()); - - const gfx::Point& min_scroll_offset = src.MinimumScrollOffset(); - dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollXMin, - min_scroll_offset.x()); - dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollYMin, - min_scroll_offset.y()); - - const gfx::Point& max_scroll_offset = src.MaximumScrollOffset(); - dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollXMax, - max_scroll_offset.x()); - dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollYMax, - max_scroll_offset.y()); +void BlinkAXTreeSource::SerializeElementAttributes( + WebAXObject src, + WebElement element, + AXContentNodeData* dst) const { + if (element.HasAttribute("class")) { + TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kClassName, + element.GetAttribute("class").Utf8()); } - if (dst->id == image_data_node_id_) { - // In general, string attributes should be truncated using - // TruncateAndAddStringAttribute, but ImageDataUrl contains a data url - // representing an image, so add it directly using AddStringAttribute. - dst->AddStringAttribute(ax::mojom::StringAttribute::kImageDataUrl, - src.ImageDataUrl(max_image_data_size_).Utf8()); + // ARIA role. + if (element.HasAttribute("role")) { + TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kRole, + element.GetAttribute("role").Utf8()); + } else { + std::string role = GetEquivalentAriaRoleString(dst->role); + if (!role.empty()) + TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kRole, + role); } - // aria-dropeffect is deprecated in WAI-ARIA 1.1. - WebVector<ax::mojom::Dropeffect> src_dropeffects; - src.Dropeffects(src_dropeffects); - if (!src_dropeffects.empty()) { - for (auto&& dropeffect : src_dropeffects) { - dst->AddDropeffect(dropeffect); + // Frames and iframes: + // If there are children, the fallback content has been rendered and should + // be used instead. For example, the fallback content may be rendered if + // there was an error loading an <object>. In that case, only expose the + // children. A node should not have both children and a child tree. + WebFrame* frame = WebFrame::FromFrameOwnerElement(element); + if (frame && !src.ChildCount()) + dst->child_routing_id = RenderFrame::GetRoutingIdForWebFrame(frame); +} + +void BlinkAXTreeSource::SerializeHTMLAttributes(WebAXObject src, + WebElement element, + AXContentNodeData* dst) const { + // TODO(ctguil): The tagName in WebKit is lower cased but + // HTMLElement::nodeName calls localNameUpper. Consider adding + // a WebElement method that returns the original lower cased tagName. + TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kHtmlTag, + base::ToLowerASCII(element.TagName().Utf8())); + for (unsigned i = 0; i < element.AttributeCount(); ++i) { + std::string name = base::ToLowerASCII(element.AttributeLocalName(i).Utf8()); + if (name != "class") { // class already in kClassName. + std::string value = element.AttributeValue(i).Utf8(); + dst->html_attributes.push_back(std::make_pair(name, value)); } } + +// TODO(nektar): Turn off kHTMLAccessibilityMode for automation and Mac +// and remove ifdef. +#if defined(OS_WIN) || defined(OS_CHROMEOS) + if (dst->role == ax::mojom::Role::kMath && element.InnerHTML().length()) { + TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kInnerHtml, + element.InnerHTML().Utf8()); + } +#endif } blink::WebDocument BlinkAXTreeSource::GetMainDocument() const { @@ -1253,31 +1340,36 @@ void BlinkAXTreeSource::AddImageAnnotations(blink::WebAXObject& src, return; } - // Reject images that are explicitly empty, or that have a name already. - // - // In the future, we may annotate some images that have a name - // if we think we can add additional useful information. + // Reject images that are explicitly empty, or that have a + // meaningful name already. ax::mojom::NameFrom name_from; blink::WebVector<WebAXObject> name_objects; blink::WebString web_name = src.GetName(name_from, name_objects); - // Normally we don't assign an annotation to an image if it already - // has a name. There are a few exceptions where we ignore the name. - bool treat_name_as_empty = false; + // If an image has a nonempty name, compute whether we should add an + // image annotation or not. + bool should_annotate_image_with_nonempty_name = false; // When visual debugging is enabled, the "title" attribute is set to a // string beginning with a "%". If the name comes from that string we // can ignore it, and treat the name as empty. if (image_annotation_debugging_ && base::StartsWith(web_name.Utf8(), "%", base::CompareCase::SENSITIVE)) - treat_name_as_empty = true; + should_annotate_image_with_nonempty_name = true; + + if (features::IsAugmentExistingImageLabelsEnabled()) { + // If the name consists of mostly stopwords, we can add an image + // annotations. See ax_image_stopwords.h for details. + if (image_annotator_->ImageNameHasMostlyStopwords(web_name.Utf8())) + should_annotate_image_with_nonempty_name = true; + } // If the image's name is explicitly empty, or if it has a name (and // we're not treating the name as empty), then it's ineligible for // an annotation. if ((name_from == ax::mojom::NameFrom::kAttributeExplicitlyEmpty || !web_name.IsEmpty()) && - !treat_name_as_empty) { + !should_annotate_image_with_nonempty_name) { dst->SetImageAnnotationStatus( ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation); return; @@ -1291,13 +1383,13 @@ void BlinkAXTreeSource::AddImageAnnotations(blink::WebAXObject& src, if (dst->role == ax::mojom::Role::kRootWebArea) { std::string filename = GURL(document().Url()).ExtractFileName(); if (base::StartsWith(dst_name, filename, base::CompareCase::SENSITIVE)) - treat_name_as_empty = true; + should_annotate_image_with_nonempty_name = true; } // |dst| may be a document or link containing an image. Skip annotating // it if it already has text other than whitespace. if (!base::ContainsOnlyChars(dst_name, base::kWhitespaceASCII) && - !treat_name_as_empty) { + !should_annotate_image_with_nonempty_name) { dst->SetImageAnnotationStatus( ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation); return; diff --git a/chromium/content/renderer/accessibility/blink_ax_tree_source.h b/chromium/content/renderer/accessibility/blink_ax_tree_source.h index 1cfefe0cbe8..59783fb817e 100644 --- a/chromium/content/renderer/accessibility/blink_ax_tree_source.h +++ b/chromium/content/renderer/accessibility/blink_ax_tree_source.h @@ -12,6 +12,7 @@ #include "base/optional.h" #include "content/common/ax_content_node_data.h" +#include "content/common/ax_content_tree_data.h" #include "third_party/blink/public/web/web_ax_object.h" #include "third_party/blink/public/web/web_document.h" #include "ui/accessibility/ax_mode.h" @@ -91,12 +92,6 @@ class BlinkAXTreeSource bool ShouldLoadInlineTextBoxes(const blink::WebAXObject& obj) const; void SetLoadInlineTextBoxesForId(int32_t id); - // Call this to enable populating the DOMNodeID for each node. This is - // currently only used for accessible PDF exporting. Warning, this is totally - // unrelated to the accessibility node ID, or the ID attribute for an HTML - // element. - void EnableDOMNodeIDs(); - // AXTreeSource implementation. bool GetTreeData(AXContentTreeData* tree_data) const override; blink::WebAXObject GetRoot() const override; @@ -131,6 +126,45 @@ class BlinkAXTreeSource return focus_; } + void SerializeBoundingBoxAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializePDFAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeSparseAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeNameAndDescriptionAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeValueAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeStateAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeStyleAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeInlineTextBoxAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeMarkerAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeLiveRegionAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeListAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeTableAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeScrollAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeChooserPopupAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeOtherScreenReaderAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeEditableTextAttributes(blink::WebAXObject src, + AXContentNodeData* dst) const; + void SerializeElementAttributes(blink::WebAXObject src, + blink::WebElement element, + AXContentNodeData* dst) const; + void SerializeHTMLAttributes(blink::WebAXObject src, + blink::WebElement element, + AXContentNodeData* dst) const; + blink::WebAXObject ComputeRoot() const; // Max length for attributes such as aria-label. @@ -157,11 +191,6 @@ class BlinkAXTreeSource // A set of IDs for which we should always load inline text boxes. std::set<int32_t> load_inline_text_boxes_ids_; - // Whether we should store Blink DOMNodeIds in the accessibility tree. - // Warning, this is totally unrelated to the accessibility node ID, or the ID - // attribute for an HTML element. - bool enable_dom_node_ids_ = false; - // The ID of the object to fetch image data for. int image_data_node_id_ = -1; diff --git a/chromium/content/renderer/accessibility/render_accessibility_impl.cc b/chromium/content/renderer/accessibility/render_accessibility_impl.cc index a504fc800b9..5ee61fd1adc 100644 --- a/chromium/content/renderer/accessibility/render_accessibility_impl.cc +++ b/chromium/content/renderer/accessibility/render_accessibility_impl.cc @@ -17,12 +17,13 @@ #include "base/debug/crash_logging.h" #include "base/location.h" #include "base/memory/ptr_util.h" -#include "base/metrics/histogram_macros.h" #include "base/single_thread_task_runner.h" #include "base/strings/string_split.h" #include "base/strings/utf_string_conversions.h" #include "base/threading/thread_task_runner_handle.h" +#include "base/timer/elapsed_timer.h" #include "build/build_config.h" +#include "content/public/renderer/render_thread.h" #include "content/renderer/accessibility/ax_action_target_factory.h" #include "content/renderer/accessibility/ax_image_annotator.h" #include "content/renderer/accessibility/blink_ax_action_target.h" @@ -31,6 +32,8 @@ #include "content/renderer/render_frame_proxy.h" #include "content/renderer/render_view_impl.h" #include "services/image_annotation/public/mojom/image_annotation.mojom.h" +#include "services/metrics/public/cpp/mojo_ukm_recorder.h" +#include "services/metrics/public/cpp/ukm_builders.h" #include "third_party/blink/public/platform/task_type.h" #include "third_party/blink/public/platform/web_float_rect.h" #include "third_party/blink/public/web/web_document.h" @@ -57,6 +60,8 @@ using blink::WebView; namespace { +constexpr int kDelayForDeferredEvents = 350; + void SetAccessibilityCrashKey(ui::AXMode mode) { // Add a crash key with the ax_mode, to enable searching for top crashes that // occur when accessibility is turned on. This adds it for each renderer, @@ -125,7 +130,6 @@ void AXTreeSnapshotterImpl::SnapshotContentTree(ui::AXMode ax_mode, BlinkAXTreeSource tree_source(render_frame_, ax_mode); tree_source.SetRoot(root); - tree_source.EnableDOMNodeIDs(); ScopedFreezeBlinkAXTreeSource freeze(&tree_source); // The serializer returns an AXContentTreeUpdate, which can store a complete @@ -177,8 +181,12 @@ RenderAccessibilityImpl::RenderAccessibilityImpl( serializer_(&tree_source_), plugin_tree_source_(nullptr), last_scroll_offset_(gfx::Size()), - ack_pending_(false), + event_schedule_status_(EventScheduleStatus::kNotWaiting), reset_token_(0) { + mojo::PendingRemote<ukm::mojom::UkmRecorderInterface> recorder; + content::RenderThread::Get()->BindHostReceiver( + recorder.InitWithNewPipeAndPassReceiver()); + ukm_recorder_ = std::make_unique<ukm::MojoUkmRecorder>(std::move(recorder)); WebView* web_view = render_frame_->GetRenderView()->GetWebView(); WebSettings* settings = web_view->GetSettings(); @@ -195,6 +203,16 @@ RenderAccessibilityImpl::RenderAccessibilityImpl( settings->SetInlineTextBoxAccessibilityEnabled(true); #endif +#if defined(OS_MACOSX) + // aria-modal currently prunes the accessibility tree on Mac only. + settings->SetAriaModalPrunesAXTree(true); +#endif + + if (render_frame_->IsMainFrame()) + event_schedule_mode_ = EventScheduleMode::kDeferEvents; + else + event_schedule_mode_ = EventScheduleMode::kProcessEventsImmediately; + const WebDocument& document = GetMainDocument(); if (!document.IsNull()) { ax_context_ = std::make_unique<WebAXContext>(document); @@ -222,9 +240,14 @@ void RenderAccessibilityImpl::DidCreateNewDocument() { } void RenderAccessibilityImpl::DidCommitProvisionalLoad( - bool is_same_document_navigation, ui::PageTransition transition) { has_injected_stylesheet_ = false; + + // If we have events scheduled, but not sent, cancel them + CancelScheduledEvents(); + // Defer events during initial page load. + event_schedule_mode_ = EventScheduleMode::kDeferEvents; + // Remove the image annotator if the page is loading and it was added for // the one-shot image annotation (i.e. AXMode for image annotation is not // set). @@ -283,46 +306,61 @@ void RenderAccessibilityImpl::AccessibilityModeChanged(const ui::AXMode& mode) { } void RenderAccessibilityImpl::HitTest( - const ui::AXActionData& action_data, + const gfx::Point& point, + ax::mojom::Event event_to_fire, + int request_id, mojom::RenderAccessibility::HitTestCallback callback) { - // This method should be called exclusively for kHitTest actions. - DCHECK_EQ(action_data.action, ax::mojom::Action::kHitTest); - DCHECK_NE(action_data.hit_test_event_to_fire, ax::mojom::Event::kNone); - WebAXObject ax_object; const WebDocument& document = GetMainDocument(); if (!document.IsNull()) { auto root_obj = WebAXObject::FromWebDocument(document); if (root_obj.UpdateLayoutAndCheckValidity()) - ax_object = root_obj.HitTest(action_data.target_point); + ax_object = root_obj.HitTest(point); } // Return if no attached accessibility object was found for the main document. if (ax_object.IsDetached()) { - std::move(callback).Run(/*child_frame_hit_test_info=*/nullptr); + std::move(callback).Run(/*hit_test_response=*/nullptr); return; } - // If the object that was hit has a child frame, we have to send a message - // back to the browser to do the hit test in the child frame, recursively. + // If the result was in the same frame, return the result. AXContentNodeData data; ScopedFreezeBlinkAXTreeSource freeze(&tree_source_); tree_source_.SerializeNode(ax_object, &data); if (data.child_routing_id == MSG_ROUTING_NONE) { - // Otherwise, send an event on the node that was hit. - const std::vector<ui::AXEventIntent> intents; - HandleAXEvent(ui::AXEvent( - ax_object.AxID(), action_data.hit_test_event_to_fire, - ax::mojom::EventFrom::kAction, intents, action_data.request_id)); - - // The mojo message still needs a reply. - std::move(callback).Run(/*child_frame_hit_test_info=*/nullptr); + // Optionally fire an event, if requested to. This is a good fit for + // features like touch exploration on Android, Chrome OS, and + // possibly other platforms - if the user explore a particular point, + // we fire a hover event on the nearest object under the point. + // + // Avoid using this mechanism to fire a particular sentinel event + // and then listen for that event to associate it with the hit test + // request. Instead, the mojo reply should be used directly. + if (event_to_fire != ax::mojom::Event::kNone) { + const std::vector<ui::AXEventIntent> intents; + HandleAXEvent(ui::AXEvent(ax_object.AxID(), event_to_fire, + ax::mojom::EventFrom::kAction, intents, + request_id)); + } + + // Reply with the result. + const auto& frame_token = render_frame_->GetWebFrame()->GetFrameToken(); + std::move(callback).Run( + mojom::HitTestResponse::New(frame_token, point, ax_object.AxID())); return; } - gfx::Point transformed_point = action_data.target_point; - bool is_remote_frame = RenderFrameProxy::FromRoutingID(data.child_routing_id); - if (is_remote_frame) { + // The result was in a child frame. Reply so that the + // client can do a hit test on the child frame recursively. + // If it's a remote frame, transform the point into the child frame's + // coordinate system. + gfx::Point transformed_point = point; + blink::WebFrame* child_frame = + blink::WebFrame::FromFrameOwnerElement(ax_object.GetNode()); + DCHECK(child_frame); + + if (child_frame->IsWebRemoteFrame()) { // Remote frames don't have access to the information from the visual // viewport regarding the visual viewport offset, so we adjust the // coordinates before sending them to the remote renderer. @@ -337,12 +375,8 @@ void RenderAccessibilityImpl::HitTest( gfx::Rect(rect).OffsetFromOrigin(); } - // Signal to the caller that we haven't handled this hit test yet, and that - // a new one will need to be performed over the child frame found. - std::move(callback).Run(mojom::ChildFrameHitTestInfo::New( - data.child_routing_id, transformed_point, - action_data.hit_test_event_to_fire)); - return; + std::move(callback).Run(mojom::HitTestResponse::New( + child_frame->GetFrameToken(), transformed_point, ax_object.AxID())); } void RenderAccessibilityImpl::PerformAction(const ui::AXActionData& data) { @@ -354,6 +388,9 @@ void RenderAccessibilityImpl::PerformAction(const ui::AXActionData& data) { if (!root.UpdateLayoutAndCheckValidity()) return; + // If an action was requested, we no longer want to defer events. + event_schedule_mode_ = EventScheduleMode::kProcessEventsImmediately; + std::unique_ptr<ui::AXActionTarget> target = AXActionTargetFactory::CreateFromNodeId(document, plugin_tree_source_, data.target_node_id); @@ -491,7 +528,7 @@ void RenderAccessibilityImpl::MarkWebAXObjectDirty(const WebAXObject& obj, if (subtree) serializer_.InvalidateSubtree(obj); - ScheduleSendAccessibilityEventsIfNeeded(); + ScheduleSendPendingAccessibilityEvents(); } void RenderAccessibilityImpl::HandleAXEvent(const ui::AXEvent& event) { @@ -549,10 +586,37 @@ void RenderAccessibilityImpl::HandleAXEvent(const ui::AXEvent& event) { } pending_events_.push_back(event); - ScheduleSendAccessibilityEventsIfNeeded(); + // Once we get the first load, we should no longer defer events. + if (event.event_type == ax::mojom::Event::kLoadComplete) + event_schedule_mode_ = EventScheduleMode::kProcessEventsImmediately; + + ScheduleSendPendingAccessibilityEvents(); +} + +bool RenderAccessibilityImpl::ShouldSerializeNodeForEvent( + const WebAXObject& obj, + const ui::AXEvent& event) const { + if (obj.IsDetached()) + return false; + + if (event.event_type == ax::mojom::Event::kTextSelectionChanged && + !obj.IsNativeTextControl()) { + // Selection changes on non-native text controls cause no change to the + // control node's data. + // + // Selection offsets exposed via kTextSelStart and kTextSelEnd are only used + // for plain text controls, (input of a text field type, and textarea). Rich + // editable areas, such as contenteditables, use AXTreeData. + // + // TODO(nektar): Remove kTextSelStart and kTextSelEnd from the renderer. + return false; + } + + return true; } -void RenderAccessibilityImpl::ScheduleSendAccessibilityEventsIfNeeded() { +void RenderAccessibilityImpl::ScheduleSendPendingAccessibilityEvents( + bool scheduling_from_task) { // Don't send accessibility events for frames that are not in the frame tree // yet (i.e., provisional frames used for remote-to-local navigations, which // haven't committed yet). Doing so might trigger layout, which may not work @@ -561,16 +625,56 @@ void RenderAccessibilityImpl::ScheduleSendAccessibilityEventsIfNeeded() { if (!render_frame_ || !render_frame_->in_frame_tree()) return; - if (!ack_pending_ && !weak_factory_.HasWeakPtrs()) { - // When no accessibility events are in-flight post a task to send - // the events to the browser. We use PostTask so that we can queue - // up additional events. - render_frame_->GetTaskRunner(blink::TaskType::kInternalDefault) - ->PostTask(FROM_HERE, - base::BindOnce( - &RenderAccessibilityImpl::SendPendingAccessibilityEvents, - weak_factory_.GetWeakPtr())); + switch (event_schedule_status_) { + case EventScheduleStatus::kScheduledDeferred: + if (event_schedule_mode_ == + EventScheduleMode::kProcessEventsImmediately) { + // Cancel scheduled deferred events so we can schedule events to be + // sent immediately. + CancelScheduledEvents(); + break; + } + // We have already scheduled a task to send pending events. + return; + case EventScheduleStatus::kScheduledImmediate: + // The send pending events task have been scheduled, but has not started. + return; + case EventScheduleStatus::kWaitingForAck: + // Events have been sent, wait for ack. + return; + case EventScheduleStatus::kNotWaiting: + // Once the events have been handled, we schedule the pending events from + // that task. In this case, there would be a weak ptr still in use. + if (!scheduling_from_task && + weak_factory_for_pending_events_.HasWeakPtrs()) + return; + break; } + + base::TimeDelta delay = base::TimeDelta::FromMilliseconds(0); + switch (event_schedule_mode_) { + case EventScheduleMode::kDeferEvents: + event_schedule_status_ = EventScheduleStatus::kScheduledDeferred; + // During page load, process changes on a delay so that they occur in + // larger batches, which helps improve efficiency of page loads. + delay = base::TimeDelta::FromMilliseconds(kDelayForDeferredEvents); + break; + case EventScheduleMode::kProcessEventsImmediately: + event_schedule_status_ = EventScheduleStatus::kScheduledImmediate; + delay = base::TimeDelta::FromMilliseconds(0); + break; + } + + // When no accessibility events are in-flight post a task to send + // the events to the browser. We use PostTask so that we can queue + // up additional events. + render_frame_->GetTaskRunner(blink::TaskType::kInternalDefault) + ->PostDelayedTask( + FROM_HERE, + base::BindOnce( + &RenderAccessibilityImpl::SendPendingAccessibilityEvents, + weak_factory_for_pending_events_.GetWeakPtr()), + delay); } int RenderAccessibilityImpl::GenerateAXID() { @@ -587,37 +691,31 @@ void RenderAccessibilityImpl::SetPluginTreeSource( } void RenderAccessibilityImpl::OnPluginRootNodeUpdated() { - // Search the accessibility tree for an EMBED element and post a + // Search the accessibility tree for plugin's root object and post a // children changed notification on it to force it to update the // plugin accessibility tree. - - ScopedFreezeBlinkAXTreeSource freeze(&tree_source_); - WebAXObject root = tree_source_.GetRoot(); - if (!root.UpdateLayoutAndCheckValidity()) + WebAXObject obj = GetPluginRoot(); + if (obj.IsNull()) return; - base::queue<WebAXObject> objs_to_explore; - objs_to_explore.push(root); - while (objs_to_explore.size()) { - WebAXObject obj = objs_to_explore.front(); - objs_to_explore.pop(); + HandleAXEvent(ui::AXEvent(obj.AxID(), ax::mojom::Event::kChildrenChanged)); +} - WebNode node = obj.GetNode(); - if (!node.IsNull() && node.IsElementNode()) { - WebElement element = node.To<WebElement>(); - if (element.HasHTMLTagName("embed")) { - HandleAXEvent( - ui::AXEvent(obj.AxID(), ax::mojom::Event::kChildrenChanged)); - break; - } - } +void RenderAccessibilityImpl::ShowPluginContextMenu() { + // Search the accessibility tree for plugin's root object and invoke + // ShowContextMenu() on it to show context menu for plugin. + WebAXObject obj = GetPluginRoot(); + if (obj.IsNull()) + return; - // Explore children of this object. - std::vector<WebAXObject> children; - tree_source_.GetChildren(obj, &children); - for (size_t i = 0; i < children.size(); ++i) - objs_to_explore.push(children[i]); - } + const WebDocument& document = GetMainDocument(); + if (document.IsNull()) + return; + + std::unique_ptr<ui::AXActionTarget> target = + AXActionTargetFactory::CreateFromNodeId(document, plugin_tree_source_, + obj.AxID()); + target->ShowContextMenu(); } WebDocument RenderAccessibilityImpl::GetMainDocument() { @@ -633,7 +731,10 @@ std::string RenderAccessibilityImpl::GetLanguage() { void RenderAccessibilityImpl::SendPendingAccessibilityEvents() { TRACE_EVENT0("accessibility", "RenderAccessibilityImpl::SendPendingAccessibilityEvents"); + base::ElapsedTimer timer_; + // Clear status here in case we return early. + event_schedule_status_ = EventScheduleStatus::kNotWaiting; WebDocument document = GetMainDocument(); if (document.IsNull()) return; @@ -641,8 +742,6 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() { if (pending_events_.empty() && dirty_objects_.empty()) return; - ack_pending_ = true; - // Make a copy of the events, because it's possible that // actions inside this loop will cause more events to be // queued up. @@ -660,7 +759,8 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() { // If there's a layout complete message, we need to send location changes. bool had_layout_complete_messages = false; - // If there's a load complete message, we need to send image metrics. + // If there's a load complete message, we need to change the event schedule + // mode. bool had_load_complete_messages = false; ScopedFreezeBlinkAXTreeSource freeze(&tree_source_); @@ -670,7 +770,7 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() { page_language_ = root.Language().Utf8(); // Loop over each event and generate an updated event message. - for (auto& event : src_events) { + for (ui::AXEvent& event : src_events) { if (event.event_type == ax::mojom::Event::kLayoutComplete) had_layout_complete_messages = true; @@ -683,8 +783,22 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() { if (!obj.UpdateLayoutAndCheckValidity()) continue; + // Make sure it's a descendant of our root node - exceptions include the + // scroll area that's the parent of the main document (we ignore it), and + // possibly nodes attached to a different document. + if (!tree_source_.IsInTree(obj)) + continue; + // If it's ignored, find the first ancestor that's not ignored. - while (!obj.IsDetached() && obj.AccessibilityIsIgnored()) { + // + // Note that "IsDetached()" also calls "IsNull()". Additionally, + // "ParentObject()" always gets the first ancestor that is included in tree + // (ignored or unignored), so it will never return objects that are not + // included in the tree at all. + if (!obj.AccessibilityIsIncludedInTree()) + obj = obj.ParentObject(); + for (; !obj.IsDetached() && obj.AccessibilityIsIgnored(); + obj = obj.ParentObject()) { // There are 3 states of nodes that we care about here. // (x) Unignored, included in tree // [x] Ignored, included in tree @@ -702,9 +816,6 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() { // change, but we must also re-serialize [2] since its children // have changed. <1> was never part of the ax tree, and therefore // does not need to be serialized. - // - // So on the way to the Unignored parent, ancestors that are - // included in the tree must also be serialized. // Note that [3] will be serialized to (3) during : // |AXTreeSerializer<>::SerializeChangedNodes| when node [2] is // being serialized, since it will detect the Ignored state had @@ -713,30 +824,28 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() { // Similarly, during Event::kTextChanged, if any Ignored, // but included in tree ancestor uses NameFrom::kContents, // they must also be re-serialized in case the name changed. - if (obj.AccessibilityIsIncludedInTree()) { + if (ShouldSerializeNodeForEvent(obj, event)) { DirtyObject dirty_object; dirty_object.obj = obj; dirty_object.event_from = event.event_from; + dirty_object.event_intents = event.event_intents; dirty_objects.push_back(dirty_object); } - obj = obj.ParentObject(); } - // Make sure it's a descendant of our root node - exceptions include the - // scroll area that's the parent of the main document (we ignore it), and - // possibly nodes attached to a different document. - if (!tree_source_.IsInTree(obj)) - continue; - events.push_back(event); VLOG(1) << "Accessibility event: " << ui::ToString(event.event_type) << " on node id " << event.id; - DirtyObject dirty_object; - dirty_object.obj = obj; - dirty_object.event_from = event.event_from; - dirty_objects.push_back(dirty_object); + // Some events don't cause any changes to their associated objects. + if (ShouldSerializeNodeForEvent(obj, event)) { + DirtyObject dirty_object; + dirty_object.obj = obj; + dirty_object.event_from = event.event_from; + dirty_object.event_intents = event.event_intents; + dirty_objects.push_back(dirty_object); + } } // Popups have a document lifecycle managed separately from the main document @@ -786,6 +895,7 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() { AXContentTreeUpdate update; update.event_from = dirty_objects[i].event_from; + update.event_intents = dirty_objects[i].event_intents; // If there's a plugin, force the tree data to be generated in every // message so the plugin can merge its own tree data changes. if (plugin_tree_source_) @@ -812,26 +922,31 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() { updates.push_back(update); - if (had_load_complete_messages) - RecordImageMetrics(&update); - VLOG(1) << "Accessibility tree update:\n" << update.ToString(); } + event_schedule_status_ = EventScheduleStatus::kWaitingForAck; render_accessibility_manager_->HandleAccessibilityEvents( updates, events, reset_token_, base::BindOnce(&RenderAccessibilityImpl::OnAccessibilityEventsHandled, - weak_factory_.GetWeakPtr())); + weak_factory_for_pending_events_.GetWeakPtr())); reset_token_ = 0; if (had_layout_complete_messages) SendLocationChanges(); - if (had_load_complete_messages) + if (had_load_complete_messages) { has_injected_stylesheet_ = false; + event_schedule_mode_ = EventScheduleMode::kProcessEventsImmediately; + } if (image_annotation_debugging_) AddImageAnnotationDebuggingAttributes(updates); + + ukm::builders::Accessibility_Renderer(document.GetUkmSourceId()) + .SetCpuTime_SendPendingAccessibilityEvents( + timer_.Elapsed().InMilliseconds()) + .Record(ukm_recorder_.get()); } void RenderAccessibilityImpl::SendLocationChanges() { @@ -889,9 +1004,16 @@ void RenderAccessibilityImpl::SendLocationChanges() { } void RenderAccessibilityImpl::OnAccessibilityEventsHandled() { - DCHECK(ack_pending_); - ack_pending_ = false; - SendPendingAccessibilityEvents(); + DCHECK_EQ(event_schedule_status_, EventScheduleStatus::kWaitingForAck); + event_schedule_status_ = EventScheduleStatus::kNotWaiting; + switch (event_schedule_mode_) { + case EventScheduleMode::kDeferEvents: + ScheduleSendPendingAccessibilityEvents(true); + break; + case EventScheduleMode::kProcessEventsImmediately: + SendPendingAccessibilityEvents(); + break; + } } void RenderAccessibilityImpl::OnLoadInlineTextBoxes( @@ -1083,51 +1205,6 @@ void RenderAccessibilityImpl::Scroll(const ui::AXActionTarget* target, target->SetScrollOffset(gfx::Point(x, y)); } -void RenderAccessibilityImpl::RecordImageMetrics(AXContentTreeUpdate* update) { - if (!GetAccessibilityMode().has_mode(ui::AXMode::kScreenReader)) - return; - float scale_factor = render_frame_->GetDeviceScaleFactor(); - for (size_t i = 0; i < update->nodes.size(); ++i) { - ui::AXNodeData& node_data = update->nodes[i]; - if (node_data.role != ax::mojom::Role::kImage) - continue; - // Convert to DIPs based on screen scale factor. - int width = node_data.relative_bounds.bounds.width() / scale_factor; - int height = node_data.relative_bounds.bounds.height() / scale_factor; - if (width == 0 || height == 0) - continue; - // We log the min size in a histogram with a max of 10000, so set a ceiling - // of 10000 on min_size. - int min_size = std::min({width, height, 10000}); - int max_size = std::max(width, height); - // The ratio is always the smaller divided by the larger so as not to go - // over 100%. - int ratio = min_size * 100.0 / max_size; - const std::string name = - node_data.GetStringAttribute(ax::mojom::StringAttribute::kName); - bool explicitly_empty = node_data.GetNameFrom() == - ax::mojom::NameFrom::kAttributeExplicitlyEmpty; - if (!name.empty()) { - UMA_HISTOGRAM_PERCENTAGE( - "Accessibility.ScreenReader.Image.SizeRatio.Labeled", ratio); - UMA_HISTOGRAM_COUNTS_10000( - "Accessibility.ScreenReader.Image.MinSize.Labeled", min_size); - } else if (explicitly_empty) { - UMA_HISTOGRAM_PERCENTAGE( - "Accessibility.ScreenReader.Image.SizeRatio.ExplicitlyUnlabeled", - ratio); - UMA_HISTOGRAM_COUNTS_10000( - "Accessibility.ScreenReader.Image.MinSize.ExplicitlyUnlabeled", - min_size); - } else { - UMA_HISTOGRAM_PERCENTAGE( - "Accessibility.ScreenReader.Image.SizeRatio.Unlabeled", ratio); - UMA_HISTOGRAM_COUNTS_10000( - "Accessibility.ScreenReader.Image.MinSize.Unlabeled", min_size); - } - } -} - void RenderAccessibilityImpl::AddImageAnnotationDebuggingAttributes( const std::vector<AXContentTreeUpdate>& updates) { DCHECK(image_annotation_debugging_); @@ -1208,4 +1285,52 @@ blink::WebDocument RenderAccessibilityImpl::GetPopupDocument() { return WebDocument(); } +WebAXObject RenderAccessibilityImpl::GetPluginRoot() { + ScopedFreezeBlinkAXTreeSource freeze(&tree_source_); + WebAXObject root = tree_source_.GetRoot(); + if (!root.UpdateLayoutAndCheckValidity()) + return WebAXObject(); + + base::queue<WebAXObject> objs_to_explore; + objs_to_explore.push(root); + while (objs_to_explore.size()) { + WebAXObject obj = objs_to_explore.front(); + objs_to_explore.pop(); + + WebNode node = obj.GetNode(); + if (!node.IsNull() && node.IsElementNode()) { + WebElement element = node.To<WebElement>(); + if (element.HasHTMLTagName("embed")) { + return obj; + } + } + + // Explore children of this object. + std::vector<WebAXObject> children; + tree_source_.GetChildren(obj, &children); + for (const auto& child : children) + objs_to_explore.push(child); + } + + return WebAXObject(); +} + +void RenderAccessibilityImpl::CancelScheduledEvents() { + switch (event_schedule_status_) { + case EventScheduleStatus::kScheduledDeferred: + case EventScheduleStatus::kScheduledImmediate: // Fallthrough + weak_factory_for_pending_events_.InvalidateWeakPtrs(); + event_schedule_status_ = EventScheduleStatus::kNotWaiting; + break; + case EventScheduleStatus::kWaitingForAck: + case EventScheduleStatus::kNotWaiting: // Fallthrough + break; + } +} + +RenderAccessibilityImpl::DirtyObject::DirtyObject() = default; +RenderAccessibilityImpl::DirtyObject::DirtyObject(const DirtyObject& other) = + default; +RenderAccessibilityImpl::DirtyObject::~DirtyObject() = default; + } // namespace content diff --git a/chromium/content/renderer/accessibility/render_accessibility_impl.h b/chromium/content/renderer/accessibility/render_accessibility_impl.h index 47814c1adcc..1275797c723 100644 --- a/chromium/content/renderer/accessibility/render_accessibility_impl.h +++ b/chromium/content/renderer/accessibility/render_accessibility_impl.h @@ -12,6 +12,8 @@ #include "base/macros.h" #include "base/memory/weak_ptr.h" #include "content/common/ax_content_node_data.h" +#include "content/common/ax_content_tree_data.h" +#include "content/common/ax_content_tree_update.h" #include "content/common/content_export.h" #include "content/common/render_accessibility.mojom.h" #include "content/public/renderer/plugin_ax_tree_source.h" @@ -37,6 +39,10 @@ class AXActionTarget; struct AXEvent; } +namespace ukm { +class MojoUkmRecorder; +} + namespace content { class AXImageAnnotator; @@ -115,14 +121,16 @@ class CONTENT_EXPORT RenderAccessibilityImpl : public RenderAccessibility, int GenerateAXID() override; void SetPluginTreeSource(PluginAXTreeSource* source) override; void OnPluginRootNodeUpdated() override; + void ShowPluginContextMenu() override; // RenderFrameObserver implementation. void DidCreateNewDocument() override; - void DidCommitProvisionalLoad(bool is_same_document_navigation, - ui::PageTransition transition) override; + void DidCommitProvisionalLoad(ui::PageTransition transition) override; void AccessibilityModeChanged(const ui::AXMode& mode) override; - void HitTest(const ui::AXActionData& action_data, + void HitTest(const gfx::Point& point, + ax::mojom::Event event_to_fire, + int request_id, mojom::RenderAccessibility::HitTestCallback callback); void PerformAction(const ui::AXActionData& data); void Reset(int32_t reset_token); @@ -151,8 +159,25 @@ class CONTENT_EXPORT RenderAccessibilityImpl : public RenderAccessibility, private: struct DirtyObject { + DirtyObject(); + DirtyObject(const DirtyObject& other); + ~DirtyObject(); blink::WebAXObject obj; ax::mojom::EventFrom event_from; + std::vector<ui::AXEventIntent> event_intents; + }; + + enum class EventScheduleMode { kDeferEvents, kProcessEventsImmediately }; + + enum class EventScheduleStatus { + // Events have been scheduled with a delay, but have not been sent. + kScheduledDeferred, + // Events have been scheduled without a delay, but have not been sent. + kScheduledImmediate, + // Events have been sent, waiting for callback. + kWaitingForAck, + // Events are not scheduled and we are not waiting for an ack. + kNotWaiting }; // Callback that will be called from the browser upon handling the message @@ -183,14 +208,28 @@ class CONTENT_EXPORT RenderAccessibilityImpl : public RenderAccessibility, void Scroll(const ui::AXActionTarget* target, ax::mojom::Action scroll_action); - void ScheduleSendAccessibilityEventsIfNeeded(); - void RecordImageMetrics(AXContentTreeUpdate* update); + + // Whether an event should mark its associated object dirty. + bool ShouldSerializeNodeForEvent(const blink::WebAXObject& obj, + const ui::AXEvent& event) const; + + // If we are calling this from a task, scheduling is allowed even if there is + // a running task + void ScheduleSendPendingAccessibilityEvents( + bool scheduling_from_task = false); void AddImageAnnotationDebuggingAttributes( const std::vector<AXContentTreeUpdate>& updates); // Returns the document for the active popup if any. blink::WebDocument GetPopupDocument(); + // Searches the accessibility tree for plugin's root object and returns it. + // Returns an empty WebAXObject if no root object is present. + blink::WebAXObject GetPluginRoot(); + + // Cancels scheduled events that are not yet in flight + void CancelScheduledEvents(); + // The RenderAccessibilityManager that owns us. RenderAccessibilityManager* render_accessibility_manager_; @@ -233,8 +272,8 @@ class CONTENT_EXPORT RenderAccessibilityImpl : public RenderAccessibility, // is fixed. gfx::Size last_scroll_offset_; - // Set if we are waiting for an accessibility event ack. - bool ack_pending_; + // Current event scheduling status + EventScheduleStatus event_schedule_status_; // Nonzero if the browser requested we reset the accessibility state. // We need to return this token in the next IPC. @@ -244,6 +283,9 @@ class CONTENT_EXPORT RenderAccessibilityImpl : public RenderAccessibility, // (only when debugging flags are enabled, never under normal circumstances). bool has_injected_stylesheet_ = false; + // We defer events to improve performance during the initial page load. + EventScheduleMode event_schedule_mode_; + // Whether we should highlight annotation results visually on the page // for debugging. bool image_annotation_debugging_ = false; @@ -251,8 +293,11 @@ class CONTENT_EXPORT RenderAccessibilityImpl : public RenderAccessibility, // The specified page language, or empty if unknown. std::string page_language_; + std::unique_ptr<ukm::MojoUkmRecorder> ukm_recorder_; + // So we can queue up tasks to be executed later. - base::WeakPtrFactory<RenderAccessibilityImpl> weak_factory_{this}; + base::WeakPtrFactory<RenderAccessibilityImpl> + weak_factory_for_pending_events_{this}; friend class AXImageAnnotatorTest; friend class PluginActionHandlingTest; diff --git a/chromium/content/renderer/accessibility/render_accessibility_impl_browsertest.cc b/chromium/content/renderer/accessibility/render_accessibility_impl_browsertest.cc index 3781c691064..1c3dec29b14 100644 --- a/chromium/content/renderer/accessibility/render_accessibility_impl_browsertest.cc +++ b/chromium/content/renderer/accessibility/render_accessibility_impl_browsertest.cc @@ -17,6 +17,7 @@ #include "base/test/task_environment.h" #include "base/time/time.h" #include "build/build_config.h" +#include "content/common/ax_content_tree_update.h" #include "content/common/frame_messages.h" #include "content/common/render_accessibility.mojom-test-utils.h" #include "content/common/render_accessibility.mojom.h" @@ -246,6 +247,42 @@ class RenderAccessibilityImplTest : public RenderViewTest { } ~RenderAccessibilityImplTest() override = default; + void ScheduleSendPendingAccessibilityEvents() { + GetRenderAccessibilityImpl()->ScheduleSendPendingAccessibilityEvents(); + } + + void ExpectScheduleStatusScheduledDeferred() { + EXPECT_EQ(GetRenderAccessibilityImpl()->event_schedule_status_, + RenderAccessibilityImpl::EventScheduleStatus::kScheduledDeferred); + } + + void ExpectScheduleStatusScheduledImmediate() { + EXPECT_EQ( + GetRenderAccessibilityImpl()->event_schedule_status_, + RenderAccessibilityImpl::EventScheduleStatus::kScheduledImmediate); + } + + void ExpectScheduleStatusWaitingForAck() { + EXPECT_EQ(GetRenderAccessibilityImpl()->event_schedule_status_, + RenderAccessibilityImpl::EventScheduleStatus::kWaitingForAck); + } + + void ExpectScheduleStatusNotWaiting() { + EXPECT_EQ(GetRenderAccessibilityImpl()->event_schedule_status_, + RenderAccessibilityImpl::EventScheduleStatus::kNotWaiting); + } + + void ExpectScheduleModeDeferEvents() { + EXPECT_EQ(GetRenderAccessibilityImpl()->event_schedule_mode_, + RenderAccessibilityImpl::EventScheduleMode::kDeferEvents); + } + + void ExpectScheduleModeProcessEventsImmediately() { + EXPECT_EQ( + GetRenderAccessibilityImpl()->event_schedule_mode_, + RenderAccessibilityImpl::EventScheduleMode::kProcessEventsImmediately); + } + protected: RenderViewImpl* view() { return static_cast<RenderViewImpl*>(view_); @@ -404,6 +441,62 @@ TEST_F(RenderAccessibilityImplTest, SendFullAccessibilityTreeOnReload) { EXPECT_EQ(5, CountAccessibilityNodesSentToBrowser()); } +TEST_F(RenderAccessibilityImplTest, TestDeferred) { + constexpr char html[] = R"HTML( + <body> + <div> + a + </div> + </body> + )HTML"; + LoadHTML(html); + task_environment_.RunUntilIdle(); + + // We should have had load complete, causing us to send subsequent events + // without delay. + ExpectScheduleStatusNotWaiting(); + ExpectScheduleModeProcessEventsImmediately(); + + // Simulate a page load to test deferred behavior. + GetRenderAccessibilityImpl()->DidCommitProvisionalLoad( + ui::PageTransition::PAGE_TRANSITION_LINK); + ClearHandledUpdates(); + WebDocument document = GetMainFrame()->GetDocument(); + EXPECT_FALSE(document.IsNull()); + WebAXObject root_obj = WebAXObject::FromWebDocument(document); + EXPECT_FALSE(root_obj.IsNull()); + + // No events should have been scheduled or sent. + ExpectScheduleStatusNotWaiting(); + ExpectScheduleModeDeferEvents(); + + // Send an event, it should be scheduled with a delay. + GetRenderAccessibilityImpl()->HandleAXEvent( + ui::AXEvent(root_obj.AxID(), ax::mojom::Event::kLiveRegionChanged)); + ExpectScheduleStatusScheduledDeferred(); + ExpectScheduleModeDeferEvents(); + + task_environment_.RunUntilIdle(); + // Ensure event is not sent as it is scheduled with a delay. + ExpectScheduleStatusScheduledDeferred(); + ExpectScheduleModeDeferEvents(); + + // Perform action, causing immediate event processing. + ui::AXActionData action; + action.action = ax::mojom::Action::kFocus; + GetRenderAccessibilityImpl()->PerformAction(action); + ScheduleSendPendingAccessibilityEvents(); + + // Ensure task has been scheduled without delay. + ExpectScheduleStatusScheduledImmediate(); + ExpectScheduleModeProcessEventsImmediately(); + + task_environment_.RunUntilIdle(); + // Event has been sent, no longer waiting on ack. + ExpectScheduleStatusNotWaiting(); + ExpectScheduleModeProcessEventsImmediately(); +} + TEST_F(RenderAccessibilityImplTest, HideAccessibilityObject) { // Test RenderAccessibilityImpl and make sure it sends the // proper event to the browser when an object in the tree diff --git a/chromium/content/renderer/accessibility/render_accessibility_manager.cc b/chromium/content/renderer/accessibility/render_accessibility_manager.cc index 2278b175dec..06eb5da41a2 100644 --- a/chromium/content/renderer/accessibility/render_accessibility_manager.cc +++ b/chromium/content/renderer/accessibility/render_accessibility_manager.cc @@ -69,10 +69,13 @@ void RenderAccessibilityManager::FatalError() { } void RenderAccessibilityManager::HitTest( - const ui::AXActionData& action_data, + const gfx::Point& point, + ax::mojom::Event event_to_fire, + int request_id, mojom::RenderAccessibility::HitTestCallback callback) { DCHECK(render_accessibility_); - render_accessibility_->HitTest(action_data, std::move(callback)); + render_accessibility_->HitTest(point, event_to_fire, request_id, + std::move(callback)); } void RenderAccessibilityManager::PerformAction(const ui::AXActionData& data) { diff --git a/chromium/content/renderer/accessibility/render_accessibility_manager.h b/chromium/content/renderer/accessibility/render_accessibility_manager.h index 57ebb7d7311..516c07bbd57 100644 --- a/chromium/content/renderer/accessibility/render_accessibility_manager.h +++ b/chromium/content/renderer/accessibility/render_accessibility_manager.h @@ -9,6 +9,7 @@ #include "base/macros.h" #include "content/common/ax_content_node_data.h" +#include "content/common/ax_content_tree_update.h" #include "content/common/content_export.h" #include "content/common/render_accessibility.mojom.h" #include "mojo/public/cpp/bindings/associated_receiver.h" @@ -60,7 +61,9 @@ class CONTENT_EXPORT RenderAccessibilityManager // mojom::RenderAccessibility implementation. void SetMode(uint32_t ax_mode) override; void FatalError() override; - void HitTest(const ui::AXActionData& action_data, + void HitTest(const gfx::Point& point, + ax::mojom::Event event_to_fire, + int request_id, mojom::RenderAccessibility::HitTestCallback callback) override; void PerformAction(const ui::AXActionData& data) override; void Reset(int32_t reset_token) override; |