Repository navigation
Expand file tree
/
Copy pathax_object_cache_impl.cc
More file actions
5174 lines (4457 loc) · 191 KB
/
Copy pathax_object_cache_impl.cc
File metadata and controls
5174 lines (4457 loc) · 191 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright (C) 2014, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
#include "base/auto_reset.h"
#include "base/containers/contains.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/ranges/algorithm.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "third_party/blink/public/mojom/permissions/permission.mojom-blink.h"
#include "third_party/blink/public/mojom/permissions/permission_status.mojom-blink.h"
#include "third_party/blink/public/mojom/render_accessibility.mojom-blink.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/web/web_local_frame_client.h"
#include "third_party/blink/renderer/core/accessibility/scoped_blink_ax_event_intent.h"
#include "third_party/blink/renderer/core/aom/accessible_node.h"
#include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/document_lifecycle.h"
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
#include "third_party/blink/renderer/core/dom/slot_assignment_engine.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
#include "third_party/blink/renderer/core/events/event_util.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
#include "third_party/blink/renderer/core/html/canvas/html_canvas_element.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
#include "third_party/blink/renderer/core/html/forms/html_label_element.h"
#include "third_party/blink/renderer/core/html/forms/html_opt_group_element.h"
#include "third_party/blink/renderer/core/html/forms/html_option_element.h"
#include "third_party/blink/renderer/core/html/forms/html_select_element.h"
#include "third_party/blink/renderer/core/html/forms/listed_element.h"
#include "third_party/blink/renderer/core/html/html_area_element.h"
#include "third_party/blink/renderer/core/html/html_frame_owner_element.h"
#include "third_party/blink/renderer/core/html/html_head_element.h"
#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/core/html/html_object_element.h"
#include "third_party/blink/renderer/core/html/html_plugin_element.h"
#include "third_party/blink/renderer/core/html/html_progress_element.h"
#include "third_party/blink/renderer/core/html/html_script_element.h"
#include "third_party/blink/renderer/core/html/html_slot_element.h"
#include "third_party/blink/renderer/core/html/html_style_element.h"
#include "third_party/blink/renderer/core/html/html_table_cell_element.h"
#include "third_party/blink/renderer/core/html/html_table_element.h"
#include "third_party/blink/renderer/core/html/html_table_row_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/input_type_names.h"
#include "third_party/blink/renderer/core/layout/layout_image.h"
#include "third_party/blink/renderer/core/layout/layout_inline.h"
#include "third_party/blink/renderer/core/layout/layout_text.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.h"
#include "third_party/blink/renderer/core/layout/ng/table/layout_ng_table.h"
#include "third_party/blink/renderer/core/layout/ng/table/layout_ng_table_cell.h"
#include "third_party/blink/renderer/core/layout/ng/table/layout_ng_table_row.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/core/page/focus_controller.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/page/page_animator.h"
#include "third_party/blink/renderer/core/style/content_data.h"
#include "third_party/blink/renderer/core/svg/svg_graphics_element.h"
#include "third_party/blink/renderer/core/svg/svg_style_element.h"
#include "third_party/blink/renderer/modules/accessibility/ax_image_map_link.h"
#include "third_party/blink/renderer/modules/accessibility/ax_inline_text_box.h"
#include "third_party/blink/renderer/modules/accessibility/ax_layout_object.h"
#include "third_party/blink/renderer/modules/accessibility/ax_list_box.h"
#include "third_party/blink/renderer/modules/accessibility/ax_list_box_option.h"
#include "third_party/blink/renderer/modules/accessibility/ax_media_control.h"
#include "third_party/blink/renderer/modules/accessibility/ax_media_element.h"
#include "third_party/blink/renderer/modules/accessibility/ax_menu_list.h"
#include "third_party/blink/renderer/modules/accessibility/ax_menu_list_option.h"
#include "third_party/blink/renderer/modules/accessibility/ax_menu_list_popup.h"
#include "third_party/blink/renderer/modules/accessibility/ax_progress_indicator.h"
#include "third_party/blink/renderer/modules/accessibility/ax_relation_cache.h"
#include "third_party/blink/renderer/modules/accessibility/ax_slider.h"
#include "third_party/blink/renderer/modules/accessibility/ax_validation_message.h"
#include "third_party/blink/renderer/modules/accessibility/ax_virtual_object.h"
#include "third_party/blink/renderer/modules/permissions/permission_utils.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "ui/accessibility/ax_common.h"
#include "ui/accessibility/ax_enums.mojom-blink.h"
#include "ui/accessibility/ax_event.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/mojom/ax_relative_bounds.mojom-blink.h"
// Prevent code that runs during the lifetime of the stack from altering the
// document lifecycle, for the main document, and the popup document if present.
#if DCHECK_IS_ON()
#define SCOPED_DISALLOW_LIFECYCLE_TRANSITION() \
DocumentLifecycle::DisallowTransitionScope scoped(document_->Lifecycle()); \
DocumentLifecycle::DisallowTransitionScope scoped2( \
popup_document_ ? popup_document_->Lifecycle() \
: document_->Lifecycle());
#else
#define SCOPED_DISALLOW_LIFECYCLE_TRANSITION()
#endif // DCHECK_IS_ON()
namespace blink {
namespace {
bool IsInitialEmptyDocument(const Document& document) {
// Do not fire for initial empty top document. This helps avoid thrashing the
// a11y tree, causing an extra serialization.
// TODO(accessibility) This is an ugly special case -- find a better way.
// Note: Document::IsInitialEmptyDocument() did not work -- should it?
if (document.body() && document.body()->hasChildren())
return false;
if (document.head() && document.head()->hasChildren())
return false;
if (document.ParentDocument())
return false;
// No contents and not a child document, return true if about::blank.
return document.Url().IsAboutBlankURL();
}
// Return a node for the current layout object or ancestor layout object.
Node* GetClosestNodeForLayoutObject(const LayoutObject* layout_object) {
if (!layout_object)
return nullptr;
Node* node = layout_object->GetNode();
return node ? node : GetClosestNodeForLayoutObject(layout_object->Parent());
}
// Return true if display locked or inside slot recalc, false otherwise.
// Also returns false if not a safe time to perform the check.
bool IsDisplayLocked(const Node* node, bool inclusive = false) {
if (!node)
return false;
// The IsDisplayLockedPreventingPaint() function may attempt to do
// a flat tree traversal of ancestors. If we're in a flat tree traversal
// forbidden scope, return false. Additionally, flat tree traversal
// might call AssignedSlot, so if we're in a slot assignment recalc
// forbidden scope, return false.
if (node->GetDocument().IsFlatTreeTraversalForbidden() ||
node->GetDocument()
.GetSlotAssignmentEngine()
.HasPendingSlotAssignmentRecalc()) {
return false; // Cannot safely perform this check now.
}
return DisplayLockUtilities::IsDisplayLockedPreventingPaint(node, inclusive);
}
bool IsDisplayLocked(const LayoutObject* object) {
bool inclusive = false;
while (object) {
if (const auto* node = object->GetNode())
return IsDisplayLocked(node, inclusive);
inclusive = true;
object = object->Parent();
}
return false;
}
bool IsActive(Document& document) {
return document.IsActive() && !document.IsDetached();
}
bool HasAriaCellRole(Element* elem) {
DCHECK(elem);
const AtomicString& role_str = elem->FastGetAttribute(html_names::kRoleAttr);
if (role_str.empty())
return false;
return ui::IsCellOrTableHeader(AXObject::AriaRoleStringToRoleEnum(role_str));
}
// Can role="presentation" aka "none" propagate to descendants of this node?
// Example: it propagates from table->tbody->tr->td, making them all ignored.
bool RolePresentationPropagates(Node* node) {
// Check for list markup.
if (IsA<HTMLMenuElement>(node) || IsA<HTMLUListElement>(node) ||
IsA<HTMLOListElement>(node)) {
return true;
}
// Check for <table>.
if (IsA<HTMLTableElement>(node))
return true; // table section, table row, table cells,
// Check for display: table CSS.
if (node->GetLayoutObject() && node->GetLayoutObject()->IsTable())
return true;
return false;
}
// Return true if whitespace is not necessary to keep adjacent_node separate
// in screen reader output from surrounding nodes.
bool CanIgnoreSpaceNextTo(LayoutObject* layout_object,
bool is_after,
int counter = 0) {
if (!layout_object)
return true;
if (counter > 3)
return false; // Don't recurse more than 3 times.
auto* elem = DynamicTo<Element>(layout_object->GetNode());
// Can usually ignore space next to a <br>.
// Exception: if the space was next to a <br> with an ARIA role.
if (layout_object->IsBR()) {
// As an example of a <br> with a role, Google Docs uses:
// <span contenteditable=false> <br role="presentation></span>.
// This construct hides the <br> from the AX tree and uses the space
// instead, presenting a hard line break as a soft line break.
DCHECK(elem);
return !is_after || !elem->FastHasAttribute(html_names::kRoleAttr);
}
// If adjacent to a whitespace character, the current space can be ignored.
if (layout_object->IsText()) {
auto* layout_text = To<LayoutText>(layout_object);
if (layout_text->HasEmptyText())
return false;
if (layout_text->GetText().Impl()->ContainsOnlyWhitespaceOrEmpty())
return true;
auto adjacent_char =
is_after ? layout_text->FirstCharacterAfterWhitespaceCollapsing()
: layout_text->LastCharacterAfterWhitespaceCollapsing();
return adjacent_char == ' ' || adjacent_char == '\n' ||
adjacent_char == '\t';
}
// Keep spaces between images and other visible content.
if (layout_object->IsLayoutImage())
return false;
// Do not keep spaces between blocks.
if (!layout_object->IsLayoutInline())
return true;
// If next to an element that a screen reader will always read separately,
// the the space can be ignored.
// Elements that are naturally focusable even without a tabindex tend
// to be rendered separately even if there is no space between them.
// Some ARIA roles act like table cells and don't need adjacent whitespace to
// indicate separation.
// False negatives are acceptable in that they merely lead to extra whitespace
// static text nodes.
if (elem && HasAriaCellRole(elem))
return true;
// Test against the appropriate child text node.
auto* layout_inline = To<LayoutInline>(layout_object);
LayoutObject* child =
is_after ? layout_inline->FirstChild() : layout_inline->LastChild();
if (!child && elem) {
// No children of inline element. Check adjacent sibling in same direction.
Node* adjacent_node =
is_after ? NodeTraversal::NextIncludingPseudoSkippingChildren(*elem)
: NodeTraversal::PreviousAbsoluteSiblingIncludingPseudo(*elem);
return adjacent_node &&
CanIgnoreSpaceNextTo(adjacent_node->GetLayoutObject(), is_after,
++counter);
}
return CanIgnoreSpaceNextTo(child, is_after, ++counter);
}
bool IsLayoutTextRelevantForAccessibility(const LayoutText& layout_text) {
if (!layout_text.Parent())
return false;
const Node* node = layout_text.GetNode();
DCHECK(node); // Anonymous text is processed earlier, doesn't reach here.
#if DCHECK_IS_ON()
DCHECK(node->GetDocument().Lifecycle().GetState() >=
DocumentLifecycle::kAfterPerformLayout)
<< "Unclean document at lifecycle "
<< node->GetDocument().Lifecycle().ToString();
#endif
// Ignore empty text.
if (layout_text.HasEmptyText())
return false;
// Always keep if anything other than collapsible whitespace.
if (!layout_text.IsAllCollapsibleWhitespace() || layout_text.IsBR())
return true;
// Will now look at sibling nodes. We need the closest element to the
// whitespace markup-wise, e.g. tag1 in these examples:
// [whitespace] <tag1><tag2>x</tag2></tag1>
// <span>[whitespace]</span> <tag1><tag2>x</tag2></tag1>.
// Do not use LayoutTreeBuilderTraversal or FlatTreeTraversal as this may need
// to be called during slot assignment, when flat tree traversal is forbidden.
Node* prev_node =
NodeTraversal::PreviousAbsoluteSiblingIncludingPseudo(*node);
if (!prev_node)
return false;
Node* next_node = NodeTraversal::NextIncludingPseudoSkippingChildren(*node);
if (!next_node)
return false;
// Ignore extra whitespace-only text if a sibling will be presented
// separately by screen readers whether whitespace is there or not.
if (CanIgnoreSpaceNextTo(prev_node->GetLayoutObject(), false) ||
CanIgnoreSpaceNextTo(next_node->GetLayoutObject(), true)) {
return false;
}
// If the prev/next node is also a text node and the adjacent character is
// not whitespace, CanIgnoreSpaceNextTo will return false. In some cases that
// is what we want; in other cases it is not. Examples:
//
// 1a: <p><span>Hello</span><span>[whitespace]</span><span>World</span></p>
// 1b: <p><span>Hello</span>[whitespace]<span>World</span></p>
// 2: <div><ul><li style="display:inline;">x</li>[whitespace]</ul>y</div>
//
// In the first case, we want to preserve the whitespace (crbug.com/435765).
// In the second case, the whitespace in the markup is not relevant because
// the "x" is separated from the "y" by virtue of being inside a different
// block. In order to distinguish these two scenarios, we can use the
// LayoutBox associated with each node. For the first scenario, each node's
// LayoutBox is the LayoutBlockFlow associated with the <p>. For the second
// scenario, the LayoutBox of "x" and the whitespace is the LayoutBlockFlow
// associated with the <ul>; the LayoutBox of "y" is the one associated with
// the <div>.
LayoutBox* box = layout_text.EnclosingBox();
if (!box)
return false;
if (prev_node->GetLayoutObject() && prev_node->GetLayoutObject()->IsText()) {
LayoutBox* prev_box = prev_node->GetLayoutObject()->EnclosingBox();
if (prev_box != box)
return false;
}
if (next_node->GetLayoutObject() && next_node->GetLayoutObject()->IsText()) {
LayoutBox* next_box = next_node->GetLayoutObject()->EnclosingBox();
if (next_box != box)
return false;
}
return true;
}
bool IsHiddenTextNodeRelevantForAccessibility(const Text& text_node,
bool is_display_locked) {
// Children of an <iframe> tag will always be replaced by a new Document,
// either loaded from the iframe src or empty. In fact, we don't even parse
// them and they are treated like one text node. Consider irrelevant.
if (AXObject::IsFrame(text_node.parentElement()))
return false;
// Layout has more info available to determine if whitespace is relevant.
// If display-locked, layout object may be missing or stale:
// Assume that all display-locked text nodes are relevant, but only create
// an AXNodeObject in order to avoid using a stale layout object.
if (is_display_locked)
return true;
// If unrendered + no parent, it is in a shadow tree. Consider irrelevant.
if (!text_node.parentElement()) {
DCHECK(text_node.IsInShadowTree());
return false;
}
// If unrendered and in <canvas>, consider even whitespace relevant.
if (text_node.parentElement()->IsInCanvasSubtree())
return true;
// Must be unrendered because of CSS. Consider relevant if non-whitespace.
// Allowing rendered non-whitespace to be considered relevant will allow
// use for accessible relations such as labelledby and describedby.
return !text_node.ContainsOnlyWhitespaceOrEmpty();
}
bool IsShadowContentRelevantForAccessibility(const Node* node) {
DCHECK(node->ContainingShadowRoot());
// Return false if inside a shadow tree of something that can't have children,
// for example, an <img> has a user agent shadow root containing a <span> for
// the alt text. Do not create an accessible for that as it would be unable
// to have a parent that has it as a child.
if (!AXObject::CanHaveChildren(To<Element>(*node->OwnerShadowHost()))) {
return false;
}
// Native <img> create extra child nodes to hold alt text, which are not
// allowed as children. Note: images can have image map children, but these
// are moved from the <map> descendants and are not descendants of the image.
// See AXNodeObject::AddImageMapChildren().
if (node->IsInUserAgentShadowRoot() &&
IsA<HTMLImageElement>(node->OwnerShadowHost())) {
return false;
}
// Don't use non-<option> descendants of an AXMenuList.
// If the UseAXMenuList flag is on, we use a specialized class AXMenuList
// for handling the user-agent shadow DOM exposed by a <select> element.
// That class adds a mock AXMenuListPopup, which adds AXMenuListOption
// children for <option> descendants only.
if (AXObjectCacheImpl::UseAXMenuList() && node->IsInUserAgentShadowRoot() &&
!IsA<HTMLOptionElement>(node)) {
// Find any ancestor <select> if it is present.
Node* host = node->OwnerShadowHost();
auto* select_element = DynamicTo<HTMLSelectElement>(host);
if (!select_element) {
// An <optgroup> can be a shadow host too -- look for it's owner <select>.
if (auto* opt_group_element = DynamicTo<HTMLOptGroupElement>(host))
select_element = opt_group_element->OwnerSelectElement();
}
if (select_element) {
if (!select_element->GetLayoutObject())
return select_element->IsInCanvasSubtree();
// Non-option: only create AXObject if not inside an AXMenuList.
return !AXObjectCacheImpl::ShouldCreateAXMenuListFor(
select_element->GetLayoutObject());
}
}
// Outside of AXMenuList descendants, all other non-slot user agent shadow
// nodes are relevant.
const HTMLSlotElement* slot_element =
ToHTMLSlotElementIfSupportsAssignmentOrNull(node);
if (!slot_element)
return true;
// Slots are relevant if they have content.
// However, this can only be checked during safe times.
// During other times we must assume that the <slot> is relevant.
// TODO(accessibility) Consider removing this rule, but it will require
// a different way of dealing with these PDF test failures:
// https://chromium-review.googlesource.com/c/chromium/src/+/2965317
// For some reason the iframe tests hang, waiting for content to change. In
// other words, returning true here causes some tree updates not to occur.
if (node->GetDocument().IsFlatTreeTraversalForbidden() ||
node->GetDocument()
.GetSlotAssignmentEngine()
.HasPendingSlotAssignmentRecalc()) {
return true;
}
// If the slot element's host is an <object>/<embed>with any descendant nodes
// (including whitespace), LayoutTreeBuilderTraversal::FirstChild will
// return a node. We should only treat that node as slot content if it is
// being used as fallback content.
if (const HTMLPlugInElement* plugin_element =
DynamicTo<HTMLPlugInElement>(node->OwnerShadowHost())) {
return plugin_element->UseFallbackContent();
}
return LayoutTreeBuilderTraversal::FirstChild(*slot_element);
}
bool IsLayoutObjectRelevantForAccessibility(const LayoutObject& layout_object) {
if (layout_object.IsAnonymous()) {
// Anonymous means there is no DOM node, and it's been inserted by the
// layout engine within the tree. An example is an anonymous block that is
// inserted as a parent of an inline where there are block siblings.
return AXObjectCacheImpl::IsRelevantPseudoElementDescendant(layout_object);
}
if (layout_object.IsText())
return IsLayoutTextRelevantForAccessibility(To<LayoutText>(layout_object));
// An AXMenuListOption will be created, which is a subclass of AXNodeObject,
// not of AXLayoutObject.
if (AXObjectCacheImpl::ShouldCreateAXMenuListOptionFor(
layout_object.GetNode())) {
return false;
}
// An AXImageMapLink will be created, which is a subclass of AXNodeObject, not
// of AXLayoutObject.
if (IsA<HTMLAreaElement>(layout_object.GetNode()))
return false;
return true;
}
bool IsSubtreePrunedForAccessibility(const Element* node) {
if (IsA<HTMLAreaElement>(node) && !IsA<HTMLMapElement>(node->parentNode()))
return true; // <area> without parent <map> is not relevant.
if (IsA<HTMLMapElement>(node))
return true; // Contains children for an img, but is not its own object.
if (node->HasTagName(html_names::kColgroupTag))
return true; // Affects table layout, but doesn't get it's own AXObject.
if (node->IsPseudoElement()) {
if (!AXObjectCacheImpl::IsRelevantPseudoElement(*node))
return true;
}
if (const HTMLSlotElement* slot =
ToHTMLSlotElementIfSupportsAssignmentOrNull(node)) {
if (!AXObjectCacheImpl::IsRelevantSlotElement(*slot))
return true;
}
// <optgroup> is irrelevant inside of a <select> menulist.
if (auto* opt_group = DynamicTo<HTMLOptGroupElement>(node)) {
if (auto* select = opt_group->OwnerSelectElement()) {
if (select->UsesMenuList())
return true;
}
}
// An HTML <title> does not require an AXObject: the document's name is
// retrieved directly via the inner text.
if (IsA<HTMLTitleElement>(node))
return true;
return false;
}
// Return true if node is head/style/script or any descendant of those.
// Also returns true for descendants of any type of frame, because the frame
// itself is in the tree, but not DOM descendants (their contents are in a
// different document).
bool IsInPrunableHiddenContainerInclusive(const Node& node,
bool parent_ax_known,
bool is_display_locked) {
int max_depth_to_check = INT_MAX;
if (parent_ax_known) {
// Optimization: only need to check the current object if the parent the
// parent_ax is already known, because it we are attempting to add this
// object from something already relevant in the AX tree, and therefore
// can't be inside a <head>, <style>, <script> or SVG <style> element.
// However, there is an edge case that if it is display locked content
// we must also check the parent, which can be visible and included
// in the tree. This edge case is handled to satisfy tests and is not
// likely to be a real-world condition.
max_depth_to_check = is_display_locked ? 2 : 1;
}
for (const Node* ancestor = &node; ancestor;
ancestor = ancestor->parentElement()) {
// Objects inside <head> are pruned.
if (IsA<HTMLHeadElement>(ancestor))
return true;
// Objects inside a <style> are pruned.
if (IsA<HTMLStyleElement>(ancestor))
return true;
// Objects inside a <script> are true.
if (IsA<HTMLScriptElement>(ancestor))
return true;
// Elements inside of a frame/iframe are true unless inside a document
// that is a child of the frame. In the case where descendants are allowed,
// they will be in a different document, and therefore this loop will not
// reach the frame/iframe.
if (AXObject::IsFrame(ancestor))
return true;
// Style elements in SVG are not display: none, unlike HTML style
// elements, but they are still hidden along with their contents and thus
// treated as true for accessibility.
if (IsA<SVGStyleElement>(ancestor))
return true;
if (--max_depth_to_check <= 0)
break;
}
// All other nodes are relevant, even if hidden.
return false;
}
// -----------------------------------------------------------------------------
// DetermineAXObjectType() determines what type of AXObject should be created
// for the given node and layout_object.
// * Pass in the Node, the LayoutObject or both.
// * Passing in |parent_ax_known| when there is known parent is an optimization
// and does not affect the return value.
// Some general rules:
// * If neither the node nor layout object are relevant for accessibility, will
// return kPruneSubtree, which will cause no AXObject to be created, and
// result in the entire subtree being pruned at that point.
// * If the node is part of a forbidden subtree, then kPruneSubtree is used.
// * If both the node and layout are relevant, kAXLayoutObject is preferred,
// otherwise: kAXNodeObject for relevant nodes, kLayoutObject for layout.
// -----------------------------------------------------------------------------
AXObjectType DetermineAXObjectType(const Node* node,
const LayoutObject* layout_object,
bool parent_ax_known = false) {
DCHECK(layout_object || node);
bool is_display_locked =
node ? IsDisplayLocked(node) : IsDisplayLocked(layout_object);
if (is_display_locked)
layout_object = nullptr;
DCHECK(!node || !layout_object || layout_object->GetNode() == node);
bool is_node_relevant = false;
if (node) {
if (!node->isConnected()) {
return kPruneSubtree;
}
if (node->ContainingShadowRoot() &&
!IsShadowContentRelevantForAccessibility(node)) {
return kPruneSubtree;
}
if (!IsA<Element>(node) && !IsA<Text>(node)) {
// All remaining types, such as the document node, doctype node.
return layout_object ? kAXLayoutObject : kPruneSubtree;
}
if (const Element* element = DynamicTo<Element>(node)) {
if (IsSubtreePrunedForAccessibility(element))
return kPruneSubtree;
else
is_node_relevant = true;
} else { // Text is the only remaining type.
if (layout_object) {
// If there's layout for this text, it will either be pruned or an
// AXLayoutObject will be created for it. The logic of whether to return
// kAXLayoutObject or kPruneSubtree will come purely from
// is_layout_relevant further down.
return IsLayoutObjectRelevantForAccessibility(*layout_object)
? kAXLayoutObject
: kPruneSubtree;
} else {
// Otherwise, base the decision on the best info we have on the node.
is_node_relevant = IsHiddenTextNodeRelevantForAccessibility(
To<Text>(*node), is_display_locked);
}
}
}
bool is_layout_relevant =
layout_object && IsLayoutObjectRelevantForAccessibility(*layout_object);
// Prune if neither the LayoutObject nor Node are relevant.
if (!is_layout_relevant && !is_node_relevant)
return kPruneSubtree;
// If a node is not rendered, prune if it is in head/style/script or a DOM
// descendant of an iframe.
if (!is_layout_relevant && IsInPrunableHiddenContainerInclusive(
*node, parent_ax_known, is_display_locked)) {
return kPruneSubtree;
}
return is_layout_relevant ? kAXLayoutObject : kAXNodeObject;
}
} // namespace
// static
bool AXObjectCacheImpl::use_ax_menu_list_ = false;
// static
AXObjectCache* AXObjectCacheImpl::Create(Document& document,
const ui::AXMode& ax_mode) {
return MakeGarbageCollected<AXObjectCacheImpl>(document, ax_mode);
}
AXObjectCacheImpl::AXObjectCacheImpl(Document& document,
const ui::AXMode& ax_mode)
: document_(document),
ax_mode_(ax_mode),
validation_message_axid_(0),
active_aria_modal_dialog_(nullptr),
relation_cache_(std::make_unique<AXRelationCache>(this)),
accessibility_event_permission_(mojom::blink::PermissionStatus::ASK),
permission_service_(document.GetExecutionContext()),
permission_observer_receiver_(this, document.GetExecutionContext()),
render_accessibility_host_(document.GetExecutionContext()),
ax_tree_source_(BlinkAXTreeSource::Create(*this)),
ax_tree_serializer_(std::make_unique<ui::AXTreeSerializer<AXObject*>>(
ax_tree_source_,
/*crash_on_error*/ true)) {
use_ax_menu_list_ = GetSettings()->GetUseAXMenuList();
}
AXObjectCacheImpl::~AXObjectCacheImpl() {
#if DCHECK_IS_ON()
DCHECK(has_been_disposed_);
#endif
}
// This is called shortly before the AXObjectCache is deleted.
// The destruction of the AXObjectCache will do most of the cleanup.
void AXObjectCacheImpl::Dispose() {
DCHECK(!has_been_disposed_) << "Something is wrong, trying to dispose twice.";
// Don't perform expensive computations while tearing down.
has_been_disposed_ = true;
// Detach all objects now. This prevents more work from occurring if we wait
// for the rendering engine to detach each node individually, because that
// will cause the renderer to attempt to potentially repair parents, and
// detach each child individually as Detach() calls ClearChildren().
// TODO(accessibility) We could just remove this method if code that checks
// HasBeenDisposed()/has_been_disposed_ had another way to check for shutdown.
for (auto& entry : objects_) {
AXObject* obj = entry.value;
obj->Detach();
}
}
void AXObjectCacheImpl::AddInspectorAgent(InspectorAccessibilityAgent* agent) {
agents_.insert(agent);
}
void AXObjectCacheImpl::RemoveInspectorAgent(
InspectorAccessibilityAgent* agent) {
agents_.erase(agent);
}
AXObject* AXObjectCacheImpl::Root() {
if (AXObject* root = SafeGet(document_))
return root;
return GetOrCreate(document_);
}
AXObject* AXObjectCacheImpl::ObjectFromAXID(AXID id) const {
auto it = objects_.find(id);
return it != objects_.end() ? it->value : nullptr;
}
Node* AXObjectCacheImpl::FocusedElement() {
Node* focused_node = document_->FocusedElement();
if (!focused_node)
focused_node = document_;
// A popup is showing: return the focus within instead of the focus in the
// main document. Do not do this for HTML <select>, which has special
// focus manager using the kActiveDescendantId.
if (GetPopupDocumentIfShowing() && !IsA<HTMLSelectElement>(focused_node)) {
if (Node* focus_in_popup = GetPopupDocumentIfShowing()->FocusedElement())
return focus_in_popup;
}
return focused_node;
}
void AXObjectCacheImpl::UpdateLifecycleIfNeeded(Document& document) {
DCHECK(document.defaultView());
DCHECK(document.GetFrame());
DCHECK(document.View());
document.View()->UpdateAllLifecyclePhasesExceptPaint(
DocumentUpdateReason::kAccessibility);
}
void AXObjectCacheImpl::UpdateAXForAllDocuments() {
#if DCHECK_IS_ON()
DCHECK(!IsFrozen())
<< "Don't call UpdateAXForAllDocuments() here; layout and a11y are "
"already clean at the start of serialization.";
DCHECK(!updating_layout_and_ax_) << "Undesirable recursion.";
base::AutoReset<bool> updating(&updating_layout_and_ax_, true);
#endif
// First update the layout for the main and popup document.
UpdateLifecycleIfNeeded(GetDocument());
if (Document* popup_document = GetPopupDocumentIfShowing())
UpdateLifecycleIfNeeded(*popup_document);
// Next flush all accessibility events and dirty objects, for both the main
// and popup document.
if (IsDirty() || HasDirtyObjects())
ProcessDeferredAccessibilityEvents(GetDocument());
}
AXObject* AXObjectCacheImpl::GetOrCreateFocusedObjectFromNode(Node* node) {
#if DCHECK_IS_ON()
DCHECK(GetDocument().Lifecycle().GetState() >=
DocumentLifecycle::kAfterPerformLayout);
if (GetPopupDocumentIfShowing()) {
DCHECK(GetPopupDocumentIfShowing()->Lifecycle().GetState() >=
DocumentLifecycle::kAfterPerformLayout);
}
#endif
AXObject* obj = GetOrCreate(node);
if (!obj)
return nullptr;
Settings* settings = GetSettings();
if (settings && settings->GetAriaModalPrunesAXTree()) {
// It is possible for the active_aria_modal_dialog_ to become detached in
// between the time a node claims focus and the time we notify platforms
// of that focus change. For instance given an aria-modal dialog which was
// newly unhidden (rather than newly added to the DOM):
// * HandleFocusedUIElementChanged calls UpdateActiveAriaModalDialog
// * UpdateActiveAriaModalDialog sets the value of active_aria_modal_dialog_
// and then marks the entire tree dirty if that value changed.
// * The subsequent tree update results in the stored active dialog being
// detached and replaced.
// Should this occur, the focused node we're getting or creating here is
// not a descendant of active_aria_modal_dialog_ and is thus pruned from
// the tree. This leads to firing the event on the included parent object,
// which is likely a non-focusable container.
// We could probably address this situation in one of the clean-layout
// functions (e.g. HandleNodeGainedFocusWithCleanLayout). However, because
// both HandleNodeGainedFocusWithCleanLayout and FocusedObject call
// GetOrCreateFocusedObjectFromNode, detecting and correcting this issue
// here seems like it covers more bases.
// TODO(crbug.com/1328815): We need to take a close look at the aria-modal
// tree pruning logic to be sure there are not other situations where we
// incorrectly prune content which should be exposed.
if (active_aria_modal_dialog_ && active_aria_modal_dialog_->IsDetached())
UpdateActiveAriaModalDialog(node);
}
// the HTML element, for example, is focusable but has an AX object that is
// ignored
if (!obj->AccessibilityIsIncludedInTree())
obj = obj->ParentObjectIncludedInTree();
return obj;
}
AXObject* AXObjectCacheImpl::FocusedObject() {
return GetOrCreateFocusedObjectFromNode(FocusedElement());
}
const ui::AXMode& AXObjectCacheImpl::GetAXMode() {
return ax_mode_;
}
void AXObjectCacheImpl::SetAXMode(const ui::AXMode& ax_mode) {
ax_mode_ = ax_mode;
}
AXObject* AXObjectCacheImpl::Get(const LayoutObject* layout_object,
AXObject* parent_for_repair) {
if (!layout_object)
return nullptr;
if (Node* node = layout_object->GetNode()) {
// If there is a node, it is preferred for backing the AXObject.
DCHECK(!layout_object_mapping_.Contains(layout_object));
return Get(node);
}
auto it_id = layout_object_mapping_.find(layout_object);
if (it_id == layout_object_mapping_.end()) {
return nullptr;
}
AXID ax_id = it_id->value;
DCHECK(!WTF::IsHashTraitsDeletedValue<HashTraits<AXID>>(ax_id));
if (IsDisplayLocked(layout_object) ||
!IsLayoutObjectRelevantForAccessibility(*layout_object)) {
// Change from AXLayoutObject -> AXNodeObject.
// We previously saved the node in the cache with its layout object,
// but now it's in a locked subtree so we should remove the entry with its
// layout object and replace it with an AXNodeObject created from the node
// instead. Do this later at a safe time.
Remove(const_cast<LayoutObject*>(layout_object));
return nullptr;
}
auto it_result = objects_.find(ax_id);
AXObject* result = it_result != objects_.end() ? it_result->value : nullptr;
DCHECK(result) << "Had AXID for Node but no entry in objects_";
DCHECK(result->IsAXNodeObject());
// Do not allow detached objects except when disposing entire tree.
DCHECK(!result->IsDetached() || has_been_disposed_)
<< "Detached AXNodeObject in map: "
<< "AXID#" << ax_id << " LayoutObject=" << layout_object;
if (result->CachedParentObject()) {
DCHECK(!parent_for_repair ||
parent_for_repair == result->CachedParentObject())
<< "If there is both a previous parent, and a parent supplied for "
"repair, they must match.";
} else if (parent_for_repair) {
result->SetParent(parent_for_repair);
}
// If there is no node for the AXObject, then it is an anonymous layout
// object (e.g. a pseudo-element or object introduced to match the structure
// of content). Such objects can only be created or destroyed via creation of
// their parents and recursion via AddPseudoElementChildrenFromLayoutTree.
// RepairMissingParent will not be able to restore a missing parent; instead
// we should never need to do that.
DCHECK(!result->IsMissingParent() || !result->GetNode())
<< "Had AXObject but is missing parent: " << layout_object << " "
<< result->ToString(true, true);
return result;
}
AXObject* AXObjectCacheImpl::SafeGet(const Node* node,
bool allow_display_locking_invalidation,
bool allow_layout_object_relevance_check) {
if (!node)
return nullptr;
#if DCHECK_IS_ON()
if (const Element* element = DynamicTo<Element>(node)) {
if (AccessibleNode* accessible_node = element->ExistingAccessibleNode()) {
DCHECK(!accessible_node_mapping_.Contains(accessible_node))
<< "The accessible node directly attached to an element should not "
"have its own AXObject: "
<< element;
}
}
#endif
auto iter = node_object_mapping_.find(node);
if (iter == node_object_mapping_.end()) {
return nullptr;
}
AXID node_id = iter->value;
auto it_result = objects_.find(node_id);
if (it_result == objects_.end()) {
return nullptr;
}
AXObject* result = it_result->value;
DCHECK(result) << "AXID#" << node_id
<< " in map, but matches an AXObject of null, for " << node;
// When shutting down, allow detached nodes to be in the map, and do not
// attempt invalidations.
if (has_been_disposed_) {
return result->IsDetached() ? nullptr : result;
}
DCHECK(!result->IsDetached()) << "Detached object was in map.";
// Compute whether an allowed invalidation is necessary that alter whether the
// current object should be an AXLayoutObject vs AXNodeObject.
bool is_ax_layout_object = IsA<AXLayoutObject>(result);
bool should_be_ax_layout_object = is_ax_layout_object;
if (node->GetLayoutObject()) {
if (allow_display_locking_invalidation) {
// Nodes that are display locked may have stale layout objects. We enforce
// that they use AXNodeObject because the layout is not up-to-date.
should_be_ax_layout_object = !IsDisplayLocked(node);
}
if (should_be_ax_layout_object && allow_layout_object_relevance_check &&
!IsLayoutObjectRelevantForAccessibility(*node->GetLayoutObject())) {
should_be_ax_layout_object = false;
}
}
// Process any computed invalidation, queuing the work for later.
// TODO(crbug.com/1380449) Replace this system with notifications that
// invalidate based on Blink's knowledge of what's changing, instead of
// checking objects when we touch them.
if (is_ax_layout_object != should_be_ax_layout_object) {
Invalidate(node->GetDocument(), result->AXObjectID());
}
return result;
}
AXObject* AXObjectCacheImpl::Get(const Node* node) {
return SafeGet(node, /* allow_display_locking_invalidation */ true,
/* allow_layout_object_relevance_check */ true);
}
AXObject* AXObjectCacheImpl::Get(NGAbstractInlineTextBox* inline_text_box) {
if (!inline_text_box)
return nullptr;
auto it_ax = inline_text_box_object_mapping_.find(inline_text_box);
AXID ax_id =
it_ax != inline_text_box_object_mapping_.end() ? it_ax->value : 0;
DCHECK(!WTF::IsHashTraitsDeletedValue<HashTraits<AXID>>(ax_id));
if (!ax_id)
return nullptr;