summaryrefslogtreecommitdiff
path: root/chromium/content/renderer/accessibility
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/content/renderer/accessibility')
-rw-r--r--chromium/content/renderer/accessibility/aom_content_ax_tree.cc1
-rw-r--r--chromium/content/renderer/accessibility/ax_image_annotator.cc32
-rw-r--r--chromium/content/renderer/accessibility/ax_image_annotator.h2
-rw-r--r--chromium/content/renderer/accessibility/ax_image_stopwords.cc575
-rw-r--r--chromium/content/renderer/accessibility/ax_image_stopwords.h71
-rw-r--r--chromium/content/renderer/accessibility/ax_image_stopwords_unittest.cc116
-rw-r--r--chromium/content/renderer/accessibility/blink_ax_tree_source.cc1076
-rw-r--r--chromium/content/renderer/accessibility/blink_ax_tree_source.h51
-rw-r--r--chromium/content/renderer/accessibility/render_accessibility_impl.cc405
-rw-r--r--chromium/content/renderer/accessibility/render_accessibility_impl.h61
-rw-r--r--chromium/content/renderer/accessibility/render_accessibility_impl_browsertest.cc93
-rw-r--r--chromium/content/renderer/accessibility/render_accessibility_manager.cc7
-rw-r--r--chromium/content/renderer/accessibility/render_accessibility_manager.h5
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;